flounder

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

commit 9bf79136159cacf43fc95e56b467fe1143317c96
parent c26272ec81129523df8496118cc4432e56a334ab
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Sun, 25 Oct 2020 21:52:33 -0700

add admin command, qa cleanup

Diffstat:
MREADME.md | 5++++-
Madmin.go | 42+++++++++++++++++++++++++++++++++++++++++-
Mhttp.go | 26++++++++++++++++++--------
Mmain.go | 44++++++++++++++++++++++++++++++--------------
Mtemplates/login.html | 3+--
Mtemplates/user_page.html | 19+------------------
6 files changed, 95 insertions(+), 44 deletions(-)

diff --git a/README.md b/README.md @@ -4,7 +4,6 @@ A lightweight server to help users build simple Gemini sites over http(s) Designed to help make the Gemini ecosystem more accessible. - ## Hosting Flounder is designed to be very simple to host, and should be able to be relatively easily run by a single person. @@ -15,4 +14,8 @@ Once you've installed Flounder, you'll want to set the configuration variables. 2. For local testing, flounder will generate a TLS cert for you. However, for production, you'll need to generate a cert that matches \*.your-domain signed by a Certificate Authority. 3. Set the cookie store key +I'm working on an admin interface and some admin tools, but right now, you'll have to do a lot of administration at the command line via sqlite + Flounder uses the HTTP templates in the specified templates folder. If you want to modify the look and feel of your site, or host new files, you can modify these files. + + diff --git a/admin.go b/admin.go @@ -6,5 +6,45 @@ package main // Run some scripts to setup your instance -func initialize() { +import ( + "fmt" + "io/ioutil" + "log" + "os" + "path" +) + +// TODO improve cli +func runAdminCommand() { + if len(os.Args) < 4 { + fmt.Println("expected subcommand with parameter") + os.Exit(1) + } + switch os.Args[2] { + case "activate-user": + username := os.Args[3] + activateUser(username) + // reset password + // delete user (with are you sure?) + } +} + +func activateUser(username string) { + _, err := DB.Exec("UPDATE user SET active = true WHERE username = $1", username) + if err != nil { + log.Fatal(err) + } + log.Println("Activated user", username) + baseIndex := `# Welcome to Flounder! +## About +Flounder is an ultra-lightweight platform for making and sharing small websites. You can get started by editing this page -- remove this content and replace it with whatever you like! It will be live at <your-name>.flounder.online. You can go there right now to see what this page currently looks like. Here is a link to a page which will give you more information about using flounder: +=> https://admin.flounder.online + +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. +=> https://admin.flounder.online/gemini_text_guide.gmi + +Have fun!` + 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) } diff --git a/http.go b/http.go @@ -231,6 +231,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request) { PageTitle string }{"Your account is not active yet. Pending admin approval", c.SiteTitle} t.ExecuteTemplate(w, "login.html", data) + return } if bcrypt.CompareHashAndPassword(db_password, []byte(password)) == nil { log.Println("logged in") @@ -321,7 +322,6 @@ func registerHandler(w http.ResponseWriter, r *http.Request) { }{c.Host, errors, "Register"} t.ExecuteTemplate(w, "register.html", data) } else { - os.Mkdir(path.Join(c.FilesDirectory, username), os.ModePerm) data := struct { Host string Message string @@ -335,19 +335,29 @@ func registerHandler(w http.ResponseWriter, r *http.Request) { // Server a user's file func userFile(w http.ResponseWriter, r *http.Request) { userName := strings.Split(r.Host, ".")[0] - fileName := path.Join(c.FilesDirectory, userName, filepath.Clean(r.URL.Path)) + p := filepath.Clean(r.URL.Path) + if p == "/" { + p = "index.gmi" + } + fileName := path.Join(c.FilesDirectory, userName, p) extension := path.Ext(fileName) - if r.URL.Path == "/static/style.css" { + if r.URL.Path == "/style.css" { http.ServeFile(w, r, path.Join(c.TemplatesDirectory, "static/style.css")) } if extension == ".gmi" || extension == ".gemini" { - // covert to html - stat, _ := os.Stat(fileName) + _, err := os.Stat(fileName) + if err != nil { + renderError(w, "404: file not found", 404) + return + } file, _ := os.Open(fileName) + htmlString := gmi.Parse(file).HTML() - reader := strings.NewReader(htmlString) - w.Header().Set("Content-Type", "text/html") - http.ServeContent(w, r, fileName, stat.ModTime(), reader) + data := struct { + SiteBody template.HTML + PageTitle string + }{template.HTML(htmlString), userName} + t.ExecuteTemplate(w, "user_page.html", data) } else { http.ServeFile(w, r, fileName) } diff --git a/main.go b/main.go @@ -4,6 +4,7 @@ import ( "crypto/rand" "database/sql" "flag" + "fmt" "github.com/gorilla/sessions" "io" "io/ioutil" @@ -26,7 +27,7 @@ type File struct { } func getUsers() ([]string, error) { - rows, err := DB.Query(`SELECT username from user`) + rows, err := DB.Query(`SELECT username from user WHERE active is true`) if err != nil { return nil, err } @@ -98,7 +99,8 @@ func createTablesIfDNE() { username TEXT NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, - approved boolean NOT NULL DEFAULT false, + active boolean NOT NULL DEFAULT false, + admin boolean NOT NULL DEFAULT false, created_at INTEGER DEFAULT (strftime('%s', 'now')) ); @@ -113,6 +115,7 @@ CREATE TABLE IF NOT EXISTS cookie_key ( // Generate a cryptographically secure key for the cookie store func generateCookieKeyIfDNE() []byte { rows, err := DB.Query("SELECT value FROM cookie_key LIMIT 1") + defer rows.Close() if err != nil { log.Fatal(err) } @@ -138,8 +141,15 @@ func generateCookieKeyIfDNE() []byte { } func main() { - configPath := flag.String("c", "flounder.toml", "path to config file") + configPath := flag.String("c", "flounder.toml", "path to config file") // doesnt work atm + if len(os.Args) < 2 { + fmt.Println("expected 'admin' or 'serve' subcommand") + os.Exit(1) + } + flag.Parse() + var err error + log.Println("Loading config", *configPath) c, err = getConfig(*configPath) if err != nil { log.Fatal(err) @@ -161,15 +171,21 @@ func main() { createTablesIfDNE() cookie := generateCookieKeyIfDNE() SessionStore = sessions.NewCookieStore(cookie) - wg := new(sync.WaitGroup) - wg.Add(2) - go func() { - runHTTPServer() - wg.Done() - }() - go func() { - runGeminiServer() - wg.Done() - }() - wg.Wait() + + switch os.Args[1] { + case "serve": + wg := new(sync.WaitGroup) + wg.Add(2) + go func() { + runHTTPServer() + wg.Done() + }() + go func() { + runGeminiServer() + wg.Done() + }() + wg.Wait() + case "admin": + runAdminCommand() + } } diff --git a/templates/login.html b/templates/login.html @@ -1,6 +1,5 @@ {{template "header" .}} -<h1>{{.PageTitle}}</h1> -<h1>Log in</h1> +<h1>Login</h1> <form action="/login" method="post"> <p> <label for="username">Username</label> diff --git a/templates/user_page.html b/templates/user_page.html @@ -1,20 +1,3 @@ -{{$domain := .Host}} {{template "header" .}} -<h1>{{.PageTitle}}!</h1> -{{template "nav.html" .}} -<h2>All users:</h2> -{{ range .Users}} -<a href="https://{{.}}.{{$domain}}" class='person-link'>{{.}}</a> -{{end}} -<h2>Recently updated files:</h2> -{{ range .Files }} -<div> - <a href="https://{{.Creator}}.{{$domain}}" class='person-link'> - {{ .Creator }}</a> - <em>{{.UpdatedTime}}</em> - <a href="https://{{.Creator}}.{{$domain}}/{{.Name}}"> - {{ .Name}} - </a> -</div> -{{end}} +{{.SiteBody}} {{template "footer" .}}