create basic frontend
This commit is contained in:
@@ -194,11 +194,14 @@ func main() {
|
||||
SELECT 'albums', 500000
|
||||
WHERE NOT EXISTS (SELECT * FROM sqlite_sequence)
|
||||
`)
|
||||
orm.Exec(`
|
||||
INSERT INTO users(username, password)
|
||||
SELECT 'admin', 'admin'
|
||||
WHERE NOT EXISTS (SELECT * FROM users)
|
||||
`)
|
||||
orm.FirstOrCreate(&db.User{}, db.User{
|
||||
Name: "admin",
|
||||
Password: "admin",
|
||||
})
|
||||
orm.FirstOrCreate(&db.User{}, db.User{
|
||||
Name: "senan",
|
||||
Password: "password",
|
||||
})
|
||||
startTime := time.Now()
|
||||
tx = orm.Begin()
|
||||
err := godirwalk.Walk(os.Args[1], &godirwalk.Options{
|
||||
|
||||
@@ -9,6 +9,11 @@ import (
|
||||
"github.com/sentriz/gonic/handler"
|
||||
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
"github.com/wader/gormstore"
|
||||
)
|
||||
|
||||
var (
|
||||
dbCon = db.New()
|
||||
)
|
||||
|
||||
type middleware func(next http.HandlerFunc) http.HandlerFunc
|
||||
@@ -25,17 +30,15 @@ func newChain(wares ...middleware) middleware {
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
address := ":5000"
|
||||
func setSubsonicRoutes(mux *http.ServeMux) {
|
||||
cont := handler.Controller{
|
||||
DB: db.New(),
|
||||
DB: dbCon,
|
||||
}
|
||||
withWare := newChain(
|
||||
cont.LogConnection,
|
||||
cont.EnableCORS,
|
||||
cont.CheckParameters,
|
||||
)
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/rest/ping", withWare(cont.Ping))
|
||||
mux.HandleFunc("/rest/ping.view", withWare(cont.Ping))
|
||||
mux.HandleFunc("/rest/stream", withWare(cont.Stream))
|
||||
@@ -56,11 +59,31 @@ func main() {
|
||||
mux.HandleFunc("/rest/getAlbumList2.view", withWare(cont.GetAlbumList))
|
||||
mux.HandleFunc("/rest/getLicense", withWare(cont.GetLicence))
|
||||
mux.HandleFunc("/rest/getLicense.view", withWare(cont.GetLicence))
|
||||
mux.HandleFunc("/", withWare(cont.NotFound))
|
||||
// mux.HandleFunc("/rest/getMusicDirectory", withWare(cont.GetMusicDirectory))
|
||||
// mux.HandleFunc("/rest/getMusicDirectory.view", withWare(cont.GetMusicDirectory))
|
||||
// mux.HandleFunc("/rest/getIndexes", withWare(cont.GetIndexes))
|
||||
// mux.HandleFunc("/rest/getIndexes.view", withWare(cont.GetIndexes))
|
||||
}
|
||||
|
||||
func setAdminRoutes(mux *http.ServeMux) {
|
||||
cont := handler.Controller{
|
||||
DB: dbCon,
|
||||
SStore: gormstore.New(dbCon, []byte("saynothinboys")),
|
||||
}
|
||||
withWare := newChain(
|
||||
cont.LogConnection,
|
||||
cont.EnableCORS,
|
||||
)
|
||||
server := http.FileServer(http.Dir("static"))
|
||||
mux.Handle("/admin/static/", http.StripPrefix("/admin/static/", server))
|
||||
mux.HandleFunc("/admin/login", withWare(cont.ServeLogin))
|
||||
mux.HandleFunc("/admin/authenticate", withWare(cont.ServeAuthenticate))
|
||||
mux.HandleFunc("/admin/home", withWare(cont.ServeHome))
|
||||
mux.HandleFunc("/admin/create_user", withWare(cont.ServeCreateUser))
|
||||
mux.HandleFunc("/admin/logout", withWare(cont.ServeLogout))
|
||||
}
|
||||
|
||||
func main() {
|
||||
address := ":5000"
|
||||
mux := http.NewServeMux()
|
||||
setSubsonicRoutes(mux)
|
||||
setAdminRoutes(mux)
|
||||
server := &http.Server{
|
||||
Addr: address,
|
||||
Handler: mux,
|
||||
|
||||
@@ -50,7 +50,7 @@ type Cover struct {
|
||||
|
||||
// User represents the users table
|
||||
type User struct {
|
||||
IDBase
|
||||
Username string `gorm:"not null;unique_index"`
|
||||
Base
|
||||
Name string `gorm:"not null;unique_index"`
|
||||
Password string
|
||||
}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -7,6 +7,7 @@ require (
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect
|
||||
github.com/go-sql-driver/mysql v1.4.1 // indirect
|
||||
github.com/gofrs/uuid v3.2.0+incompatible // indirect
|
||||
github.com/gorilla/sessions v1.1.3
|
||||
github.com/jinzhu/gorm v1.9.2
|
||||
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a // indirect
|
||||
github.com/jinzhu/now v1.0.0 // indirect
|
||||
@@ -16,6 +17,7 @@ require (
|
||||
github.com/mozillazg/go-unidecode v0.1.1
|
||||
github.com/myesui/uuid v1.0.0 // indirect
|
||||
github.com/twinj/uuid v1.0.0
|
||||
github.com/wader/gormstore v0.0.0-20190302154359-acb787ba3755
|
||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a // indirect
|
||||
google.golang.org/appengine v1.5.0 // indirect
|
||||
)
|
||||
|
||||
8
go.sum
8
go.sum
@@ -49,6 +49,12 @@ github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57 h1:eqyIo2HjKhKe/mJzTG
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
|
||||
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.6.2/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
@@ -100,6 +106,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/twinj/uuid v1.0.0 h1:fzz7COZnDrXGTAOHGuUGYd6sG+JMq+AoE7+Jlu0przk=
|
||||
github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY=
|
||||
github.com/wader/gormstore v0.0.0-20190302154359-acb787ba3755 h1:pNaEDfvqe9W2h4D+xm5f+lnZdao3Rob6O0b8SovpGbE=
|
||||
github.com/wader/gormstore v0.0.0-20190302154359-acb787ba3755/go.mod h1:PbEnTGtqU8NGCALR62gu2+eQYO8zQDEvaMJiPaj5Hic=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.opencensus.io v0.19.1/go.mod h1:gug0GbSHa8Pafr0d2urOSgoXHZ6x/RUlaiT0d9pqb4A=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
|
||||
77
handler/admin.go
Normal file
77
handler/admin.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/sentriz/gonic/db"
|
||||
)
|
||||
|
||||
func (c *Controller) ServeLogin(w http.ResponseWriter, r *http.Request) {
|
||||
session, _ := c.SStore.Get(r, "gonic")
|
||||
renderTemplate(w, r, session, "login", &templateData{})
|
||||
}
|
||||
|
||||
func (c *Controller) ServeAuthenticate(w http.ResponseWriter, r *http.Request) {
|
||||
session, _ := c.SStore.Get(r, "gonic")
|
||||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
if username == "" || password == "" {
|
||||
session.AddFlash("please provide both a username and password")
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, "/admin/login", 303)
|
||||
return
|
||||
}
|
||||
var user db.User
|
||||
c.DB.Where("name = ?", username).First(&user)
|
||||
if !(username == user.Name && password == user.Password) {
|
||||
session.AddFlash("invalid username / password")
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, "/admin/login", 303)
|
||||
return
|
||||
}
|
||||
session.Values["authenticated"] = true
|
||||
session.Values["user"] = user.ID
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, "/admin/home", 303)
|
||||
}
|
||||
|
||||
func (c *Controller) ServeHome(w http.ResponseWriter, r *http.Request) {
|
||||
session, _ := c.SStore.Get(r, "gonic")
|
||||
authed, _ := session.Values["authenticated"].(bool)
|
||||
if !authed {
|
||||
session.AddFlash("you are not authenticated")
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, "/admin/login", 303)
|
||||
return
|
||||
}
|
||||
var data templateData
|
||||
var user db.User
|
||||
c.DB.First(&user, session.Values["user"])
|
||||
data.UserID = user.ID
|
||||
data.Username = user.Name
|
||||
c.DB.Table("album_artists").Count(&data.ArtistCount)
|
||||
c.DB.Table("albums").Count(&data.AlbumCount)
|
||||
c.DB.Table("tracks").Count(&data.TrackCount)
|
||||
c.DB.Find(&data.Users)
|
||||
renderTemplate(w, r, session, "home", &data)
|
||||
}
|
||||
|
||||
func (c *Controller) ServeCreateUser(w http.ResponseWriter, r *http.Request) {
|
||||
session, _ := c.SStore.Get(r, "gonic")
|
||||
authed, _ := session.Values["authenticated"].(bool)
|
||||
if !authed {
|
||||
session.AddFlash("you are not authenticated")
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, "/admin/login", 303)
|
||||
return
|
||||
}
|
||||
renderTemplate(w, r, session, "create_user", &templateData{})
|
||||
}
|
||||
|
||||
func (c *Controller) ServeLogout(w http.ResponseWriter, r *http.Request) {
|
||||
session, _ := c.SStore.Get(r, "gonic")
|
||||
delete(session.Values, "authenticated")
|
||||
delete(session.Values, "user")
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, "/admin/login", 303)
|
||||
}
|
||||
@@ -4,16 +4,55 @@ import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/sentriz/gonic/db"
|
||||
"github.com/sentriz/gonic/subsonic"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/wader/gormstore"
|
||||
)
|
||||
|
||||
var (
|
||||
templates = make(map[string]*template.Template)
|
||||
)
|
||||
|
||||
func init() {
|
||||
templates["login"] = template.Must(template.ParseFiles(
|
||||
filepath.Join("templates", "layout.tmpl"),
|
||||
filepath.Join("templates", "admin", "login.tmpl"),
|
||||
))
|
||||
templates["home"] = template.Must(template.ParseFiles(
|
||||
filepath.Join("templates", "layout.tmpl"),
|
||||
filepath.Join("templates", "user.tmpl"),
|
||||
filepath.Join("templates", "admin", "home.tmpl"),
|
||||
))
|
||||
templates["create_user"] = template.Must(template.ParseFiles(
|
||||
filepath.Join("templates", "layout.tmpl"),
|
||||
filepath.Join("templates", "user.tmpl"),
|
||||
filepath.Join("templates", "admin", "create_user.tmpl"),
|
||||
))
|
||||
}
|
||||
|
||||
type Controller struct {
|
||||
DB *gorm.DB
|
||||
DB *gorm.DB
|
||||
SStore *gormstore.Store
|
||||
Templates map[string]*template.Template
|
||||
}
|
||||
|
||||
type templateData struct {
|
||||
Flashes []interface{}
|
||||
UserID uint
|
||||
Username string
|
||||
ArtistCount uint
|
||||
AlbumCount uint
|
||||
TrackCount uint
|
||||
Users []*db.User
|
||||
}
|
||||
|
||||
func getStrParam(r *http.Request, key string) string {
|
||||
@@ -40,7 +79,8 @@ func getIntParamOr(r *http.Request, key string, or int) int {
|
||||
return val
|
||||
}
|
||||
|
||||
func respondRaw(w http.ResponseWriter, r *http.Request, code int, sub *subsonic.Response) {
|
||||
func respondRaw(w http.ResponseWriter, r *http.Request,
|
||||
code int, sub *subsonic.Response) {
|
||||
res := subsonic.MetaResponse{
|
||||
Response: sub,
|
||||
}
|
||||
@@ -73,12 +113,26 @@ func respondRaw(w http.ResponseWriter, r *http.Request, code int, sub *subsonic.
|
||||
}
|
||||
}
|
||||
|
||||
func respond(w http.ResponseWriter, r *http.Request, sub *subsonic.Response) {
|
||||
func respond(w http.ResponseWriter, r *http.Request,
|
||||
sub *subsonic.Response) {
|
||||
respondRaw(w, r, http.StatusOK, sub)
|
||||
}
|
||||
|
||||
func respondError(w http.ResponseWriter, r *http.Request, code uint64, message string) {
|
||||
func respondError(w http.ResponseWriter, r *http.Request,
|
||||
code uint64, message string) {
|
||||
respondRaw(w, r, http.StatusBadRequest, subsonic.NewError(
|
||||
code, message,
|
||||
))
|
||||
}
|
||||
|
||||
func renderTemplate(w http.ResponseWriter, r *http.Request,
|
||||
s *sessions.Session, name string, data *templateData) {
|
||||
flashes := s.Flashes()
|
||||
s.Save(r, w)
|
||||
data.Flashes = flashes
|
||||
err := templates[name].ExecuteTemplate(w, "layout", data)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("500 when executing: %v", err), 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func (c *Controller) CheckParameters(next http.HandlerFunc) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
user := db.User{
|
||||
Username: username,
|
||||
Name: username,
|
||||
}
|
||||
err := c.DB.Where(user).First(&user).Error
|
||||
if gorm.IsRecordNotFoundError(err) {
|
||||
|
||||
BIN
static/images/gonic.png
Normal file
BIN
static/images/gonic.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 236 KiB |
7
static/stylesheets/awsm.css
Normal file
7
static/stylesheets/awsm.css
Normal file
File diff suppressed because one or more lines are too long
77
static/stylesheets/main.css
Normal file
77
static/stylesheets/main.css
Normal file
@@ -0,0 +1,77 @@
|
||||
div {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
form {
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
#content > * {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.light {
|
||||
color: #00000082;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.pre {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.box {
|
||||
background-color: #0000000a;
|
||||
}
|
||||
|
||||
.padded {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
#header {
|
||||
/* background-color: #fdc71b08; */
|
||||
background-image: linear-gradient(white, #fdad1b0d);
|
||||
border-bottom: 1px solid;
|
||||
padding-top: 3rem;
|
||||
}
|
||||
|
||||
#header img {
|
||||
max-width: 580px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#footer {
|
||||
background-color: #fdad1b0d;
|
||||
border-top: 1px solid;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#flashes {
|
||||
background-color: #fd1b1b29;
|
||||
}
|
||||
|
||||
#login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
#login-form input[type=submit] {
|
||||
width: 8rem;
|
||||
}
|
||||
3
static/stylesheets/tacit.css
Normal file
3
static/stylesheets/tacit.css
Normal file
File diff suppressed because one or more lines are too long
12
templates/admin/create_user.tmpl
Normal file
12
templates/admin/create_user.tmpl
Normal file
@@ -0,0 +1,12 @@
|
||||
{{ define "title" }}home{{ end }}
|
||||
|
||||
{{ define "user" }}
|
||||
<div class="padded box mono">
|
||||
<div>
|
||||
<span><u>create new user</u></span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span class="pre">howdy</span><br/>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
34
templates/admin/home.tmpl
Normal file
34
templates/admin/home.tmpl
Normal file
@@ -0,0 +1,34 @@
|
||||
{{ define "title" }}home{{ end }}
|
||||
|
||||
{{ define "user" }}
|
||||
<div class="padded box mono">
|
||||
<div>
|
||||
<span><u>stats</u></span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span class="pre">artists: {{ printf "%7v" .ArtistCount }}</span><br/>
|
||||
<span class="pre">albums: {{ printf "%7v" .AlbumCount }}</span><br/>
|
||||
<span class="pre">tracks: {{ printf "%7v" .TrackCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="padded box mono">
|
||||
<div>
|
||||
<span><u>last.fm</u></span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span class="pre">unlinked</span><br/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="padded box mono">
|
||||
<div>
|
||||
<span><u>users</u></span>
|
||||
</div>
|
||||
<div class="right">
|
||||
{{ range $user := .Users }}
|
||||
<span class="pre">{{ $user.Name }} <span class="light">created</span> <u>{{ $user.CreatedAt.Format "Jan 02, 2006" }}</u> <a href="/admin/change_password">change password</a></span><br/>
|
||||
{{ end }}
|
||||
<br>
|
||||
<a href="/admin/create_user" class="button">create new</a>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
12
templates/admin/login.tmpl
Normal file
12
templates/admin/login.tmpl
Normal file
@@ -0,0 +1,12 @@
|
||||
{{ define "title" }}gonic{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
<div class="light">
|
||||
<span>login</span>
|
||||
</div>
|
||||
<form id="login-form" action="/admin/authenticate" method="post">
|
||||
<input type="text" id="username" name="username" placeholder="username">
|
||||
<input type="password" id="password" name="password" placeholder="password">
|
||||
<input type="submit" value="login">
|
||||
</form>
|
||||
{{ end }}
|
||||
30
templates/layout.tmpl
Normal file
30
templates/layout.tmpl
Normal file
@@ -0,0 +1,30 @@
|
||||
{{ define "layout" }}
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{ template "title" }}</title>
|
||||
<link rel="stylesheet" href="/admin/static/stylesheets/awsm.css">
|
||||
<link rel="stylesheet" href="/admin/static/stylesheets/main.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<div id="header">
|
||||
<img src="/admin/static/images/gonic.png">
|
||||
</div>
|
||||
<div id="content">
|
||||
{{ if .Flashes }}
|
||||
<div id="flashes">
|
||||
<p><b>warning:</b> {{ index .Flashes 0 }}</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ template "content" . }}
|
||||
</div>
|
||||
</div>
|
||||
<div id="footer" class="padded">
|
||||
<p>senan kelly, 2019 | <a href="https://github.com/sentriz/gonic">github</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
||||
8
templates/user.tmpl
Normal file
8
templates/user.tmpl
Normal file
@@ -0,0 +1,8 @@
|
||||
{{ define "title" }}home{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
<div class="light right">
|
||||
<span>welcome <u>{{ .Username }}</u> | <a href="/admin/logout">logout</a></span>
|
||||
</div>
|
||||
{{ template "user" . }}
|
||||
{{ end }}
|
||||
Reference in New Issue
Block a user