flounder

A simple gemini site builder
Log | Files | Refs | README | LICENSE

commit 5eec59dca9972e3f10e4bd24bb8ce0127b9ea6be
parent b93b3d7c26305da0995bff88c757eab8d34c2332
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Wed, 28 Oct 2020 19:59:49 -0700

Add simple admin page

Diffstat:
Madmin.go | 28+++++++++++++++++++++++-----
Mgemini.go | 2+-
Mhttp.go | 71++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Mmain.go | 27++++++++++++++++++++++++++-
Mtemplates/admin.html | 27+++++++++++++++++++++++++--
Mtemplates/index.html | 8++++----
Mtemplates/message.html | 2+-
Mtemplates/my_site.html | 4++--
Mtemplates/static/style.css | 9++++-----
9 files changed, 142 insertions(+), 36 deletions(-)

diff --git a/admin.go b/admin.go @@ -12,6 +12,7 @@ import ( "log" "os" "path" + "path/filepath" ) // TODO improve cli @@ -23,16 +24,21 @@ func runAdminCommand() { switch os.Args[2] { case "activate-user": username := os.Args[3] - activateUser(username) - // reset password - // delete user (with are you sure?) + err := activateUser(username) + log.Fatal(err) + case "delete-user": + username := os.Args[3] + err := deleteUser(username) + log.Fatal(err) } + // reset password + } -func activateUser(username string) { +func activateUser(username string) error { _, err := DB.Exec("UPDATE user SET active = true WHERE username = $1", username) if err != nil { - log.Fatal(err) + return err } log.Println("Activated user", username) baseIndex := `# Welcome to Flounder! @@ -44,7 +50,19 @@ And here's a guide to the text format that Flounder uses to create pages, Gemini => //admin.flounder.online/gemini_text_guide.gmi Have fun!` + // Redundant filepath.Clean call just in case. + username = filepath.Clean(username) os.Mkdir(path.Join(c.FilesDirectory, username), os.ModePerm) ioutil.WriteFile(path.Join(c.FilesDirectory, username, "index.gmi"), []byte(baseIndex), 0644) os.Mkdir(path.Join(c.FilesDirectory, username), os.ModePerm) + return nil +} + +func deleteUser(username string) error { + // not sure whether we should delete files too + _, err := DB.Exec("DELETE FROM user WHERE username = $1", username) + if err != nil { + return err + } + return nil } diff --git a/gemini.go b/gemini.go @@ -20,7 +20,7 @@ func gmiIndex(w *gmi.ResponseWriter, r *gmi.Request) { log.Fatal(err) } files, err := getIndexFiles() - users, err := getUsers() + users, err := getActiveUserNames() if err != nil { log.Println(err) w.WriteHeader(40, "Internal server error") diff --git a/http.go b/http.go @@ -52,7 +52,7 @@ func rootHandler(w http.ResponseWriter, r *http.Request) { renderError(w, InternalServerErrorMsg, 500) return } - allUsers, err := getUsers() + allUsers, err := getActiveUserNames() if err != nil { log.Println(err) renderError(w, InternalServerErrorMsg, 500) @@ -78,7 +78,7 @@ func editFileHandler(w http.ResponseWriter, r *http.Request) { session, _ := SessionStore.Get(r, "cookie-session") authUser, ok := session.Values["auth_user"].(string) if !ok { - renderError(w, "Forbidden", 403) + renderError(w, "403: Forbidden", 403) return } fileName := filepath.Clean(r.URL.Path[len("/edit/"):]) @@ -134,7 +134,7 @@ func uploadFilesHandler(w http.ResponseWriter, r *http.Request) { session, _ := SessionStore.Get(r, "cookie-session") authUser, ok := session.Values["auth_user"].(string) if !ok { - renderError(w, "Forbidden", 403) + renderError(w, "403: Forbidden", 403) return } r.ParseMultipartForm(10 << 6) // why does this not work @@ -177,7 +177,7 @@ func getAuthUser(r *http.Request) (bool, string, bool) { func deleteFileHandler(w http.ResponseWriter, r *http.Request) { authd, authUser, _ := getAuthUser(r) if !authd { - renderError(w, "Forbidden", 403) + renderError(w, "403: Forbidden", 403) return } fileName := filepath.Clean(r.URL.Path[len("/delete/"):]) @@ -189,9 +189,9 @@ func deleteFileHandler(w http.ResponseWriter, r *http.Request) { } func mySiteHandler(w http.ResponseWriter, r *http.Request) { - authd, authUser, _ := getAuthUser(r) + authd, authUser, isAdmin := getAuthUser(r) if !authd { - renderError(w, "Forbidden", 403) + renderError(w, "403: Forbidden", 403) return } // check auth @@ -202,7 +202,8 @@ func mySiteHandler(w http.ResponseWriter, r *http.Request) { AuthUser string Files []*File LoggedIn bool - }{c.Host, c.SiteTitle, authUser, files, authd} + IsAdmin bool + }{c.Host, c.SiteTitle, authUser, files, authd, isAdmin} _ = t.ExecuteTemplate(w, "my_site.html", data) } @@ -338,23 +339,31 @@ func registerHandler(w http.ResponseWriter, r *http.Request) { } } -type User struct { -} - func adminHandler(w http.ResponseWriter, r *http.Request) { _, _, isAdmin := getAuthUser(r) if !isAdmin { - renderError(w, "Forbidden", 403) + renderError(w, "403: Forbidden", 403) + return + } + allUsers, err := getUsers() + if err != nil { + log.Println(err) + renderError(w, InternalServerErrorMsg, 500) return } - // LIST USERS data := struct { - users []User + Users []User LoggedIn bool IsAdmin bool PageTitle string - }{[]User{}, true, true, "admin"} - t.ExecuteTemplate(w, "admin.html", data) + Host string + }{allUsers, true, true, "Admin", c.Host} + err = t.ExecuteTemplate(w, "admin.html", data) + if err != nil { + log.Println(err) + renderError(w, InternalServerErrorMsg, 500) + return + } } // Server a user's file @@ -390,6 +399,35 @@ func userFile(w http.ResponseWriter, r *http.Request) { } } +func adminUserHandler(w http.ResponseWriter, r *http.Request) { + _, _, isAdmin := getAuthUser(r) + if r.Method == "POST" { + if !isAdmin { + renderError(w, "403: Forbidden", 403) + return + } + components := strings.Split(r.URL.Path, "/") + if len(components) < 5 { + renderError(w, "Invalid action", 400) + return + } + userName := components[3] + action := components[4] + var err error + if action == "activate" { + err = activateUser(userName) + } else if action == "delete" { + err = deleteUser(userName) + } + if err != nil { + log.Println(err) + renderError(w, InternalServerErrorMsg, 500) + return + } + http.Redirect(w, r, "/admin", 302) + } +} + func runHTTPServer() { log.Printf("Running http server with hostname %s on port %d. TLS enabled: %t", c.Host, c.HttpPort, c.HttpsEnabled) var err error @@ -413,6 +451,9 @@ func runHTTPServer() { serveMux.HandleFunc(hostname+"/register", registerHandler) serveMux.HandleFunc(hostname+"/delete/", deleteFileHandler) + // admin commands + serveMux.HandleFunc(hostname+"/admin/user/", adminUserHandler) + // TODO rate limit login https://github.com/ulule/limiter wrapped := handlers.LoggingHandler(log.Writer(), serveMux) diff --git a/main.go b/main.go @@ -26,7 +26,15 @@ type File struct { TimeAgo string } -func getUsers() ([]string, error) { +type User struct { + Username string + Email string + Active bool + Admin bool + CreatedAt int // timestamp +} + +func getActiveUserNames() ([]string, error) { rows, err := DB.Query(`SELECT username from user WHERE active is true`) if err != nil { return nil, err @@ -43,6 +51,23 @@ func getUsers() ([]string, error) { return users, nil } +func getUsers() ([]User, error) { + rows, err := DB.Query(`SELECT username, email, active, admin, created_at from user ORDER BY created_at DESC`) + if err != nil { + return nil, err + } + var users []User + for rows.Next() { + var user User + err = rows.Scan(&user.Username, &user.Email, &user.Active, &user.Admin, &user.CreatedAt) + if err != nil { + return nil, err + } + users = append(users, user) + } + return users, nil +} + func getIndexFiles() ([]*File, error) { // cache this function result := []*File{} err := filepath.Walk(c.FilesDirectory, func(thepath string, info os.FileInfo, err error) error { diff --git a/templates/admin.html b/templates/admin.html @@ -1,5 +1,28 @@ +{{$domain := .Host}} {{template "header" .}} -<h1>Admin</h1> +<h1>{{.PageTitle}}</h1> {{template "nav.html" .}} -asdfasdf +<br> +{{ range .Users }} +<a href="//{{.Username}}.{{$domain}}">{{.Username}}</a> +{{ if not .Active }} +<form action="/admin/user/{{.Username}}/activate" method="POST" class="inline"> +<input + class="button" + type="submit" + value="activate" +/> +</form> +{{ end }} +<form action="/admin/user/{{.Username}}/delete" method="POST" class="inline"> +<input + class="button delete" + type="submit" + onclick="return confirm('Are you SURE you want to delete this user?');" + value="delete" +/> +</form> +<br> +{{end}} + {{template "footer" .}} diff --git a/templates/index.html b/templates/index.html @@ -3,18 +3,18 @@ <h1>{{.PageTitle}}!</h1> {{template "nav.html" .}} <br> -Welcome to flounder! For more information and site updates, check out the <a href="https://admin.{{$domain}}">admin page</a> +Welcome to flounder! For more information and site updates, check out the <a href="//admin.{{$domain}}">admin page</a> <h2>All users:</h2> {{ range .Users}} -<a href="https://{{.}}.{{$domain}}" class='person-link'>{{.}}</a> +<a href="//{{.}}.{{$domain}}" class='person-link'>{{.}}</a> {{end}} <h2>Recently updated files:</h2> {{ range .Files }} <div> - <a href="https://{{.Creator}}.{{$domain}}" class='person-link'> + <a href="//{{.Creator}}.{{$domain}}" class='person-link'> {{ .Creator }}</a> <em>{{.TimeAgo}}</em> - <a href="https://{{.Creator}}.{{$domain}}/{{.Name}}"> + <a href="//{{.Creator}}.{{$domain}}/{{.Name}}"> {{ .Name}} </a> </div> diff --git a/templates/message.html b/templates/message.html @@ -1,5 +1,5 @@ {{template "header" .}} <h1>{{.PageTitle}}</h1> {{ .Message }} -<a href="https://{{.Host}}">Go home</a> +<a href="//{{.Host}}">Go home</a> {{template "footer" .}} diff --git a/templates/my_site.html b/templates/my_site.html @@ -2,7 +2,7 @@ {{$authUser := .AuthUser}} {{template "header" .}} <h1>Managing - <a href="https://{{$authUser}}.{{$domain}}"> + <a href="//{{$authUser}}.{{$domain}}"> {{.AuthUser}}.{{$domain}} </a> </h1> @@ -10,7 +10,7 @@ <h3>Your files:</h3> {{ range .Files }} <div> - <a href="https://{{$authUser}}.{{$domain}}/{{.Name}}"> + <a href="//{{$authUser}}.{{$domain}}/{{.Name}}"> {{ .Name }}</a> <a href="/edit/{{.Name}}">edit</a> <form action="/delete/{{.Name}}" method="POST" class="inline"> diff --git a/templates/static/style.css b/templates/static/style.css @@ -55,17 +55,16 @@ img { color: red; } +.nav { + color: blue; +} a:visited { - color: black; } a { transition-duration: 0.2s; - font-weight: bold; - color: black; } a:hover { - background-color: black; - color: white; + background-color: yellow; }