all repos — flounder @ 9bf79136159cacf43fc95e56b467fe1143317c96

A small site builder for the Gemini protocol

add admin command, qa cleanup
alex wennerberg alex@alexwennerberg.com
Sun, 25 Oct 2020 21:52:33 -0700
commit

9bf79136159cacf43fc95e56b467fe1143317c96

parent

c26272ec81129523df8496118cc4432e56a334ab

6 files changed, 95 insertions(+), 44 deletions(-)

jump to
M README.mdREADME.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 @@ 1. Install with `go get ...`

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. + +
M admin.goadmin.go

@@ -6,5 +6,45 @@ // delete user

// 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) }
M http.gohttp.go

@@ -231,6 +231,7 @@ Error string

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

}{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 @@

// 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) }
M main.gomain.go

@@ -4,6 +4,7 @@ import (

"crypto/rand" "database/sql" "flag" + "fmt" "github.com/gorilla/sessions" "io" "io/ioutil"

@@ -26,7 +27,7 @@ TimeAgo string

} 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 @@ id INTEGER PRIMARY KEY NOT NULL,

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 @@

// 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 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 @@

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() + } }
M templates/login.htmltemplates/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>
M templates/user_page.htmltemplates/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" .}}