all repos — flounder @ 5eec59dca9972e3f10e4bd24bb8ce0127b9ea6be

A small site builder for the Gemini protocol

Add simple admin page
alex wennerberg alex@alexwennerberg.com
Wed, 28 Oct 2020 19:59:49 -0700
commit

5eec59dca9972e3f10e4bd24bb8ce0127b9ea6be

parent

b93b3d7c26305da0995bff88c757eab8d34c2332

M admin.goadmin.go

@@ -12,6 +12,7 @@ "io/ioutil"

"log" "os" "path" + "path/filepath" ) // TODO improve cli

@@ -23,16 +24,21 @@ }

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. These pages are converted into HTML so they can be displayed in a web browser.

=> //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 }
M gemini.gogemini.go

@@ -20,7 +20,7 @@ if err != nil {

log.Fatal(err) } files, err := getIndexFiles() - users, err := getUsers() + users, err := getActiveUserNames() if err != nil { log.Println(err) w.WriteHeader(40, "Internal server error")
M http.gohttp.go

@@ -52,7 +52,7 @@ log.Println(err)

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 @@ if r.Method == "POST" {

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 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 @@ http.Redirect(w, r, "/my_site", 302)

} 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 @@ PageTitle string

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) }

@@ -336,25 +337,33 @@ }{c.Host, "Registration complete! The server admin will approve your request before you can log in.", "Registration Complete"}

t.ExecuteTemplate(w, "message.html", data) } } -} - -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 @@ http.ServeFile(w, r, fileName)

} } +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

@@ -412,6 +450,9 @@ serveMux.HandleFunc(hostname+"/login", loginHandler)

serveMux.HandleFunc(hostname+"/logout", logoutHandler) 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
M main.gomain.go

@@ -26,7 +26,15 @@ UpdatedTime time.Time

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

@@ -35,6 +43,23 @@ var users []string

for rows.Next() { var user string err = rows.Scan(&user) + if err != nil { + return nil, err + } + users = append(users, user) + } + 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 }
M templates/admin.htmltemplates/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" .}}
M templates/index.htmltemplates/index.html

@@ -3,18 +3,18 @@ {{template "header" .}}

<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>
M templates/message.htmltemplates/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" .}}
M templates/my_site.htmltemplates/my_site.html

@@ -2,7 +2,7 @@ {{$domain := .Host}}

{{$authUser := .AuthUser}} {{template "header" .}} <h1>Managing - <a href="https://{{$authUser}}.{{$domain}}"> + <a href="//{{$authUser}}.{{$domain}}"> {{.AuthUser}}.{{$domain}} </a> </h1>

@@ -10,7 +10,7 @@ {{template "nav.html" .}}

<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">
M templates/static/style.csstemplates/static/style.css

@@ -55,17 +55,16 @@ .error {

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; }