refactored out global. long live AppState

This commit is contained in:
ari melody 2025-01-21 14:53:18 +00:00
parent 3d674515ce
commit 384579ee5e
Signed by: ari
GPG key ID: CF99829C92678188
24 changed files with 350 additions and 375 deletions

182
main.go
View file

@ -4,16 +4,18 @@ import (
"errors"
"fmt"
"log"
"math/rand"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"arimelody-web/admin"
"arimelody-web/api"
"arimelody-web/colour"
"arimelody-web/controller"
"arimelody-web/global"
"arimelody-web/model"
"arimelody-web/templates"
"arimelody-web/view"
@ -30,48 +32,48 @@ const DEFAULT_PORT int64 = 8080
func main() {
fmt.Printf("made with <3 by ari melody\n\n")
// TODO: refactor `global` to `AppState`
// this should contain `Config` and `DB`, and be passed through to all
// handlers that need it. it's better than weird static globals everywhere!
app := model.AppState{
Config: controller.GetConfig(),
}
// initialise database connection
if global.Config.DB.Host == "" {
if app.Config.DB.Host == "" {
fmt.Fprintf(os.Stderr, "FATAL: db.host not provided! Exiting...\n")
os.Exit(1)
}
if global.Config.DB.Name == "" {
if app.Config.DB.Name == "" {
fmt.Fprintf(os.Stderr, "FATAL: db.name not provided! Exiting...\n")
os.Exit(1)
}
if global.Config.DB.User == "" {
if app.Config.DB.User == "" {
fmt.Fprintf(os.Stderr, "FATAL: db.user not provided! Exiting...\n")
os.Exit(1)
}
if global.Config.DB.Pass == "" {
if app.Config.DB.Pass == "" {
fmt.Fprintf(os.Stderr, "FATAL: db.pass not provided! Exiting...\n")
os.Exit(1)
}
var err error
global.DB, err = sqlx.Connect(
app.DB, err = sqlx.Connect(
"postgres",
fmt.Sprintf(
"host=%s port=%d user=%s dbname=%s password='%s' sslmode=disable",
global.Config.DB.Host,
global.Config.DB.Port,
global.Config.DB.User,
global.Config.DB.Name,
global.Config.DB.Pass,
app.Config.DB.Host,
app.Config.DB.Port,
app.Config.DB.User,
app.Config.DB.Name,
app.Config.DB.Pass,
),
)
if err != nil {
fmt.Fprintf(os.Stderr, "FATAL: Unable to initialise database: %v\n", err)
os.Exit(1)
}
global.DB.SetConnMaxLifetime(time.Minute * 3)
global.DB.SetMaxOpenConns(10)
global.DB.SetMaxIdleConns(10)
defer global.DB.Close()
app.DB.SetConnMaxLifetime(time.Minute * 3)
app.DB.SetMaxOpenConns(10)
app.DB.SetMaxIdleConns(10)
defer app.DB.Close()
// handle command arguments
if len(os.Args) > 1 {
@ -87,7 +89,7 @@ func main() {
totpName := os.Args[3]
secret := controller.GenerateTOTPSecret(controller.TOTP_SECRET_LENGTH)
account, err := controller.GetAccount(global.DB, username)
account, err := controller.GetAccount(app.DB, username)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to fetch account \"%s\": %v\n", username, err)
os.Exit(1)
@ -104,7 +106,7 @@ func main() {
Secret: string(secret),
}
err = controller.CreateTOTP(global.DB, &totp)
err = controller.CreateTOTP(app.DB, &totp)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create TOTP method: %v\n", err)
os.Exit(1)
@ -122,7 +124,7 @@ func main() {
username := os.Args[2]
totpName := os.Args[3]
account, err := controller.GetAccount(global.DB, username)
account, err := controller.GetAccount(app.DB, username)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to fetch account \"%s\": %v\n", username, err)
os.Exit(1)
@ -133,7 +135,7 @@ func main() {
os.Exit(1)
}
err = controller.DeleteTOTP(global.DB, account.ID, totpName)
err = controller.DeleteTOTP(app.DB, account.ID, totpName)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create TOTP method: %v\n", err)
os.Exit(1)
@ -149,7 +151,7 @@ func main() {
}
username := os.Args[2]
account, err := controller.GetAccount(global.DB, username)
account, err := controller.GetAccount(app.DB, username)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to fetch account \"%s\": %v\n", username, err)
os.Exit(1)
@ -160,7 +162,7 @@ func main() {
os.Exit(1)
}
totps, err := controller.GetTOTPsForAccount(global.DB, account.ID)
totps, err := controller.GetTOTPsForAccount(app.DB, account.ID)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create TOTP methods: %v\n", err)
os.Exit(1)
@ -182,7 +184,7 @@ func main() {
username := os.Args[2]
totpName := os.Args[3]
account, err := controller.GetAccount(global.DB, username)
account, err := controller.GetAccount(app.DB, username)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to fetch account \"%s\": %v\n", username, err)
os.Exit(1)
@ -193,7 +195,7 @@ func main() {
os.Exit(1)
}
totp, err := controller.GetTOTP(global.DB, account.ID, totpName)
totp, err := controller.GetTOTP(app.DB, account.ID, totpName)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to fetch TOTP method \"%s\": %v\n", totpName, err)
os.Exit(1)
@ -210,7 +212,7 @@ func main() {
case "createInvite":
fmt.Printf("Creating invite...\n")
invite, err := controller.CreateInvite(global.DB, 16, time.Hour * 24)
invite, err := controller.CreateInvite(app.DB, 16, time.Hour * 24)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create invite code: %v\n", err)
os.Exit(1)
@ -221,7 +223,7 @@ func main() {
case "purgeInvites":
fmt.Printf("Deleting all invites...\n")
err := controller.DeleteAllInvites(global.DB)
err := controller.DeleteAllInvites(app.DB)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete invites: %v\n", err)
os.Exit(1)
@ -231,7 +233,7 @@ func main() {
return
case "listAccounts":
accounts, err := controller.GetAllAccounts(global.DB)
accounts, err := controller.GetAllAccounts(app.DB)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to fetch accounts: %v\n", err)
os.Exit(1)
@ -259,7 +261,7 @@ func main() {
username := os.Args[2]
fmt.Printf("Deleting account \"%s\"...\n", username)
account, err := controller.GetAccount(global.DB, username)
account, err := controller.GetAccount(app.DB, username)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to fetch account \"%s\": %v\n", username, err)
os.Exit(1)
@ -277,7 +279,7 @@ func main() {
return
}
err = controller.DeleteAccount(global.DB, username)
err = controller.DeleteAccount(app.DB, username)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete account: %v\n", err)
os.Exit(1)
@ -305,20 +307,20 @@ func main() {
}
// handle DB migrations
controller.CheckDBVersionAndMigrate(global.DB)
controller.CheckDBVersionAndMigrate(app.DB)
// initial invite code
accountsCount := 0
err = global.DB.Get(&accountsCount, "SELECT count(*) FROM account")
err = app.DB.Get(&accountsCount, "SELECT count(*) FROM account")
if err != nil { panic(err) }
if accountsCount == 0 {
_, err := global.DB.Exec("DELETE FROM invite")
_, err := app.DB.Exec("DELETE FROM invite")
if err != nil {
fmt.Fprintf(os.Stderr, "FATAL: Failed to clear existing invite codes: %v\n", err)
os.Exit(1)
}
invite, err := controller.CreateInvite(global.DB, 16, time.Hour * 24)
invite, err := controller.CreateInvite(app.DB, 16, time.Hour * 24)
if err != nil {
fmt.Fprintf(os.Stderr, "FATAL: Failed to create invite code: %v\n", err)
os.Exit(1)
@ -328,28 +330,28 @@ func main() {
}
// delete expired invites
err = controller.DeleteExpiredInvites(global.DB)
err = controller.DeleteExpiredInvites(app.DB)
if err != nil {
fmt.Fprintf(os.Stderr, "FATAL: Failed to clear expired invite codes: %v\n", err)
os.Exit(1)
}
// start the web server!
mux := createServeMux()
fmt.Printf("Now serving at %s:%d\n", global.Config.BaseUrl, global.Config.Port)
mux := createServeMux(&app)
fmt.Printf("Now serving at %s:%d\n", app.Config.BaseUrl, app.Config.Port)
log.Fatal(
http.ListenAndServe(fmt.Sprintf(":%d", global.Config.Port),
global.HTTPLog(global.DefaultHeaders(mux)),
http.ListenAndServe(fmt.Sprintf(":%d", app.Config.Port),
HTTPLog(DefaultHeaders(mux)),
))
}
func createServeMux() *http.ServeMux {
func createServeMux(app *model.AppState) *http.ServeMux {
mux := http.NewServeMux()
mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler(global.DB)))
mux.Handle("/api/", http.StripPrefix("/api", api.Handler(global.DB)))
mux.Handle("/music/", http.StripPrefix("/music", view.MusicHandler(global.DB)))
mux.Handle("/uploads/", http.StripPrefix("/uploads", staticHandler(filepath.Join(global.Config.DataDirectory, "uploads"))))
mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler(app)))
mux.Handle("/api/", http.StripPrefix("/api", api.Handler(app)))
mux.Handle("/music/", http.StripPrefix("/music", view.MusicHandler(app)))
mux.Handle("/uploads/", http.StripPrefix("/uploads", staticHandler(filepath.Join(app.Config.DataDirectory, "uploads"))))
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodHead {
w.WriteHeader(http.StatusOK)
@ -390,3 +392,93 @@ func staticHandler(directory string) http.Handler {
http.FileServer(http.Dir(directory)).ServeHTTP(w, r)
})
}
var PoweredByStrings = []string{
"nerd rage",
"estrogen",
"your mother",
"awesome powers beyond comprehension",
"jared",
"the weight of my sins",
"the arc reactor",
"AA batteries",
"15 euro solar panel from ebay",
"magnets, how do they work",
"a fax machine",
"dell optiplex",
"a trans girl's nintendo wii",
"BASS",
"electricity, duh",
"seven hamsters in a big wheel",
"girls",
"mzungu hosting",
"golang",
"the state of the world right now",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)",
"the good folks at aperture science",
"free2play CDs",
"aridoodle",
"the love of creating",
"not for the sake of art; not for the sake of money; we like painting naked people",
"30 billion dollars in VC funding",
}
func DefaultHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Server", "arimelody.me")
w.Header().Add("Do-Not-Stab", "1")
w.Header().Add("X-Clacks-Overhead", "GNU Terry Pratchett")
w.Header().Add("X-Hacker", "spare me please")
w.Header().Add("X-Robots-TXT", "'; DROP TABLE pages;")
w.Header().Add("X-Thinking-With", "Portals")
w.Header().Add(
"X-Powered-By",
PoweredByStrings[rand.Intn(len(PoweredByStrings))],
)
next.ServeHTTP(w, r)
})
}
type LoggingResponseWriter struct {
http.ResponseWriter
Status int
}
func (lrw *LoggingResponseWriter) WriteHeader(status int) {
lrw.Status = status
lrw.ResponseWriter.WriteHeader(status)
}
func HTTPLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
lrw := LoggingResponseWriter{w, http.StatusOK}
next.ServeHTTP(&lrw, r)
after := time.Now()
difference := (after.Nanosecond() - start.Nanosecond()) / 1_000_000
elapsed := "<1"
if difference >= 1 {
elapsed = strconv.Itoa(difference)
}
statusColour := colour.Reset
if lrw.Status - 600 <= 0 { statusColour = colour.Red }
if lrw.Status - 500 <= 0 { statusColour = colour.Yellow }
if lrw.Status - 400 <= 0 { statusColour = colour.White }
if lrw.Status - 300 <= 0 { statusColour = colour.Green }
fmt.Printf("[%s] %s %s - %s%d%s (%sms) (%s)\n",
after.Format(time.UnixDate),
r.Method,
r.URL.Path,
statusColour,
lrw.Status,
colour.Reset,
elapsed,
r.Header["User-Agent"][0])
})
}