From 384579ee5e0a45daf98d53432e3ec3d057a7f009 Mon Sep 17 00:00:00 2001 From: ari melody Date: Tue, 21 Jan 2025 14:53:18 +0000 Subject: [PATCH 01/99] refactored out `global`. long live `AppState` --- .gitignore | 1 + admin/accounthttp.go | 48 ++++---- admin/artisthttp.go | 7 +- admin/http.go | 30 +++-- admin/releasehttp.go | 29 +++-- admin/trackhttp.go | 7 +- api/account.go | 25 ++--- api/api.go | 41 ++++--- api/artist.go | 25 ++--- api/release.go | 51 +++++---- api/track.go | 21 ++-- api/uploads.go | 6 +- controller/account.go | 3 +- controller/artist.go | 1 + {global => controller}/config.go | 47 ++------ controller/release.go | 1 + controller/track.go | 1 + discord/discord.go | 43 +++----- global/const.go | 3 - global/funcs.go | 101 ----------------- main.go | 182 +++++++++++++++++++++++-------- model/account.go | 2 + model/appstate.go | 32 ++++++ view/music.go | 18 ++- 24 files changed, 350 insertions(+), 375 deletions(-) rename {global => controller}/config.go (65%) delete mode 100644 global/const.go delete mode 100644 global/funcs.go create mode 100644 model/appstate.go diff --git a/.gitignore b/.gitignore index cccde2b..9bdf788 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ uploads/ docker-compose*.yml !docker-compose.example.yml config*.toml +arimelody-web diff --git a/admin/accounthttp.go b/admin/accounthttp.go index f0990df..b2cf18a 100644 --- a/admin/accounthttp.go +++ b/admin/accounthttp.go @@ -8,10 +8,8 @@ import ( "time" "arimelody-web/controller" - "arimelody-web/global" "arimelody-web/model" - "github.com/jmoiron/sqlx" "golang.org/x/crypto/bcrypt" ) @@ -21,11 +19,11 @@ type TemplateData struct { Token string } -func AccountHandler(db *sqlx.DB) http.Handler { +func AccountHandler(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { account := r.Context().Value("account").(*model.Account) - totps, err := controller.GetTOTPsForAccount(db, account.ID) + totps, err := controller.GetTOTPsForAccount(app.DB, account.ID) if err != nil { fmt.Printf("WARN: Failed to fetch TOTPs: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -47,10 +45,10 @@ func AccountHandler(db *sqlx.DB) http.Handler { }) } -func LoginHandler(db *sqlx.DB) http.Handler { +func LoginHandler(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { - account, err := controller.GetAccountByRequest(db, r) + account, err := controller.GetAccountByRequest(app.DB, r) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %v\n", err) @@ -107,7 +105,7 @@ func LoginHandler(db *sqlx.DB) http.Handler { TOTP: r.Form.Get("totp"), } - account, err := controller.GetAccount(db, credentials.Username) + account, err := controller.GetAccount(app.DB, credentials.Username) if err != nil { render(LoginResponse{ Message: "Invalid username or password" }) return @@ -123,7 +121,7 @@ func LoginHandler(db *sqlx.DB) http.Handler { return } - totps, err := controller.GetTOTPsForAccount(db, account.ID) + totps, err := controller.GetTOTPsForAccount(app.DB, account.ID) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to fetch TOTPs: %v\n", err) render(LoginResponse{ Message: "Something went wrong. Please try again." }) @@ -147,7 +145,7 @@ func LoginHandler(db *sqlx.DB) http.Handler { } // login success! - token, err := controller.CreateToken(db, account.ID, r.UserAgent()) + token, err := controller.CreateToken(app.DB, account.ID, r.UserAgent()) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to create token: %v\n", err) render(LoginResponse{ Message: "Something went wrong. Please try again." }) @@ -155,10 +153,10 @@ func LoginHandler(db *sqlx.DB) http.Handler { } cookie := http.Cookie{} - cookie.Name = global.COOKIE_TOKEN + cookie.Name = model.COOKIE_TOKEN cookie.Value = token.Token cookie.Expires = token.ExpiresAt - if strings.HasPrefix(global.Config.BaseUrl, "https") { + if strings.HasPrefix(app.Config.BaseUrl, "https") { cookie.Secure = true } cookie.HttpOnly = true @@ -169,17 +167,17 @@ func LoginHandler(db *sqlx.DB) http.Handler { }) } -func LogoutHandler(db *sqlx.DB) http.Handler { +func LogoutHandler(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.NotFound(w, r) return } - tokenStr := controller.GetTokenFromRequest(db, r) + tokenStr := controller.GetTokenFromRequest(app.DB, r) if len(tokenStr) > 0 { - err := controller.DeleteToken(db, tokenStr) + err := controller.DeleteToken(app.DB, tokenStr) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to revoke token: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -188,10 +186,10 @@ func LogoutHandler(db *sqlx.DB) http.Handler { } cookie := http.Cookie{} - cookie.Name = global.COOKIE_TOKEN + cookie.Name = model.COOKIE_TOKEN cookie.Value = "" cookie.Expires = time.Now() - if strings.HasPrefix(global.Config.BaseUrl, "https") { + if strings.HasPrefix(app.Config.BaseUrl, "https") { cookie.Secure = true } cookie.HttpOnly = true @@ -201,9 +199,9 @@ func LogoutHandler(db *sqlx.DB) http.Handler { }) } -func createAccountHandler(db *sqlx.DB) http.Handler { +func createAccountHandler(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - checkAccount, err := controller.GetAccountByRequest(db, r) + checkAccount, err := controller.GetAccountByRequest(app.DB, r) if err != nil { fmt.Printf("WARN: Failed to fetch account: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -260,7 +258,7 @@ func createAccountHandler(db *sqlx.DB) http.Handler { } // make sure code exists in DB - invite, err := controller.GetInvite(db, credentials.Invite) + invite, err := controller.GetInvite(app.DB, credentials.Invite) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve invite: %v\n", err) render(CreateAccountResponse{ @@ -270,7 +268,7 @@ func createAccountHandler(db *sqlx.DB) http.Handler { } if invite == nil || time.Now().After(invite.ExpiresAt) { if invite != nil { - err := controller.DeleteInvite(db, invite.Code) + err := controller.DeleteInvite(app.DB, invite.Code) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %v\n", err) } } render(CreateAccountResponse{ @@ -294,7 +292,7 @@ func createAccountHandler(db *sqlx.DB) http.Handler { Email: credentials.Email, AvatarURL: "/img/default-avatar.png", } - err = controller.CreateAccount(db, &account) + err = controller.CreateAccount(app.DB, &account) if err != nil { if strings.HasPrefix(err.Error(), "pq: duplicate key") { render(CreateAccountResponse{ @@ -309,11 +307,11 @@ func createAccountHandler(db *sqlx.DB) http.Handler { return } - err = controller.DeleteInvite(db, invite.Code) + err = controller.DeleteInvite(app.DB, invite.Code) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %v\n", err) } // registration success! - token, err := controller.CreateToken(db, account.ID, r.UserAgent()) + token, err := controller.CreateToken(app.DB, account.ID, r.UserAgent()) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to create token: %v\n", err) // gracefully redirect user to login page @@ -322,10 +320,10 @@ func createAccountHandler(db *sqlx.DB) http.Handler { } cookie := http.Cookie{} - cookie.Name = global.COOKIE_TOKEN + cookie.Name = model.COOKIE_TOKEN cookie.Value = token.Token cookie.Expires = token.ExpiresAt - if strings.HasPrefix(global.Config.BaseUrl, "https") { + if strings.HasPrefix(app.Config.BaseUrl, "https") { cookie.Secure = true } cookie.HttpOnly = true diff --git a/admin/artisthttp.go b/admin/artisthttp.go index af42cb1..d6a5e76 100644 --- a/admin/artisthttp.go +++ b/admin/artisthttp.go @@ -5,16 +5,15 @@ import ( "net/http" "strings" - "arimelody-web/global" "arimelody-web/model" "arimelody-web/controller" ) -func serveArtist() http.Handler { +func serveArtist(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { slices := strings.Split(r.URL.Path[1:], "/") id := slices[0] - artist, err := controller.GetArtist(global.DB, id) + artist, err := controller.GetArtist(app.DB, id) if err != nil { if artist == nil { http.NotFound(w, r) @@ -25,7 +24,7 @@ func serveArtist() http.Handler { return } - credits, err := controller.GetArtistCredits(global.DB, artist.ID, true) + credits, err := controller.GetArtistCredits(app.DB, artist.ID, true) if err != nil { fmt.Printf("Error rendering admin track page for %s: %s\n", id, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) diff --git a/admin/http.go b/admin/http.go index d118647..b44cfa9 100644 --- a/admin/http.go +++ b/admin/http.go @@ -9,28 +9,26 @@ import ( "arimelody-web/controller" "arimelody-web/model" - - "github.com/jmoiron/sqlx" ) -func Handler(db *sqlx.DB) http.Handler { +func Handler(app *model.AppState) http.Handler { mux := http.NewServeMux() - mux.Handle("/login", LoginHandler(db)) - mux.Handle("/register", createAccountHandler(db)) - mux.Handle("/logout", RequireAccount(db, LogoutHandler(db))) - mux.Handle("/account", RequireAccount(db, AccountHandler(db))) + mux.Handle("/login", LoginHandler(app)) + mux.Handle("/register", createAccountHandler(app)) + mux.Handle("/logout", RequireAccount(app, LogoutHandler(app))) + mux.Handle("/account", RequireAccount(app, AccountHandler(app))) mux.Handle("/static/", http.StripPrefix("/static", staticHandler())) - mux.Handle("/release/", RequireAccount(db, http.StripPrefix("/release", serveRelease()))) - mux.Handle("/artist/", RequireAccount(db, http.StripPrefix("/artist", serveArtist()))) - mux.Handle("/track/", RequireAccount(db, http.StripPrefix("/track", serveTrack()))) + mux.Handle("/release/", RequireAccount(app, http.StripPrefix("/release", serveRelease(app)))) + mux.Handle("/artist/", RequireAccount(app, http.StripPrefix("/artist", serveArtist(app)))) + mux.Handle("/track/", RequireAccount(app, http.StripPrefix("/track", serveTrack(app)))) mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } - account, err := controller.GetAccountByRequest(db, r) + account, err := controller.GetAccountByRequest(app.DB, r) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %s\n", err) } @@ -39,21 +37,21 @@ func Handler(db *sqlx.DB) http.Handler { return } - releases, err := controller.GetAllReleases(db, false, 0, true) + releases, err := controller.GetAllReleases(app.DB, false, 0, true) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - artists, err := controller.GetAllArtists(db) + artists, err := controller.GetAllArtists(app.DB) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to pull artists: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - tracks, err := controller.GetOrphanTracks(db) + tracks, err := controller.GetOrphanTracks(app.DB) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to pull orphan tracks: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -83,9 +81,9 @@ func Handler(db *sqlx.DB) http.Handler { return mux } -func RequireAccount(db *sqlx.DB, next http.Handler) http.HandlerFunc { +func RequireAccount(app *model.AppState, next http.Handler) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - account, err := controller.GetAccountByRequest(db, r) + account, err := controller.GetAccountByRequest(app.DB, r) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %v\n", err) diff --git a/admin/releasehttp.go b/admin/releasehttp.go index 9132fe8..503166b 100644 --- a/admin/releasehttp.go +++ b/admin/releasehttp.go @@ -5,19 +5,18 @@ import ( "net/http" "strings" - "arimelody-web/global" "arimelody-web/controller" "arimelody-web/model" ) -func serveRelease() http.Handler { +func serveRelease(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { slices := strings.Split(r.URL.Path[1:], "/") releaseID := slices[0] account := r.Context().Value("account").(*model.Account) - release, err := controller.GetRelease(global.DB, releaseID, true) + release, err := controller.GetRelease(app.DB, releaseID, true) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) @@ -34,10 +33,10 @@ func serveRelease() http.Handler { serveEditCredits(release).ServeHTTP(w, r) return case "addcredit": - serveAddCredit(release).ServeHTTP(w, r) + serveAddCredit(app, release).ServeHTTP(w, r) return case "newcredit": - serveNewCredit().ServeHTTP(w, r) + serveNewCredit(app).ServeHTTP(w, r) return case "editlinks": serveEditLinks(release).ServeHTTP(w, r) @@ -46,10 +45,10 @@ func serveRelease() http.Handler { serveEditTracks(release).ServeHTTP(w, r) return case "addtrack": - serveAddTrack(release).ServeHTTP(w, r) + serveAddTrack(app, release).ServeHTTP(w, r) return case "newtrack": - serveNewTrack().ServeHTTP(w, r) + serveNewTrack(app).ServeHTTP(w, r) return } http.NotFound(w, r) @@ -83,9 +82,9 @@ func serveEditCredits(release *model.Release) http.Handler { }) } -func serveAddCredit(release *model.Release) http.Handler { +func serveAddCredit(app *model.AppState, release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - artists, err := controller.GetArtistsNotOnRelease(global.DB, release.ID) + artists, err := controller.GetArtistsNotOnRelease(app.DB, release.ID) if err != nil { fmt.Printf("FATAL: Failed to pull artists not on %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -109,10 +108,10 @@ func serveAddCredit(release *model.Release) http.Handler { }) } -func serveNewCredit() http.Handler { +func serveNewCredit(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { artistID := strings.Split(r.URL.Path, "/")[3] - artist, err := controller.GetArtist(global.DB, artistID) + artist, err := controller.GetArtist(app.DB, artistID) if err != nil { fmt.Printf("FATAL: Failed to pull artists %s: %s\n", artistID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -154,9 +153,9 @@ func serveEditTracks(release *model.Release) http.Handler { }) } -func serveAddTrack(release *model.Release) http.Handler { +func serveAddTrack(app *model.AppState, release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - tracks, err := controller.GetTracksNotOnRelease(global.DB, release.ID) + tracks, err := controller.GetTracksNotOnRelease(app.DB, release.ID) if err != nil { fmt.Printf("FATAL: Failed to pull tracks not on %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -181,10 +180,10 @@ func serveAddTrack(release *model.Release) http.Handler { }) } -func serveNewTrack() http.Handler { +func serveNewTrack(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { trackID := strings.Split(r.URL.Path, "/")[3] - track, err := controller.GetTrack(global.DB, trackID) + track, err := controller.GetTrack(app.DB, trackID) if err != nil { fmt.Printf("Error rendering new track component for %s: %s\n", trackID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) diff --git a/admin/trackhttp.go b/admin/trackhttp.go index 2cea123..fa49b53 100644 --- a/admin/trackhttp.go +++ b/admin/trackhttp.go @@ -5,16 +5,15 @@ import ( "net/http" "strings" - "arimelody-web/global" "arimelody-web/model" "arimelody-web/controller" ) -func serveTrack() http.Handler { +func serveTrack(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { slices := strings.Split(r.URL.Path[1:], "/") id := slices[0] - track, err := controller.GetTrack(global.DB, id) + track, err := controller.GetTrack(app.DB, id) if err != nil { fmt.Printf("Error rendering admin track page for %s: %s\n", id, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -25,7 +24,7 @@ func serveTrack() http.Handler { return } - releases, err := controller.GetTrackReleases(global.DB, track.ID, true) + releases, err := controller.GetTrackReleases(app.DB, track.ID, true) if err != nil { fmt.Printf("FATAL: Failed to pull releases for %s: %s\n", id, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) diff --git a/api/account.go b/api/account.go index 3ce52c8..0a9a7f9 100644 --- a/api/account.go +++ b/api/account.go @@ -3,7 +3,6 @@ package api import ( "arimelody-web/controller" "arimelody-web/model" - "arimelody-web/global" "encoding/json" "fmt" "net/http" @@ -14,7 +13,7 @@ import ( "golang.org/x/crypto/bcrypt" ) -func handleLogin() http.HandlerFunc { +func handleLogin(app *model.AppState) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.NotFound(w, r) @@ -33,7 +32,7 @@ func handleLogin() http.HandlerFunc { return } - account, err := controller.GetAccount(global.DB, credentials.Username) + account, err := controller.GetAccount(app.DB, credentials.Username) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve account: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -50,7 +49,7 @@ func handleLogin() http.HandlerFunc { return } - token, err := controller.CreateToken(global.DB, account.ID, r.UserAgent()) + token, err := controller.CreateToken(app.DB, account.ID, r.UserAgent()) type LoginResponse struct { Token string `json:"token"` ExpiresAt time.Time `json:"expires_at"` @@ -67,7 +66,7 @@ func handleLogin() http.HandlerFunc { }) } -func handleAccountRegistration() http.HandlerFunc { +func handleAccountRegistration(app *model.AppState) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.NotFound(w, r) @@ -89,7 +88,7 @@ func handleAccountRegistration() http.HandlerFunc { } // make sure code exists in DB - invite, err := controller.GetInvite(global.DB, credentials.Invite) + invite, err := controller.GetInvite(app.DB, credentials.Invite) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve invite: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -101,7 +100,7 @@ func handleAccountRegistration() http.HandlerFunc { } if time.Now().After(invite.ExpiresAt) { - err := controller.DeleteInvite(global.DB, invite.Code) + err := controller.DeleteInvite(app.DB, invite.Code) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %v\n", err) } http.Error(w, "Invalid invite code", http.StatusBadRequest) return @@ -120,7 +119,7 @@ func handleAccountRegistration() http.HandlerFunc { Email: credentials.Email, AvatarURL: "/img/default-avatar.png", } - err = controller.CreateAccount(global.DB, &account) + err = controller.CreateAccount(app.DB, &account) if err != nil { if strings.HasPrefix(err.Error(), "pq: duplicate key") { http.Error(w, "An account with that username already exists", http.StatusBadRequest) @@ -131,10 +130,10 @@ func handleAccountRegistration() http.HandlerFunc { return } - err = controller.DeleteInvite(global.DB, invite.Code) + err = controller.DeleteInvite(app.DB, invite.Code) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %v\n", err) } - token, err := controller.CreateToken(global.DB, account.ID, r.UserAgent()) + token, err := controller.CreateToken(app.DB, account.ID, r.UserAgent()) type LoginResponse struct { Token string `json:"token"` ExpiresAt time.Time `json:"expires_at"` @@ -151,7 +150,7 @@ func handleAccountRegistration() http.HandlerFunc { }) } -func handleDeleteAccount() http.HandlerFunc { +func handleDeleteAccount(app *model.AppState) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.NotFound(w, r) @@ -170,7 +169,7 @@ func handleDeleteAccount() http.HandlerFunc { return } - account, err := controller.GetAccount(global.DB, credentials.Username) + account, err := controller.GetAccount(app.DB, credentials.Username) if err != nil { if strings.Contains(err.Error(), "no rows") { http.Error(w, "Invalid username or password", http.StatusBadRequest) @@ -189,7 +188,7 @@ func handleDeleteAccount() http.HandlerFunc { // TODO: check TOTP - err = controller.DeleteAccount(global.DB, account.Username) + err = controller.DeleteAccount(app.DB, account.Username) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete account: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) diff --git a/api/api.go b/api/api.go index 26e2255..b16d45d 100644 --- a/api/api.go +++ b/api/api.go @@ -7,11 +7,10 @@ import ( "arimelody-web/admin" "arimelody-web/controller" - - "github.com/jmoiron/sqlx" + "arimelody-web/model" ) -func Handler(db *sqlx.DB) http.Handler { +func Handler(app *model.AppState) http.Handler { mux := http.NewServeMux() // ACCOUNT ENDPOINTS @@ -32,7 +31,7 @@ func Handler(db *sqlx.DB) http.Handler { mux.Handle("/v1/artist/", http.StripPrefix("/v1/artist", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var artistID = strings.Split(r.URL.Path[1:], "/")[0] - artist, err := controller.GetArtist(db, artistID) + artist, err := controller.GetArtist(app.DB, artistID) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) @@ -46,13 +45,13 @@ func Handler(db *sqlx.DB) http.Handler { switch r.Method { case http.MethodGet: // GET /api/v1/artist/{id} - ServeArtist(artist).ServeHTTP(w, r) + ServeArtist(app, artist).ServeHTTP(w, r) case http.MethodPut: // PUT /api/v1/artist/{id} (admin) - admin.RequireAccount(db, UpdateArtist(artist)).ServeHTTP(w, r) + admin.RequireAccount(app, UpdateArtist(app, artist)).ServeHTTP(w, r) case http.MethodDelete: // DELETE /api/v1/artist/{id} (admin) - admin.RequireAccount(db, DeleteArtist(artist)).ServeHTTP(w, r) + admin.RequireAccount(app, DeleteArtist(app, artist)).ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -61,10 +60,10 @@ func Handler(db *sqlx.DB) http.Handler { switch r.Method { case http.MethodGet: // GET /api/v1/artist - ServeAllArtists().ServeHTTP(w, r) + ServeAllArtists(app).ServeHTTP(w, r) case http.MethodPost: // POST /api/v1/artist (admin) - admin.RequireAccount(db, CreateArtist()).ServeHTTP(w, r) + admin.RequireAccount(app, CreateArtist(app)).ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -74,7 +73,7 @@ func Handler(db *sqlx.DB) http.Handler { mux.Handle("/v1/music/", http.StripPrefix("/v1/music", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var releaseID = strings.Split(r.URL.Path[1:], "/")[0] - release, err := controller.GetRelease(db, releaseID, true) + release, err := controller.GetRelease(app.DB, releaseID, true) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) @@ -88,13 +87,13 @@ func Handler(db *sqlx.DB) http.Handler { switch r.Method { case http.MethodGet: // GET /api/v1/music/{id} - ServeRelease(release).ServeHTTP(w, r) + ServeRelease(app, release).ServeHTTP(w, r) case http.MethodPut: // PUT /api/v1/music/{id} (admin) - admin.RequireAccount(db, UpdateRelease(release)).ServeHTTP(w, r) + admin.RequireAccount(app, UpdateRelease(app, release)).ServeHTTP(w, r) case http.MethodDelete: // DELETE /api/v1/music/{id} (admin) - admin.RequireAccount(db, DeleteRelease(release)).ServeHTTP(w, r) + admin.RequireAccount(app, DeleteRelease(app, release)).ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -103,10 +102,10 @@ func Handler(db *sqlx.DB) http.Handler { switch r.Method { case http.MethodGet: // GET /api/v1/music - ServeCatalog().ServeHTTP(w, r) + ServeCatalog(app).ServeHTTP(w, r) case http.MethodPost: // POST /api/v1/music (admin) - admin.RequireAccount(db, CreateRelease()).ServeHTTP(w, r) + admin.RequireAccount(app, CreateRelease(app)).ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -116,7 +115,7 @@ func Handler(db *sqlx.DB) http.Handler { mux.Handle("/v1/track/", http.StripPrefix("/v1/track", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var trackID = strings.Split(r.URL.Path[1:], "/")[0] - track, err := controller.GetTrack(db, trackID) + track, err := controller.GetTrack(app.DB, trackID) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) @@ -130,13 +129,13 @@ func Handler(db *sqlx.DB) http.Handler { switch r.Method { case http.MethodGet: // GET /api/v1/track/{id} (admin) - admin.RequireAccount(db, ServeTrack(track)).ServeHTTP(w, r) + admin.RequireAccount(app, ServeTrack(app, track)).ServeHTTP(w, r) case http.MethodPut: // PUT /api/v1/track/{id} (admin) - admin.RequireAccount(db, UpdateTrack(track)).ServeHTTP(w, r) + admin.RequireAccount(app, UpdateTrack(app, track)).ServeHTTP(w, r) case http.MethodDelete: // DELETE /api/v1/track/{id} (admin) - admin.RequireAccount(db, DeleteTrack(track)).ServeHTTP(w, r) + admin.RequireAccount(app, DeleteTrack(app, track)).ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -145,10 +144,10 @@ func Handler(db *sqlx.DB) http.Handler { switch r.Method { case http.MethodGet: // GET /api/v1/track (admin) - admin.RequireAccount(db, ServeAllTracks()).ServeHTTP(w, r) + admin.RequireAccount(app, ServeAllTracks(app)).ServeHTTP(w, r) case http.MethodPost: // POST /api/v1/track (admin) - admin.RequireAccount(db, CreateTrack()).ServeHTTP(w, r) + admin.RequireAccount(app, CreateTrack(app)).ServeHTTP(w, r) default: http.NotFound(w, r) } diff --git a/api/artist.go b/api/artist.go index c46db59..a9676b1 100644 --- a/api/artist.go +++ b/api/artist.go @@ -10,15 +10,14 @@ import ( "strings" "time" - "arimelody-web/global" "arimelody-web/controller" "arimelody-web/model" ) -func ServeAllArtists() http.Handler { +func ServeAllArtists(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var artists = []*model.Artist{} - artists, err := controller.GetAllArtists(global.DB) + artists, err := controller.GetAllArtists(app.DB) if err != nil { fmt.Printf("WARN: Failed to serve all artists: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -35,7 +34,7 @@ func ServeAllArtists() http.Handler { }) } -func ServeArtist(artist *model.Artist) http.Handler { +func ServeArtist(app *model.AppState, artist *model.Artist) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { type ( creditJSON struct { @@ -52,7 +51,7 @@ func ServeArtist(artist *model.Artist) http.Handler { } ) - account, err := controller.GetAccountByRequest(global.DB, r) + account, err := controller.GetAccountByRequest(app.DB, r) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -60,7 +59,7 @@ func ServeArtist(artist *model.Artist) http.Handler { } show_hidden_releases := account != nil - dbCredits, err := controller.GetArtistCredits(global.DB, artist.ID, show_hidden_releases) + dbCredits, err := controller.GetArtistCredits(app.DB, artist.ID, show_hidden_releases) if err != nil { fmt.Printf("WARN: Failed to retrieve artist credits for %s: %v\n", artist.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -92,7 +91,7 @@ func ServeArtist(artist *model.Artist) http.Handler { }) } -func CreateArtist() http.Handler { +func CreateArtist(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var artist model.Artist err := json.NewDecoder(r.Body).Decode(&artist) @@ -107,7 +106,7 @@ func CreateArtist() http.Handler { } if artist.Name == "" { artist.Name = artist.ID } - err = controller.CreateArtist(global.DB, &artist) + err = controller.CreateArtist(app.DB, &artist) if err != nil { if strings.Contains(err.Error(), "duplicate key") { http.Error(w, fmt.Sprintf("Artist %s already exists\n", artist.ID), http.StatusBadRequest) @@ -122,7 +121,7 @@ func CreateArtist() http.Handler { }) } -func UpdateArtist(artist *model.Artist) http.Handler { +func UpdateArtist(app *model.AppState, artist *model.Artist) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := json.NewDecoder(r.Body).Decode(&artist) if err != nil { @@ -136,7 +135,7 @@ func UpdateArtist(artist *model.Artist) http.Handler { } else { if strings.Contains(artist.Avatar, ";base64,") { var artworkDirectory = filepath.Join("uploads", "avatar") - filename, err := HandleImageUpload(&artist.Avatar, artworkDirectory, artist.ID) + filename, err := HandleImageUpload(app, &artist.Avatar, artworkDirectory, artist.ID) // clean up files with this ID and different extensions err = filepath.Walk(artworkDirectory, func(path string, info fs.FileInfo, err error) error { @@ -155,7 +154,7 @@ func UpdateArtist(artist *model.Artist) http.Handler { } } - err = controller.UpdateArtist(global.DB, artist) + err = controller.UpdateArtist(app.DB, artist) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) @@ -167,9 +166,9 @@ func UpdateArtist(artist *model.Artist) http.Handler { }) } -func DeleteArtist(artist *model.Artist) http.Handler { +func DeleteArtist(app *model.AppState, artist *model.Artist) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - err := controller.DeleteArtist(global.DB, artist.ID) + err := controller.DeleteArtist(app.DB, artist.ID) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) diff --git a/api/release.go b/api/release.go index d17fb5f..c71043e 100644 --- a/api/release.go +++ b/api/release.go @@ -10,17 +10,16 @@ import ( "strings" "time" - "arimelody-web/global" "arimelody-web/controller" "arimelody-web/model" ) -func ServeRelease(release *model.Release) http.Handler { +func ServeRelease(app *model.AppState, release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // only allow authorised users to view hidden releases privileged := false if !release.Visible { - account, err := controller.GetAccountByRequest(global.DB, r) + account, err := controller.GetAccountByRequest(app.DB, r) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -67,14 +66,14 @@ func ServeRelease(release *model.Release) http.Handler { if release.IsReleased() || privileged { // get credits - credits, err := controller.GetReleaseCredits(global.DB, release.ID) + credits, err := controller.GetReleaseCredits(app.DB, release.ID) if err != nil { fmt.Printf("WARN: Failed to serve release %s: Credits: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } for _, credit := range credits { - artist, err := controller.GetArtist(global.DB, credit.Artist.ID) + artist, err := controller.GetArtist(app.DB, credit.Artist.ID) if err != nil { fmt.Printf("WARN: Failed to serve release %s: Artists: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -89,7 +88,7 @@ func ServeRelease(release *model.Release) http.Handler { } // get tracks - tracks, err := controller.GetReleaseTracks(global.DB, release.ID) + tracks, err := controller.GetReleaseTracks(app.DB, release.ID) if err != nil { fmt.Printf("WARN: Failed to serve release %s: Tracks: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -104,7 +103,7 @@ func ServeRelease(release *model.Release) http.Handler { } // get links - links, err := controller.GetReleaseLinks(global.DB, release.ID) + links, err := controller.GetReleaseLinks(app.DB, release.ID) if err != nil { fmt.Printf("WARN: Failed to serve release %s: Links: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -126,9 +125,9 @@ func ServeRelease(release *model.Release) http.Handler { }) } -func ServeCatalog() http.Handler { +func ServeCatalog(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - releases, err := controller.GetAllReleases(global.DB, false, 0, true) + releases, err := controller.GetAllReleases(app.DB, false, 0, true) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return @@ -146,7 +145,7 @@ func ServeCatalog() http.Handler { } catalog := []Release{} - account, err := controller.GetAccountByRequest(global.DB, r) + account, err := controller.GetAccountByRequest(app.DB, r) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -192,7 +191,7 @@ func ServeCatalog() http.Handler { }) } -func CreateRelease() http.Handler { +func CreateRelease(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.NotFound(w, r) @@ -220,7 +219,7 @@ func CreateRelease() http.Handler { if release.Artwork == "" { release.Artwork = "/img/default-cover-art.png" } - err = controller.CreateRelease(global.DB, &release) + err = controller.CreateRelease(app.DB, &release) if err != nil { if strings.Contains(err.Error(), "duplicate key") { http.Error(w, fmt.Sprintf("Release %s already exists\n", release.ID), http.StatusBadRequest) @@ -243,7 +242,7 @@ func CreateRelease() http.Handler { }) } -func UpdateRelease(release *model.Release) http.Handler { +func UpdateRelease(app *model.AppState, release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { http.NotFound(w, r) @@ -255,11 +254,11 @@ func UpdateRelease(release *model.Release) http.Handler { if len(segments) == 2 { switch segments[1] { case "tracks": - UpdateReleaseTracks(release).ServeHTTP(w, r) + UpdateReleaseTracks(app, release).ServeHTTP(w, r) case "credits": - UpdateReleaseCredits(release).ServeHTTP(w, r) + UpdateReleaseCredits(app, release).ServeHTTP(w, r) case "links": - UpdateReleaseLinks(release).ServeHTTP(w, r) + UpdateReleaseLinks(app, release).ServeHTTP(w, r) } return } @@ -281,7 +280,7 @@ func UpdateRelease(release *model.Release) http.Handler { } else { if strings.Contains(release.Artwork, ";base64,") { var artworkDirectory = filepath.Join("uploads", "musicart") - filename, err := HandleImageUpload(&release.Artwork, artworkDirectory, release.ID) + filename, err := HandleImageUpload(app, &release.Artwork, artworkDirectory, release.ID) // clean up files with this ID and different extensions err = filepath.Walk(artworkDirectory, func(path string, info fs.FileInfo, err error) error { @@ -300,7 +299,7 @@ func UpdateRelease(release *model.Release) http.Handler { } } - err = controller.UpdateRelease(global.DB, release) + err = controller.UpdateRelease(app.DB, release) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) @@ -312,7 +311,7 @@ func UpdateRelease(release *model.Release) http.Handler { }) } -func UpdateReleaseTracks(release *model.Release) http.Handler { +func UpdateReleaseTracks(app *model.AppState, release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var trackIDs = []string{} err := json.NewDecoder(r.Body).Decode(&trackIDs) @@ -321,7 +320,7 @@ func UpdateReleaseTracks(release *model.Release) http.Handler { return } - err = controller.UpdateReleaseTracks(global.DB, release.ID, trackIDs) + err = controller.UpdateReleaseTracks(app.DB, release.ID, trackIDs) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) @@ -333,7 +332,7 @@ func UpdateReleaseTracks(release *model.Release) http.Handler { }) } -func UpdateReleaseCredits(release *model.Release) http.Handler { +func UpdateReleaseCredits(app *model.AppState, release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { type creditJSON struct { Artist string @@ -358,7 +357,7 @@ func UpdateReleaseCredits(release *model.Release) http.Handler { }) } - err = controller.UpdateReleaseCredits(global.DB, release.ID, credits) + err = controller.UpdateReleaseCredits(app.DB, release.ID, credits) if err != nil { if strings.Contains(err.Error(), "duplicate key") { http.Error(w, "Artists may only be credited once\n", http.StatusBadRequest) @@ -374,7 +373,7 @@ func UpdateReleaseCredits(release *model.Release) http.Handler { }) } -func UpdateReleaseLinks(release *model.Release) http.Handler { +func UpdateReleaseLinks(app *model.AppState, release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPut { http.NotFound(w, r) @@ -388,7 +387,7 @@ func UpdateReleaseLinks(release *model.Release) http.Handler { return } - err = controller.UpdateReleaseLinks(global.DB, release.ID, links) + err = controller.UpdateReleaseLinks(app.DB, release.ID, links) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) @@ -400,9 +399,9 @@ func UpdateReleaseLinks(release *model.Release) http.Handler { }) } -func DeleteRelease(release *model.Release) http.Handler { +func DeleteRelease(app *model.AppState, release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - err := controller.DeleteRelease(global.DB, release.ID) + err := controller.DeleteRelease(app.DB, release.ID) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) diff --git a/api/track.go b/api/track.go index 8727b4f..c342e08 100644 --- a/api/track.go +++ b/api/track.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" - "arimelody-web/global" "arimelody-web/controller" "arimelody-web/model" ) @@ -17,7 +16,7 @@ type ( } ) -func ServeAllTracks() http.Handler { +func ServeAllTracks(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { type Track struct { ID string `json:"id"` @@ -26,7 +25,7 @@ func ServeAllTracks() http.Handler { var tracks = []Track{} var dbTracks = []*model.Track{} - dbTracks, err := controller.GetAllTracks(global.DB) + dbTracks, err := controller.GetAllTracks(app.DB) if err != nil { fmt.Printf("WARN: Failed to pull tracks from DB: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -50,9 +49,9 @@ func ServeAllTracks() http.Handler { }) } -func ServeTrack(track *model.Track) http.Handler { +func ServeTrack(app *model.AppState, track *model.Track) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - dbReleases, err := controller.GetTrackReleases(global.DB, track.ID, false) + dbReleases, err := controller.GetTrackReleases(app.DB, track.ID, false) if err != nil { fmt.Printf("WARN: Failed to pull track releases for %s from DB: %s\n", track.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -74,7 +73,7 @@ func ServeTrack(track *model.Track) http.Handler { }) } -func CreateTrack() http.Handler { +func CreateTrack(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.NotFound(w, r) @@ -93,7 +92,7 @@ func CreateTrack() http.Handler { return } - id, err := controller.CreateTrack(global.DB, &track) + id, err := controller.CreateTrack(app.DB, &track) if err != nil { fmt.Printf("WARN: Failed to create track: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -106,7 +105,7 @@ func CreateTrack() http.Handler { }) } -func UpdateTrack(track *model.Track) http.Handler { +func UpdateTrack(app *model.AppState, track *model.Track) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPut || r.URL.Path == "/" { http.NotFound(w, r) @@ -124,7 +123,7 @@ func UpdateTrack(track *model.Track) http.Handler { return } - err = controller.UpdateTrack(global.DB, track) + err = controller.UpdateTrack(app.DB, track) if err != nil { fmt.Printf("WARN: Failed to update track %s: %s\n", track.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -141,7 +140,7 @@ func UpdateTrack(track *model.Track) http.Handler { }) } -func DeleteTrack(track *model.Track) http.Handler { +func DeleteTrack(app *model.AppState, track *model.Track) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete || r.URL.Path == "/" { http.NotFound(w, r) @@ -149,7 +148,7 @@ func DeleteTrack(track *model.Track) http.Handler { } var trackID = r.URL.Path[1:] - err := controller.DeleteTrack(global.DB, trackID) + err := controller.DeleteTrack(app.DB, trackID) if err != nil { fmt.Printf("WARN: Failed to delete track %s: %s\n", trackID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) diff --git a/api/uploads.go b/api/uploads.go index 6b1c496..ddcf6ee 100644 --- a/api/uploads.go +++ b/api/uploads.go @@ -1,7 +1,7 @@ package api import ( - "arimelody-web/global" + "arimelody-web/model" "bufio" "encoding/base64" "errors" @@ -11,12 +11,12 @@ import ( "strings" ) -func HandleImageUpload(data *string, directory string, filename string) (string, error) { +func HandleImageUpload(app *model.AppState, data *string, directory string, filename string) (string, error) { split := strings.Split(*data, ";base64,") header := split[0] imageData, err := base64.StdEncoding.DecodeString(split[1]) ext, _ := strings.CutPrefix(header, "data:image/") - directory = filepath.Join(global.Config.DataDirectory, directory) + directory = filepath.Join(app.Config.DataDirectory, directory) switch ext { case "png": diff --git a/controller/account.go b/controller/account.go index 3547d35..044faec 100644 --- a/controller/account.go +++ b/controller/account.go @@ -1,7 +1,6 @@ package controller import ( - "arimelody-web/global" "arimelody-web/model" "errors" "fmt" @@ -72,7 +71,7 @@ func GetTokenFromRequest(db *sqlx.DB, r *http.Request) string { return tokenStr } - cookie, err := r.Cookie(global.COOKIE_TOKEN) + cookie, err := r.Cookie(model.COOKIE_TOKEN) if err != nil { return "" } diff --git a/controller/artist.go b/controller/artist.go index c52b78d..1a613aa 100644 --- a/controller/artist.go +++ b/controller/artist.go @@ -2,6 +2,7 @@ package controller import ( "arimelody-web/model" + "github.com/jmoiron/sqlx" ) diff --git a/global/config.go b/controller/config.go similarity index 65% rename from global/config.go rename to controller/config.go index 20e152f..28d4be4 100644 --- a/global/config.go +++ b/controller/config.go @@ -1,4 +1,4 @@ -package global +package controller import ( "errors" @@ -6,44 +6,21 @@ import ( "os" "strconv" - "github.com/jmoiron/sqlx" + "arimelody-web/model" + "github.com/pelletier/go-toml/v2" ) -type ( - dbConfig struct { - Host string `toml:"host"` - Port int64 `toml:"port"` - Name string `toml:"name"` - User string `toml:"user"` - Pass string `toml:"pass"` - } - - discordConfig struct { - AdminID string `toml:"admin_id" comment:"NOTE: admin_id to be deprecated in favour of local accounts and SSO."` - ClientID string `toml:"client_id"` - Secret string `toml:"secret"` - } - - config struct { - BaseUrl string `toml:"base_url" comment:"Used for OAuth redirects."` - Port int64 `toml:"port"` - DataDirectory string `toml:"data_dir"` - DB dbConfig `toml:"db"` - Discord discordConfig `toml:"discord"` - } -) - -var Config = func() config { +func GetConfig() model.Config { configFile := os.Getenv("ARIMELODY_CONFIG") if configFile == "" { configFile = "config.toml" } - config := config{ + config := model.Config{ BaseUrl: "https://arimelody.me", Port: 8080, - DB: dbConfig{ + DB: model.DBConfig{ Host: "127.0.0.1", Port: 5432, User: "arimelody", @@ -63,20 +40,18 @@ var Config = func() config { err = toml.Unmarshal([]byte(data), &config) if err != nil { - fmt.Fprintf(os.Stderr, "FATAL: Failed to parse configuration file: %v\n", err) - os.Exit(1) + panic(fmt.Sprintf("FATAL: Failed to parse configuration file: %v\n", err)) } err = handleConfigOverrides(&config) if err != nil { - fmt.Fprintf(os.Stderr, "FATAL: Failed to parse environment variable %v\n", err) - os.Exit(1) + panic(fmt.Sprintf("FATAL: Failed to parse environment variable %v\n", err)) } return config -}() +} -func handleConfigOverrides(config *config) error { +func handleConfigOverrides(config *model.Config) error { var err error if env, has := os.LookupEnv("ARIMELODY_BASE_URL"); has { config.BaseUrl = env } @@ -101,5 +76,3 @@ func handleConfigOverrides(config *config) error { return nil } - -var DB *sqlx.DB diff --git a/controller/release.go b/controller/release.go index c9791ac..362669a 100644 --- a/controller/release.go +++ b/controller/release.go @@ -5,6 +5,7 @@ import ( "fmt" "arimelody-web/model" + "github.com/jmoiron/sqlx" ) diff --git a/controller/track.go b/controller/track.go index d302045..fa4efc1 100644 --- a/controller/track.go +++ b/controller/track.go @@ -2,6 +2,7 @@ package controller import ( "arimelody-web/model" + "github.com/jmoiron/sqlx" ) diff --git a/discord/discord.go b/discord/discord.go index 8952c85..d46f32d 100644 --- a/discord/discord.go +++ b/discord/discord.go @@ -1,38 +1,17 @@ package discord import ( + "arimelody-web/model" "encoding/json" "errors" "fmt" "net/http" "net/url" "strings" - - "arimelody-web/global" ) const API_ENDPOINT = "https://discord.com/api/v10" -var CREDENTIALS_PROVIDED = true -var CLIENT_ID = func() string { - id := global.Config.Discord.ClientID - if id == "" { - // fmt.Printf("WARN: Discord client ID (DISCORD_CLIENT) was not provided.\n") - CREDENTIALS_PROVIDED = false - } - return id -}() -var CLIENT_SECRET = func() string { - secret := global.Config.Discord.Secret - if secret == "" { - // fmt.Printf("WARN: Discord secret (DISCORD_SECRET) was not provided.\n") - CREDENTIALS_PROVIDED = false - } - return secret -}() -var OAUTH_CALLBACK_URI = fmt.Sprintf("%s/admin/login", global.Config.BaseUrl) -var REDIRECT_URI = fmt.Sprintf("https://discord.com/oauth2/authorize?client_id=%s&response_type=code&redirect_uri=%s&scope=identify", CLIENT_ID, OAUTH_CALLBACK_URI) - type ( AccessTokenResponse struct { AccessToken string `json:"access_token"` @@ -68,15 +47,15 @@ type ( } ) -func GetOAuthTokenFromCode(code string) (string, error) { +func GetOAuthTokenFromCode(app *model.AppState, code string) (string, error) { // let's get an oauth token! req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/oauth2/token", API_ENDPOINT), strings.NewReader(url.Values{ - "client_id": {CLIENT_ID}, - "client_secret": {CLIENT_SECRET}, + "client_id": {app.Config.Discord.ClientID}, + "client_secret": {app.Config.Discord.Secret}, "grant_type": {"authorization_code"}, "code": {code}, - "redirect_uri": {OAUTH_CALLBACK_URI}, + "redirect_uri": {GetOAuthCallbackURI(app.Config.BaseUrl)}, }.Encode())) req.Header.Add("Content-Type", "application/x-www-form-urlencoded") @@ -115,3 +94,15 @@ func GetDiscordUserFromAuth(token string) (DiscordUser, error) { return auth_info.User, nil } + +func GetOAuthCallbackURI(baseURL string) string { + return fmt.Sprintf("%s/admin/login", baseURL) +} + +func GetRedirectURI(app *model.AppState) string { + return fmt.Sprintf( + "https://discord.com/oauth2/authorize?client_id=%s&response_type=code&redirect_uri=%s&scope=identify", + app.Config.Discord.ClientID, + GetOAuthCallbackURI(app.Config.BaseUrl), + ) +} diff --git a/global/const.go b/global/const.go deleted file mode 100644 index 157668d..0000000 --- a/global/const.go +++ /dev/null @@ -1,3 +0,0 @@ -package global - -const COOKIE_TOKEN string = "AM_TOKEN" diff --git a/global/funcs.go b/global/funcs.go deleted file mode 100644 index 49edb01..0000000 --- a/global/funcs.go +++ /dev/null @@ -1,101 +0,0 @@ -package global - -import ( - "fmt" - "math/rand" - "net/http" - "strconv" - "time" - - "arimelody-web/colour" -) - -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]) - }) -} diff --git a/main.go b/main.go index cbda0a7..23fc997 100644 --- a/main.go +++ b/main.go @@ -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]) + }) +} diff --git a/model/account.go b/model/account.go index 031cae9..72720a0 100644 --- a/model/account.go +++ b/model/account.go @@ -2,6 +2,8 @@ package model import "time" +const COOKIE_TOKEN string = "AM_TOKEN" + type ( Account struct { ID string `json:"id" db:"id"` diff --git a/model/appstate.go b/model/appstate.go new file mode 100644 index 0000000..08016b7 --- /dev/null +++ b/model/appstate.go @@ -0,0 +1,32 @@ +package model + +import "github.com/jmoiron/sqlx" + +type ( + DBConfig struct { + Host string `toml:"host"` + Port int64 `toml:"port"` + Name string `toml:"name"` + User string `toml:"user"` + Pass string `toml:"pass"` + } + + DiscordConfig struct { + AdminID string `toml:"admin_id" comment:"NOTE: admin_id to be deprecated in favour of local accounts and SSO."` + ClientID string `toml:"client_id"` + Secret string `toml:"secret"` + } + + Config struct { + BaseUrl string `toml:"base_url" comment:"Used for OAuth redirects."` + Port int64 `toml:"port"` + DataDirectory string `toml:"data_dir"` + DB DBConfig `toml:"db"` + Discord DiscordConfig `toml:"discord"` + } + + AppState struct { + DB *sqlx.DB + Config Config + } +) diff --git a/view/music.go b/view/music.go index 227aa5c..cae325f 100644 --- a/view/music.go +++ b/view/music.go @@ -8,36 +8,34 @@ import ( "arimelody-web/controller" "arimelody-web/model" "arimelody-web/templates" - - "github.com/jmoiron/sqlx" ) // HTTP HANDLER METHODS -func MusicHandler(db *sqlx.DB) http.Handler { +func MusicHandler(app *model.AppState) http.Handler { mux := http.NewServeMux() mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { - ServeCatalog(db).ServeHTTP(w, r) + ServeCatalog(app).ServeHTTP(w, r) return } - release, err := controller.GetRelease(db, r.URL.Path[1:], true) + release, err := controller.GetRelease(app.DB, r.URL.Path[1:], true) if err != nil { http.NotFound(w, r) return } - ServeGateway(db, release).ServeHTTP(w, r) + ServeGateway(app, release).ServeHTTP(w, r) })) return mux } -func ServeCatalog(db *sqlx.DB) http.Handler { +func ServeCatalog(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - releases, err := controller.GetAllReleases(db, true, 0, true) + releases, err := controller.GetAllReleases(app.DB, true, 0, true) if err != nil { fmt.Printf("FATAL: Failed to pull releases for catalog: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -57,12 +55,12 @@ func ServeCatalog(db *sqlx.DB) http.Handler { }) } -func ServeGateway(db *sqlx.DB, release *model.Release) http.Handler { +func ServeGateway(app *model.AppState, release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // only allow authorised users to view hidden releases privileged := false if !release.Visible { - account, err := controller.GetAccountByRequest(db, r) + account, err := controller.GetAccountByRequest(app.DB, r) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) From 5531ef6bab6b55bed292705ebe60e7fb794f907f Mon Sep 17 00:00:00 2001 From: ari melody Date: Tue, 21 Jan 2025 15:08:59 +0000 Subject: [PATCH 02/99] remove account API endpoints account management should be done on the frontend. some work will need to be done to generate API keys for external clients, but notably some API endpoints are currently used by the frontend using session tokens. --- api/account.go | 201 ------------------------------------------------- api/api.go | 12 --- 2 files changed, 213 deletions(-) delete mode 100644 api/account.go diff --git a/api/account.go b/api/account.go deleted file mode 100644 index 0a9a7f9..0000000 --- a/api/account.go +++ /dev/null @@ -1,201 +0,0 @@ -package api - -import ( - "arimelody-web/controller" - "arimelody-web/model" - "encoding/json" - "fmt" - "net/http" - "os" - "strings" - "time" - - "golang.org/x/crypto/bcrypt" -) - -func handleLogin(app *model.AppState) http.HandlerFunc { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.NotFound(w, r) - return - } - - type LoginRequest struct { - Username string `json:"username"` - Password string `json:"password"` - } - - credentials := LoginRequest{} - err := json.NewDecoder(r.Body).Decode(&credentials) - if err != nil { - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - account, err := controller.GetAccount(app.DB, credentials.Username) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve account: %v\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - if account == nil { - http.Error(w, "Invalid username or password", http.StatusBadRequest) - return - } - - err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(credentials.Password)) - if err != nil { - http.Error(w, "Invalid username or password", http.StatusBadRequest) - return - } - - token, err := controller.CreateToken(app.DB, account.ID, r.UserAgent()) - type LoginResponse struct { - Token string `json:"token"` - ExpiresAt time.Time `json:"expires_at"` - } - - err = json.NewEncoder(w).Encode(LoginResponse{ - Token: token.Token, - ExpiresAt: token.ExpiresAt, - }) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to return session token: %v\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } - }) -} - -func handleAccountRegistration(app *model.AppState) http.HandlerFunc { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.NotFound(w, r) - return - } - - type RegisterRequest struct { - Username string `json:"username"` - Email string `json:"email"` - Password string `json:"password"` - Invite string `json:"invite"` - } - - credentials := RegisterRequest{} - err := json.NewDecoder(r.Body).Decode(&credentials) - if err != nil { - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - // make sure code exists in DB - invite, err := controller.GetInvite(app.DB, credentials.Invite) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve invite: %v\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - if invite == nil { - http.Error(w, "Invalid invite code", http.StatusBadRequest) - return - } - - if time.Now().After(invite.ExpiresAt) { - err := controller.DeleteInvite(app.DB, invite.Code) - if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %v\n", err) } - http.Error(w, "Invalid invite code", http.StatusBadRequest) - return - } - - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(credentials.Password), bcrypt.DefaultCost) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to generate password hash: %v\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - account := model.Account{ - Username: credentials.Username, - Password: string(hashedPassword), - Email: credentials.Email, - AvatarURL: "/img/default-avatar.png", - } - err = controller.CreateAccount(app.DB, &account) - if err != nil { - if strings.HasPrefix(err.Error(), "pq: duplicate key") { - http.Error(w, "An account with that username already exists", http.StatusBadRequest) - return - } - fmt.Fprintf(os.Stderr, "WARN: Failed to create account: %v\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - err = controller.DeleteInvite(app.DB, invite.Code) - if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %v\n", err) } - - token, err := controller.CreateToken(app.DB, account.ID, r.UserAgent()) - type LoginResponse struct { - Token string `json:"token"` - ExpiresAt time.Time `json:"expires_at"` - } - - err = json.NewEncoder(w).Encode(LoginResponse{ - Token: token.Token, - ExpiresAt: token.ExpiresAt, - }) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to return session token: %v\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } - }) -} - -func handleDeleteAccount(app *model.AppState) http.HandlerFunc { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.NotFound(w, r) - return - } - - type LoginRequest struct { - Username string `json:"username"` - Password string `json:"password"` - } - - credentials := LoginRequest{} - err := json.NewDecoder(r.Body).Decode(&credentials) - if err != nil { - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - - account, err := controller.GetAccount(app.DB, credentials.Username) - if err != nil { - if strings.Contains(err.Error(), "no rows") { - http.Error(w, "Invalid username or password", http.StatusBadRequest) - return - } - fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve account: %v\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(credentials.Password)) - if err != nil { - http.Error(w, "Invalid password", http.StatusBadRequest) - return - } - - // TODO: check TOTP - - err = controller.DeleteAccount(app.DB, account.Username) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to delete account: %v\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) - w.Write([]byte("Account deleted successfully\n")) - }) -} diff --git a/api/api.go b/api/api.go index b16d45d..9489126 100644 --- a/api/api.go +++ b/api/api.go @@ -13,20 +13,8 @@ import ( func Handler(app *model.AppState) http.Handler { mux := http.NewServeMux() - // ACCOUNT ENDPOINTS - - /* - // temporarily disabling these - // accounts should really be handled via the frontend rn, and juggling - // two different token bearer methods kinda sucks!! - // i'll look into generating API tokens on the frontend in the future // TODO: generate API keys on the frontend - mux.Handle("/v1/login", handleLogin()) - mux.Handle("/v1/register", handleAccountRegistration()) - mux.Handle("/v1/delete-account", handleDeleteAccount()) - */ - // ARTIST ENDPOINTS mux.Handle("/v1/artist/", http.StripPrefix("/v1/artist", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { From 0052c470f94fffc72357a83bbf9669f5e6726ed6 Mon Sep 17 00:00:00 2001 From: ari melody Date: Tue, 21 Jan 2025 17:13:06 +0000 Subject: [PATCH 03/99] (incomplete) change password feature --- admin/accounthttp.go | 71 +++++++++++++++++++++++++++-------- admin/http.go | 2 +- admin/views/edit-account.html | 14 +++++-- admin/views/layout.html | 5 ++- controller/config.go | 2 + main.go | 4 +- model/appstate.go | 1 + schema_migration/000-init.sql | 2 - 8 files changed, 76 insertions(+), 25 deletions(-) diff --git a/admin/accounthttp.go b/admin/accounthttp.go index b2cf18a..b354fd5 100644 --- a/admin/accounthttp.go +++ b/admin/accounthttp.go @@ -3,6 +3,7 @@ package admin import ( "fmt" "net/http" + "net/url" "os" "strings" "time" @@ -13,13 +14,22 @@ import ( "golang.org/x/crypto/bcrypt" ) -type TemplateData struct { +type loginRegisterResponse struct { Account *model.Account Message string Token string } func AccountHandler(app *model.AppState) http.Handler { + mux := http.NewServeMux() + + mux.Handle("/password", changePasswordHandler(app)) + mux.Handle("/", accountIndexHandler(app)) + + return mux +} + +func accountIndexHandler(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { account := r.Context().Value("account").(*model.Account) @@ -29,14 +39,18 @@ func AccountHandler(app *model.AppState) http.Handler { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } - type AccountResponse struct { + type accountResponse struct { Account *model.Account TOTPs []model.TOTP + Message string + Error string } - err = pages["account"].Execute(w, AccountResponse{ + err = pages["account"].Execute(w, accountResponse{ Account: account, TOTPs: totps, + Message: r.URL.Query().Get("message"), + Error: r.URL.Query().Get("error"), }) if err != nil { fmt.Printf("WARN: Failed to render admin account page: %v\n", err) @@ -59,7 +73,7 @@ func LoginHandler(app *model.AppState) http.Handler { return } - err = pages["login"].Execute(w, TemplateData{}) + err = pages["login"].Execute(w, loginRegisterResponse{}) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Error rendering admin login page: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -213,12 +227,12 @@ func createAccountHandler(app *model.AppState) http.Handler { return } - type CreateAccountResponse struct { + type CreateaccountResponse struct { Account *model.Account Message string } - render := func(data CreateAccountResponse) { + render := func(data CreateaccountResponse) { err := pages["create-account"].Execute(w, data) if err != nil { fmt.Printf("WARN: Error rendering create account page: %s\n", err) @@ -227,7 +241,7 @@ func createAccountHandler(app *model.AppState) http.Handler { } if r.Method == http.MethodGet { - render(CreateAccountResponse{}) + render(CreateaccountResponse{}) return } @@ -238,7 +252,7 @@ func createAccountHandler(app *model.AppState) http.Handler { err = r.ParseForm() if err != nil { - render(CreateAccountResponse{ + render(CreateaccountResponse{ Message: "Malformed data.", }) return @@ -261,7 +275,7 @@ func createAccountHandler(app *model.AppState) http.Handler { invite, err := controller.GetInvite(app.DB, credentials.Invite) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve invite: %v\n", err) - render(CreateAccountResponse{ + render(CreateaccountResponse{ Message: "Something went wrong. Please try again.", }) return @@ -271,7 +285,7 @@ func createAccountHandler(app *model.AppState) http.Handler { err := controller.DeleteInvite(app.DB, invite.Code) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %v\n", err) } } - render(CreateAccountResponse{ + render(CreateaccountResponse{ Message: "Invalid invite code.", }) return @@ -280,7 +294,7 @@ func createAccountHandler(app *model.AppState) http.Handler { hashedPassword, err := bcrypt.GenerateFromPassword([]byte(credentials.Password), bcrypt.DefaultCost) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to generate password hash: %v\n", err) - render(CreateAccountResponse{ + render(CreateaccountResponse{ Message: "Something went wrong. Please try again.", }) return @@ -295,13 +309,13 @@ func createAccountHandler(app *model.AppState) http.Handler { err = controller.CreateAccount(app.DB, &account) if err != nil { if strings.HasPrefix(err.Error(), "pq: duplicate key") { - render(CreateAccountResponse{ + render(CreateaccountResponse{ Message: "An account with that username already exists.", }) return } fmt.Fprintf(os.Stderr, "WARN: Failed to create account: %v\n", err) - render(CreateAccountResponse{ + render(CreateaccountResponse{ Message: "Something went wrong. Please try again.", }) return @@ -330,14 +344,41 @@ func createAccountHandler(app *model.AppState) http.Handler { cookie.Path = "/" http.SetCookie(w, &cookie) - err = pages["login"].Execute(w, TemplateData{ + err = pages["login"].Execute(w, loginRegisterResponse{ Account: &account, Token: token.Token, }) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to render login page: %v\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } }) } + +func changePasswordHandler(app *model.AppState) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.NotFound(w, r) + return + } + + account := r.Context().Value("account").(*model.Account) + + r.ParseForm() + + currentPassword := r.Form.Get("current-password") + if err := bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(currentPassword)); err != nil { + http.Redirect(w, r, "/admin/account?error=" + url.PathEscape("Incorrect password."), http.StatusFound) + return + } + + newPassword := r.Form.Get("new-password") + + http.Redirect( + w, r, "/admin/account?message=" + + url.PathEscape(fmt.Sprintf("Updating password to %s", newPassword)), + http.StatusFound, + ) + }) +} diff --git a/admin/http.go b/admin/http.go index b44cfa9..763537a 100644 --- a/admin/http.go +++ b/admin/http.go @@ -17,7 +17,7 @@ func Handler(app *model.AppState) http.Handler { mux.Handle("/login", LoginHandler(app)) mux.Handle("/register", createAccountHandler(app)) mux.Handle("/logout", RequireAccount(app, LogoutHandler(app))) - mux.Handle("/account", RequireAccount(app, AccountHandler(app))) + mux.Handle("/account/", RequireAccount(app, http.StripPrefix("/account", AccountHandler(app)))) mux.Handle("/static/", http.StripPrefix("/static", staticHandler())) mux.Handle("/release/", RequireAccount(app, http.StripPrefix("/release", serveRelease(app)))) mux.Handle("/artist/", RequireAccount(app, http.StripPrefix("/artist", serveArtist(app)))) diff --git a/admin/views/edit-account.html b/admin/views/edit-account.html index 4d89052..fd527b4 100644 --- a/admin/views/edit-account.html +++ b/admin/views/edit-account.html @@ -6,22 +6,28 @@ {{define "content"}}
+ {{if .Message}} +

{{.Message}}

+ {{end}} + {{if .Error}} +

{{.Error}}

+ {{end}}

Account Settings ({{.Account.Username}})

Change Password

-
+
- + - + - +
diff --git a/admin/views/layout.html b/admin/views/layout.html index 0a33b72..bacf014 100644 --- a/admin/views/layout.html +++ b/admin/views/layout.html @@ -26,7 +26,10 @@
{{if .Account}} + {{else}}
From 6bd0b5ec0e853d13af0f4b8ed7827d7944ee0efc Mon Sep 17 00:00:00 2001 From: ari melody Date: Wed, 27 Aug 2025 18:42:30 +0100 Subject: [PATCH 70/99] update pgp key --- public/keys/ari@arimelody.space_public.asc | 77 ++++++++++------------ 1 file changed, 35 insertions(+), 42 deletions(-) diff --git a/public/keys/ari@arimelody.space_public.asc b/public/keys/ari@arimelody.space_public.asc index 4323eba..9e6fea2 100644 --- a/public/keys/ari@arimelody.space_public.asc +++ b/public/keys/ari@arimelody.space_public.asc @@ -2,19 +2,19 @@ mDMEZNW03RYJKwYBBAHaRw8BAQdAuMUNVjXT7m/YisePPnSYY6lc1Xmm3oS79ZEO JriRCZy0IGFyaSBtZWxvZHkgPGFyaUBhcmltZWxvZHkuc3BhY2U+iQJJBBMWCgHx -AhsDBQkIgw/zBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAhkBFiEE7o3rjWKH -LnoJHirfz5mCnJJngYgFAmino5w1FIAAAAAAEAAccHJvb2ZAYXJpYWRuZS5pZGRu -czphcmltZWxvZHkuc3BhY2U/dHlwZT1UWFQ6FIAAAAAAEAAhcHJvb2ZAYXJpYWRu -ZS5pZGh0dHBzOi8vZmVkaS5hcmltZWxvZHkuc3BhY2UvQGFyaUQUgAAAAAAQACtw -cm9vZkBhcmlhZG5lLmlkaHR0cHM6Ly9mb3JnZS5ibGlzcy50b3duL2FyaS9rZXlv -eGlkZS1wcm9vZkkUgAAAAAAQADBwcm9vZkBhcmlhZG5lLmlkaHR0cHM6Ly9mb3Jn -ZS5hcmltZWxvZHkuc3BhY2UvYXJpL2tleW94aWRlLXByb29mRhSAAAAAABAALXBy -b29mQGFyaWFkbmUuaWRodHRwczovL2NvZGViZXJnLm9yZy9hcmltZWxvZHkva2V5 -b3hpZGUtcHJvb2ZlFIAAAAAAEABMcHJvb2ZAYXJpYWRuZS5pZGh0dHBzOi8vYnNr -eS5hcHAvcHJvZmlsZS9kaWQ6cGxjOnljdDZjdmdmaXBuZ2l6cnk1dW16a3hyMy9w -b3N0LzNsaWlucW90cXRjMjIACgkQz5mCnJJngYjDpQEAgFn3bXcxw3xF0dwrSURh -qpciMY31bkQy9eDMSKcbloIA/1hX1MnUKETdiAtrrK08z4udIXaJr52E5D7IAZk1 -pZwBtB1hcmkgbWVsb2R5IDxhcmlAYXJpbWVsb2R5Lm1lPoiTBBMWCgA7FiEE7o3r +AhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAhkBNRSAAAAAABAAHHByb29m +QGFyaWFkbmUuaWRkbnM6YXJpbWVsb2R5LnNwYWNlP3R5cGU9VFhUOhSAAAAAABAA +IXByb29mQGFyaWFkbmUuaWRodHRwczovL2ZlZGkuYXJpbWVsb2R5LnNwYWNlL0Bh +cmlEFIAAAAAAEAArcHJvb2ZAYXJpYWRuZS5pZGh0dHBzOi8vZm9yZ2UuYmxpc3Mu +dG93bi9hcmkva2V5b3hpZGUtcHJvb2ZJFIAAAAAAEAAwcHJvb2ZAYXJpYWRuZS5p +ZGh0dHBzOi8vZm9yZ2UuYXJpbWVsb2R5LnNwYWNlL2FyaS9rZXlveGlkZS1wcm9v +ZkYUgAAAAAAQAC1wcm9vZkBhcmlhZG5lLmlkaHR0cHM6Ly9jb2RlYmVyZy5vcmcv +YXJpbWVsb2R5L2tleW94aWRlLXByb29mZRSAAAAAABAATHByb29mQGFyaWFkbmUu +aWRodHRwczovL2Jza3kuYXBwL3Byb2ZpbGUvZGlkOnBsYzp5Y3Q2Y3ZnZmlwbmdp +enJ5NXVtemt4cjMvcG9zdC8zbGlpbnFvdHF0YzIyFiEE7o3rjWKHLnoJHirfz5mC +nJJngYgFAmivQJsFCQ0/jT4ACgkQz5mCnJJngYgdtQD+K8AMkLvR1ZKxl0tw8/FO +vwS9HknEW13GajSAY/W1/NoA/17mnVnlTFhepKo1ETnxe2BpdOaKR85K0n2qffzC +8SAAtB1hcmkgbWVsb2R5IDxhcmlAYXJpbWVsb2R5Lm1lPoiTBBMWCgA7FiEE7o3r jWKHLnoJHirfz5mCnJJngYgFAmTVtN0CGwMFCwkIBwICIgIGFQoJCAsCBBYCAwEC HgcCF4AACgkQz5mCnJJngYhgmwD+ME3CwlOWZX0kyaxbRoUClgOg8bVDuwnoJFbv EoJGNyoBANf2Ko2db0mqjwzeNd+75oZIX3bqGDozdIqbTY9/btsBiQIKBBMWCgGy @@ -29,38 +29,31 @@ cnk1dW16a3hyMy9wb3N0LzNsaWlucW90cXRjMjJEFIAAAAAAEAArcHJvb2ZAYXJp YWRuZS5pZGh0dHBzOi8vZ2l0LmFyaW1lbG9keS5tZS9hcmkva2V5b3hpZGVfcHJv b2YACgkQz5mCnJJngYh3+QD+Pbo3bM4oWtUicGUGEp4jiFoBqSNlyl9rFPY0ODDS DxEBANaXz/No/Hn3mEwNdrFigj/YPm7TH/4UBbHAxN6hDggPiQJGBBMWCgHuAhsD -BQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheABQkIgw/zFiEE7o3rjWKHLnoJHirf -z5mCnJJngYgFAmino5VJFIAAAAAAEAAwcHJvb2ZAYXJpYWRuZS5pZGh0dHBzOi8v -Zm9yZ2UuYXJpbWVsb2R5LnNwYWNlL2FyaS9rZXlveGlkZS1wcm9vZkYUgAAAAAAQ -AC1wcm9vZkBhcmlhZG5lLmlkaHR0cHM6Ly9jb2RlYmVyZy5vcmcvYXJpbWVsb2R5 -L2tleW94aWRlLXByb29mOhSAAAAAABAAIXByb29mQGFyaWFkbmUuaWRodHRwczov -L2ZlZGkuYXJpbWVsb2R5LnNwYWNlL0BhcmllFIAAAAAAEABMcHJvb2ZAYXJpYWRu -ZS5pZGh0dHBzOi8vYnNreS5hcHAvcHJvZmlsZS9kaWQ6cGxjOnljdDZjdmdmaXBu -Z2l6cnk1dW16a3hyMy9wb3N0LzNsaWlucW90cXRjMjI1FIAAAAAAEAAccHJvb2ZA -YXJpYWRuZS5pZGRuczphcmltZWxvZHkuc3BhY2U/dHlwZT1UWFREFIAAAAAAEAAr -cHJvb2ZAYXJpYWRuZS5pZGh0dHBzOi8vZm9yZ2UuYmxpc3MudG93bi9hcmkva2V5 -b3hpZGUtcHJvb2YACgkQz5mCnJJngYgKNQD/UA2THttICUvz2p5cbPlJIm/QStRE -6crttsTeFSsyocgBAPDXpkdssPNNnxxVvCNATTTxiS08Cy+xxQVrjWztjlUCuDME +BQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheASRSAAAAAABAAMHByb29mQGFyaWFk +bmUuaWRodHRwczovL2ZvcmdlLmFyaW1lbG9keS5zcGFjZS9hcmkva2V5b3hpZGUt +cHJvb2ZGFIAAAAAAEAAtcHJvb2ZAYXJpYWRuZS5pZGh0dHBzOi8vY29kZWJlcmcu +b3JnL2FyaW1lbG9keS9rZXlveGlkZS1wcm9vZjoUgAAAAAAQACFwcm9vZkBhcmlh +ZG5lLmlkaHR0cHM6Ly9mZWRpLmFyaW1lbG9keS5zcGFjZS9AYXJpZRSAAAAAABAA +THByb29mQGFyaWFkbmUuaWRodHRwczovL2Jza3kuYXBwL3Byb2ZpbGUvZGlkOnBs +Yzp5Y3Q2Y3ZnZmlwbmdpenJ5NXVtemt4cjMvcG9zdC8zbGlpbnFvdHF0YzIyNRSA +AAAAABAAHHByb29mQGFyaWFkbmUuaWRkbnM6YXJpbWVsb2R5LnNwYWNlP3R5cGU9 +VFhURBSAAAAAABAAK3Byb29mQGFyaWFkbmUuaWRodHRwczovL2ZvcmdlLmJsaXNz +LnRvd24vYXJpL2tleW94aWRlLXByb29mFiEE7o3rjWKHLnoJHirfz5mCnJJngYgF +AmivQJsFCQ0/jT4ACgkQz5mCnJJngYhk/wEAuQMYpUgyLqcYvOh1A+7f/t+DUXjz +YjQtLYw37oAESREA/074iJNi9GHGIjxYfp5lBkZxqGew1GAFIKx7Yzp64WQFuDME Z7UmihYJKwYBBAHaRw8BAQdAlt+HmIscSlmd0yB6SpjOpOtSgAnxkgt3hYfR1zD2 -05qI9QQYFgoAJgIbAhYhBO6N641ihy56CR4q38+ZgpySZ4GIBQJntSpWBQkFo55M +05qI9QQYFgoAJgIbAhYhBO6N641ihy56CR4q38+ZgpySZ4GIBQJor0AjBQkKYBsZ AIF2IAQZFgoAHRYhBCW7l1aYFH8/dnGeDGC18DhuPdt+BQJntSaKAAoJEGC18Dhu Pdt+7H4A/jLnJ2uOcYExfsa4HaeHnlJF2xxKJexnrqe2eNfJBxtaAQCkO67sWpfN -dyeW65nE0UNPvjRGfOzrS1N6mUOoYZZwBAkQz5mCnJJngYhzSQD+JLA3D6vcULvm -ibCs+kkXXcHl+r3cXB4XH6hJoRLqrOQBAO+AIrvuoopu8KO7SDzNKwoRme/rFHi6 -+0Z1WS8ukF0LuDMEZ7UmmRYJKwYBBAHaRw8BAQdAadTiVcUtyGEpiQI+yE/6O+5G +dyeW65nE0UNPvjRGfOzrS1N6mUOoYZZwBAkQz5mCnJJngYgc2gD/cFhjrwPdex9g +ZYk7jH29wQ9RpR9dEhf0C20nFZJLawgBAOBbzw4/O7OslSoIjhGs4pw9hJIBK7ds +PI6g3CeX0DUFuDMEZ7UmmRYJKwYBBAHaRw8BAQdAadTiVcUtyGEpiQI+yE/6O+5G w2h51oM4ranh2RALwm6IfgQYFgoAJgIbIBYhBO6N641ihy56CR4q38+ZgpySZ4GI -BQJntSpeBQkFo55FAAoJEM+ZgpySZ4GIv0oBAIlkgGG5KyhhrrUjRdlwrMZlsHSH -2kPp6DVVGzA0iAe1AQDC70eCzWuz1zJK8ps7taArpCHoR+u8aAY5cI6bfBWyDbg4 +BQJor0AjBQkKYBsKAAoJEM+ZgpySZ4GIoMAA/jB/exnjGvsMKuNW09bI29bsKHNW +SQLjEnuuByN6Spq6AP9yPUumsSEHr0W71iefMuNFJZnF+8qSk+uywQ/5ET+PBbg4 BGe1KjkSCisGAQQBl1UBBQEBB0D7+TnzpbU4fGd3MMk2lt37CqPKOvQPkhfF8OzT -Rp28HQMBCAeIfgQYFgoAJhYhBO6N641ihy56CR4q38+ZgpySZ4GIBQJntSo5AhsM -BQkFo5qAAAoJEM+ZgpySZ4GIQXEBAKDOJC3aXe7uOdlWKPjOin4rYu9NUW2RsbrW -1/putHMGAP9fpzpfkFHJALnUlXUjVEVEF14wAhfNwsWDa/dZQxxxC7g4BGTVtN0S -CisGAQQBl1UBBQEBB0CcDZ2s/NAGhc13AisWei+4XQKNf7z7xBK6AIXhrlkRcQMB -CAeIeAQoFgoAIBYhBO6N641ihy56CR4q38+ZgpySZ4GIBQJntT6fAh0DAAoJEM+Z -gpySZ4GIgX8A/1d8CZFSRB0TRU8h6ijTS1+O2bKJ0uwydfQHL5b3fA4OAQDOU6eG -Ml82IKGhbFoJl7wm5X4+l5+lNqwZymNoZjVhBIh4BBgWCgAgFiEE7o3rjWKHLnoJ -Hirfz5mCnJJngYgFAmTVtN0CGwwACgkQz5mCnJJngYgv8QEA9YbuFnLLUeNJZFMT -KoWeOMJos6wwPnhgnYexntxsu/cBAMd/ORp2KDaZTEwOAUxrO6K1eFkn0pKAcdPq -cdVDnsIL -=Mzcq +Rp28HQMBCAeIfgQYFgoAJgIbDBYhBO6N641ihy56CR4q38+ZgpySZ4GIBQJor0Ak +BQkKYBdqAAoJEM+ZgpySZ4GIlIoA/0fv2UQyhixu7Vkq7IeQ+NxUuEVCIGmrAu6k +ScT13ikjAQCPpIubU848yXcDUvxgcGAS7yNADU1dAWAZOi34WxajAQ== +=caf3 -----END PGP PUBLIC KEY BLOCK----- From 89bb46c49e434bcb97d0d799252d30bec71bb034 Mon Sep 17 00:00:00 2001 From: ari melody Date: Fri, 29 Aug 2025 16:29:28 +0100 Subject: [PATCH 71/99] day 1 patch --- main.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/main.go b/main.go index 9133958..f3c9f66 100644 --- a/main.go +++ b/main.go @@ -33,6 +33,7 @@ import ( const DB_VERSION = 1 const DEFAULT_PORT int64 = 8080 +const HRT_DATE int64 = 1756478697 func main() { fmt.Printf("made with <3 by ari melody\n\n") @@ -605,6 +606,10 @@ func DefaultHeaders(next http.Handler) http.Handler { "X-Powered-By", PoweredByStrings[rand.Intn(len(PoweredByStrings))], ) + w.Header().Add( + "X-Days-Since-HRT", + fmt.Sprint(math.Round(time.Since(time.Unix(HRT_DATE, 0)).Hours() / 24)), + ) next.ServeHTTP(w, r) }) } From fd4335ced470306fe7263e45ca5505a857680042 Mon Sep 17 00:00:00 2001 From: ari melody Date: Sun, 7 Sep 2025 16:18:20 +0100 Subject: [PATCH 72/99] update security checks --- main.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index f3c9f66..eaed7c6 100644 --- a/main.go +++ b/main.go @@ -574,11 +574,10 @@ func CheckRequest(app *model.AppState, next http.Handler) http.Handler { return } - // same with .php and awkward double-slash requests. - // obviously these don't affect me, but these tend to be lazy intrusion - // attempts. if that's what you're about, i don't want you on my site. - if strings.HasPrefix(r.URL.Path, "//") || - strings.HasSuffix(r.URL.Path, ".php") || + // obviously .php requests these don't affect me, but these tend to be + // lazy wordpress intrusion attempts. if that's what you're about, i + // don't want you on my site. + if strings.HasSuffix(r.URL.Path, ".php") || strings.HasSuffix(r.URL.Path, ".php7") { http.NotFound(w, r) fmt.Fprintf( From b150fa491cf668174e6f41bc579b8e08a18cc786 Mon Sep 17 00:00:00 2001 From: ari melody Date: Wed, 10 Sep 2025 13:50:46 +0100 Subject: [PATCH 73/99] update web buttons --- public/img/buttons/wangleline.png | Bin 0 -> 6479 bytes views/index.html | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 public/img/buttons/wangleline.png diff --git a/public/img/buttons/wangleline.png b/public/img/buttons/wangleline.png new file mode 100644 index 0000000000000000000000000000000000000000..b26ea3f59c6185ff6d8a1008316b4b03706c71db GIT binary patch literal 6479 zcmV-V8L;MwP)7huTF@#| z1zhUSTA|gtwOZr#c7F_FwfCvQvL6J=$7=%DF+4s!8&%NjO#~{^K3#~oR zljlB}d+xpG{k-q_p7$(L``T<_w*Uf=06++kr1bUpFMx#fPxtxi{_)?Xl!QD~V&{q& z)9;LM$AZONyZA~%i4^^Z4da=mk8;QF?_t@G7BPF=*|hd`V(0>4fNE1H8(a!P;rd6G zv8MKYp1)h=+@GjmlUcIB%Nv_=S@!Z%R1PX9;P+#5yXjX^!OG`e;J({#;+1P3WI$0r z{E-ksfDi&hH>eytn2$Dp%7xF}&nvenTzIt^+9kvZo4$UP5C{+;gn}S%hMJdkg5pWazpp0*sKtWrFAZki5#qA5ujKAWZl|%a9anJ)7Mq=x zdJpIZC9ZrlS-~usFeHP#eomYNOWD8kF!SzzoVa5*+h57xv^gsH1vme=n-@PSW6Q=j z7&dkQM{5q@uB^o5a$&VtNyL*R;z=APJ0?j*LqQ-gQi1`epN6Nth4Y^NBR`)S;*v$# zh{gnl^d07j6WG2HJpWSnyKsoFy~Bv0M&c4S^Ql7q?ji#e$lN{8OO`s0Wh)<~y|oj` zZb!A4iR%fP8=G(g#o2iTLt#`^q3rbjU?h3y*_U~B+lySaD8w!IWkQC4BSEgZJi&(j zV_3ibS={B>Y+1Jr)oLSX4x{u%NfyDoZ>$EuAu@>c#xY46CQT!^Fq18x@8Ob1AL1ud zJ9uhk4s;tBaX|UrCZDtzk`?uT^)fjFq6Z@E*l*&Y2X00n2*(m+<`sahgCXc@X~%_y z94&*~d>3+-i8Wh3;?_Sui$}Ec+lBCh;qdCxAU#1VM{4Z6yVu3S3y1LO`d4|q-h)X~ zG21MdEM`oaie@p&I3z{9pWr9$; zS>GC=|EnTNNP$h+;(JA~07V6cz?Un$-0|#mo_+f-__|s!<>a7cW>Q*IM8lyv`VSq% zqQPhLr#pYo=IxtU`Pql8uGvOju7fLQjlx-whu@b?R(?KCXAXG<84MXi!Y8at9lx)n8622pP+#rKJn}ze`p?OjvhvtJ0I1S31$meW$^jaAMks@nF$s< zNnZ#(nZnR@^pt@xzFi0d$?{(SPJxmqK^`34(#PxXLU*(uC8blzDk$ZxKd<8a`STew zaXj8gn7yBT#)oge#t65Y$8NrfR3Wsc+88vf5)=!<7b2ESp-U2pXdEdN!In&M<1Zh_ zQqYfS6Niy$iZJrbDzZ!5#FG$9>YzwSk_-rhArOWELZnCXZ|RV-Bw?|bFlS|gWT&Mu z%IBXSrFQ?vcxvC`oU?<>x!MJO3B5yr_Wc3t{}JHh0i#sPBpQ?a;_d*4J!PDA_QlMe zJ(*Esi@|AP`zx#YbjM*ThYTXv+eI=NX42%*3>r5YJ<>}s62_!!#G^@&Krx%Z5QKZY zn3GAe9U0tw$DjD)h8=AD`=>Y^8SHrbO|)<)8Co2-JA?iMOUNrOKr&f~>jE7KNs*B? z1xZ$rWa%5-U6v$_<4zDF?GR0s$tZAv*~NzU4)dEuOX)ZG0B=3xf=tkx_}-9p%>Myk z9h3@*quWzVo*SU5>U>^$^_LV@A@9MsII`>u-OYcfP3S-B;gX*4!Tvh(=H}GYwxh;BRY31iB&AL#(fr zRJaGL8Qg`rWank0S*=K_M$!-%l0;5k7D|R4bb*md(&q~y$uhF49M5Ib`ES;ca#Dxd z$s4+u0mb#~Sf7Ke8yEq;moBF;Aq6+Ng!N>A1t|Ft?2;&**vA=zrm^|(AE2j~-j*I@ z^%Qd+A&^xSSyPFJA|(3!B)Zhw`6bad6A{9%Z2SVh?5`jdL#*Gs(iAq9gq&z$S@c_bqMo@j^oL<*K_OdtjKnO9{I1> z0_oYV6#U2~tfqfoQOSZ>FZ7?{Wk}%|Htv6rs5e9`kva)j0kVv2QjrXSRX5O*369p( zv1ZF&_SE+fHy1E!Vl|h|y_l>l2fMdzq2t&gQi&wGt|JM7)!`(+{4}x)3vf8I$aCkT zX&N1^Z6GVyGVG|Dil&*6Wd+>;Mb$_~L&!!7Uq?6lckkwl-MdLdLgo)J@P*;@Ru#77&mt4D_y{lHUarp}B>S_p)B1;{H zq?nOp1;uP5;5$T=F3uSOeUN+gO;8cKfikgFTS&b3(q-|V!4FnE1MaA zm4+;TW9ayg%Si#t5+K30ok>7r8P55q(U+-c;(uG9Ie~O zgDajxGO4sTw~&a((M;)~Ue|SO8Fun>^UwiTPBzVr&8+);I|abVAKr_zax}$N)%2g> zWXltOWz|dbxo_@8{9)GlJpbg2Ja^Y^oYud9sUr$_e%Dcc^{#~h7f5jMR7ib0jgvkm zsX!QN4kpOLMNvsEgks-XSJhNx3Hn-l*i-AFX5Udlx?tCl2CO*)d1vce->5(Dr&lxY zr&j}@S!DF*mg6W<=-vG?ac>{tV3@MO1JO~)$+u%s41^+4T2V%(!$Be*$1n^WPA6HJ znH<`GkZWdM%+Qe|D9+Ahpgos1vx{SKFTT2sbnktUjxB>ZykQif<|7oAmS8Ap1V3GL z6`3Z96)T^jy{j7?!37I^e0ewrr9_~&i2n+*6#B>|EdNG>6&sL*Nz?pv2g}*E??GIy z3<80FRsUpCr>Cu(z78)j-N5Y3;LJ%Q(5xD>$KA-qx8FwPfC>&BI6zoWQCw7rNk~YB z#KfsnXgbz_%LvgE4k2Y_u>Q4o=;`QU+|+6YO`OP(VWW^0iAXepW-^gVCh_+4a`e!b z9Nc$+x0kOZ5)N_qpB|;yl1XRHVGcJp;@1o&PM?CisF<#%V{Ce76wriT`Mk$dF8@0N4SMZWwj8mn_Fn zHtkx9s;WdH-;81*1o2n`-4Hl)9Ar3b7>N|2Kp4m1emt@G32u63B~L!^2l@@Hp!@Sf zeDK^#CXYA+MHhHOA>yedCHVzh`QROxM-Rd4?c?LsA5u_UMA@JLbo=_~>*yjD3=;{5 zNhIRL<1r%9D9M!0*h$q?PpJM*TiUxiFli=oT-kU7ei~P-rg6^!x_bNY`TPtXF`U(V zc5$q=hK-&NF}&RrWG`gmz@xmgBOBTUsXmZSH7k`&NTJP6VbQ*Ouu^iM^9WR3=w)>O z(X82WKk;ascs%*9b(QpRtLxt!7VH^TBvq!ebSBr{cqdDqydNSd{{HIU@O!B;u~|ToSvcup zwrqQwog1DZZ~AzG^}Xa*Ud_DmE&S!f9Pk*V0;dL9$l58WiW$0lp%-xH+EHv~^vJhs z@IQn7s)PbzvWJxM;F24;;@+i9JaZ%y=Uu?q1wX>1S_mYgTyfDf{IL*2EH?T&y9oLM zxbkusHKm&Khfg58Gsvyi{)|W2S{#N#x#b#dtEXRJSbh&sCICY@0!Koa=Hj~7x| zbsB!3m!TtuGx?lp6qofQ6bu3o^at?Ckm1NASFy47oz=Xw?QMQNb2=Gj6OF4rrtHFM zYCruW{U=;S$wDvpyy!wcVqhfUBuNwi>WMmP1(Ne3Ncj-H<;PtBxPD382?GbMq|BWG zfe>nD5wb3k47MZm#L2%R$%Gp^_{ndtRFQ^nE+J@mYx{EIpGBXJ^uFrjTV zR7{-6+mGJHoL?ybM8kRmZeFoH;=&{HWAsU!)>K$tb=+;b}h z#YL=re+|?qd4^b1oFlHr~cn}DXQeh58D zGSrSatC(O|C2Mdd>t0yOl&kM#)UO}nwm;rWPis4oK!8{vfYoFo=Jk?FrLZ_0Q0S(& zr-wQJH=mZyPP)3fuqqld5}{C-UcVR3Y{r%2qM*2lNF>7Pqs|}}jbXA{D08}yfS7LZ z#i1i~w6(E!#~%K+>}4kX;4F&lNfyl>!R0qR111Z{wtYl0nWCu3MQdw>9BkN^lau@U_e?;yd;=^dP$y;>xXH=Z;SpcV;zZxekg)52QO; zh#gq{$_j2-I0H-BR1}l_#G}#91Ehi;6l)Q-;xTl$9>Y~J2-ThkKq}nLaIBtevxV{@!%)ba zwd(*FQ(eX9JN)F8R&e0J0S08mQTtCMk&?LLvWt1-_Yb52OL;`Py&P@s1wgaAShZ#y zk3RBH+N1V7BK2+j{IR>Z=9+7$udnBtg$r2pv+HnXI|117@g{a{`G`O`4fw#mTDEN5 ziRQ>6o=9@tHP)&4kz_GeI9$s`4%kKFdYoC7x&$bUSS`RX~ zC=-j-ipS$&@$FZTU09NKketDD%U7~w$&z&YT!qvek+|#0&scEd6*PE`v0(lWng5e{ zSZy|bbMp`Td-pGhV>S*>bZ$u+8Y}g#xpkxwCY9xxQ0622^FmL|;aW22*76t=r*SyD* zcmIMjPCxHN``+95EZ6_`2>`mg>bR(SA_v;ivX@`tM)HzgWjlpbP2IB{I=jHr+Q1d} zcTrSa!At8Fv0%KNw_baVspE$LAj@GUO`SqdPZvAhx`n#(>-qCbZy|(1Ur#qCiw%IF zuaEx4dH4&8(*E*waAv6+Ejuj{jg20nO|8gz;{kB!ef<5U`*`f>_sGf3BODH6Z(hrd zmsF6KpHCzb=C-qA^bJVsyu)FqVC--N647`9kH^EIeS4U8*(CskI$N1C)=hh7lvK(~ zEZ#*>Pa+C`2tdNuNu=!{`6cN9sNH{%%7QbQII;qOhK713jQSj_!wtZ|0V63d8_8s| zL{n2e9c^_KOd>Z;<0CzVz=9A^fWMY<}A9L6%4ec|3B6gXXvZ}0KBzgIV+xi zkdzKKvx3E>aQ&ItIIq7JfZV_i4tZjLgb*6Ctl@A=407w|MRuJpWVzDqHFei8?}DM^=H=7mY2ccfDz=P5JkEXqh)5S> zhb3w9G|;cSg1t<*FJ*}C0NLiowd401-5!HmgPlyU`H zzLF4uZ1_NLOKR9gETZknj9Q?C;sRjq}Df;`7&Ys5U()R?oQ?o4bHD zt6v3R`o#-qRtf;<>gXUReiVm)4|(}608)KT46=Lp!@W!B?&_kvY9#snD`{zI1fZ|I zh2q2>Mh;I$lu++R-Z~r=z*A3*D^YkLpO)w1fq^TZx5}y!^~kdb+#F zthx}#5ALEnl;p98?`HI%AoFMCv*@xx+;HJ2&K^3Rq<17MUMu60>m(k&H$+Qs7^OIY zQXr60+g&F%x$k;5N=;iOTIvXYdmsaVU_@}mL&fRn7+B98S6b4M`&a}u|6-0E+fQ{> z14rw0_Ik=`sc&G`m|{F}2P1L=JbuFnP!#T8`6Zt;nprg6%z0x9XjaFwY;7I8cJHIq z*28a~{(zFoVZ8mw4g4VY2+wY4VRMs}8T}K?9^)nuPVvZ^V;FhE$2EhPXO^2^ul$mMqi4|9*F&{z#BNbfoOKWaI(lOSqbUXy*%5|;9ygE_iR4$4{;!{j5RjE+0!bnmN?@}n zC+>%$DKf0;al0k-l#XT?Jn&Q thermia web button + + WangleLine button + elke web button From e5dcc4b8847b08f0fb6efb8587660a16da5955cd Mon Sep 17 00:00:00 2001 From: ari melody Date: Tue, 30 Sep 2025 19:03:35 +0100 Subject: [PATCH 74/99] embed html template and static files --- Makefile | 4 +- admin/accounthttp.go | 29 ++-- admin/artisthttp.go | 13 +- admin/http.go | 57 ++++---- admin/logshttp.go | 15 +- admin/releasehttp.go | 29 ++-- admin/templates.go | 125 ----------------- .../html}/components/credits/addcredit.html | 0 .../html}/components/credits/editcredits.html | 0 .../html}/components/credits/newcredit.html | 0 .../html}/components/links/editlinks.html | 0 .../components/release/release-list-item.html | 0 .../html}/components/tracks/addtrack.html | 0 .../html}/components/tracks/edittracks.html | 0 .../html}/components/tracks/newtrack.html | 0 .../html}/edit-account.html | 0 .../html}/edit-artist.html | 0 .../html}/edit-release.html | 0 .../{views => templates/html}/edit-track.html | 0 admin/{views => templates/html}/index.html | 0 admin/{views => templates/html}/layout.html | 0 .../{views => templates/html}/login-totp.html | 0 admin/{views => templates/html}/login.html | 0 admin/{views => templates/html}/logout.html | 0 admin/{views => templates/html}/logs.html | 0 .../templates/html}/prideflag.html | 0 admin/{views => templates/html}/register.html | 0 .../html}/totp-confirm.html | 0 .../{views => templates/html}/totp-setup.html | 0 admin/templates/templates.go | 128 ++++++++++++++++++ admin/trackhttp.go | 13 +- main.go | 55 ++++---- model/appstate.go | 7 +- {views => templates/html}/404.html | 0 {views => templates/html}/footer.html | 0 {views => templates/html}/header.html | 0 {views => templates/html}/index.html | 0 {views => templates/html}/layout.html | 0 {views => templates/html}/music-gateway.html | 0 {views => templates/html}/music.html | 0 templates/html/prideflag.html | 21 +++ templates/templates.go | 52 ++++--- view/{static.go => files.go} | 21 ++- view/index.go | 2 +- 44 files changed, 316 insertions(+), 255 deletions(-) delete mode 100644 admin/templates.go rename admin/{ => templates/html}/components/credits/addcredit.html (100%) rename admin/{ => templates/html}/components/credits/editcredits.html (100%) rename admin/{ => templates/html}/components/credits/newcredit.html (100%) rename admin/{ => templates/html}/components/links/editlinks.html (100%) rename admin/{ => templates/html}/components/release/release-list-item.html (100%) rename admin/{ => templates/html}/components/tracks/addtrack.html (100%) rename admin/{ => templates/html}/components/tracks/edittracks.html (100%) rename admin/{ => templates/html}/components/tracks/newtrack.html (100%) rename admin/{views => templates/html}/edit-account.html (100%) rename admin/{views => templates/html}/edit-artist.html (100%) rename admin/{views => templates/html}/edit-release.html (100%) rename admin/{views => templates/html}/edit-track.html (100%) rename admin/{views => templates/html}/index.html (100%) rename admin/{views => templates/html}/layout.html (100%) rename admin/{views => templates/html}/login-totp.html (100%) rename admin/{views => templates/html}/login.html (100%) rename admin/{views => templates/html}/logout.html (100%) rename admin/{views => templates/html}/logs.html (100%) rename {views => admin/templates/html}/prideflag.html (100%) rename admin/{views => templates/html}/register.html (100%) rename admin/{views => templates/html}/totp-confirm.html (100%) rename admin/{views => templates/html}/totp-setup.html (100%) create mode 100644 admin/templates/templates.go rename {views => templates/html}/404.html (100%) rename {views => templates/html}/footer.html (100%) rename {views => templates/html}/header.html (100%) rename {views => templates/html}/index.html (100%) rename {views => templates/html}/layout.html (100%) rename {views => templates/html}/music-gateway.html (100%) rename {views => templates/html}/music.html (100%) create mode 100644 templates/html/prideflag.html rename view/{static.go => files.go} (54%) diff --git a/Makefile b/Makefile index 11e565a..f96321c 100644 --- a/Makefile +++ b/Makefile @@ -2,10 +2,10 @@ EXEC = arimelody-web .PHONY: $(EXEC) -$(EXEC): +build: GOOS=linux GOARCH=amd64 go build -o $(EXEC) -bundle: $(EXEC) +bundle: build tar czf $(EXEC).tar.gz $(EXEC) admin/components/ admin/views/ admin/static/ views/ public/ schema-migration/ clean: diff --git a/admin/accounthttp.go b/admin/accounthttp.go index 945a507..b2c3b0d 100644 --- a/admin/accounthttp.go +++ b/admin/accounthttp.go @@ -1,17 +1,18 @@ package admin import ( - "database/sql" - "fmt" - "net/http" - "net/url" - "os" + "database/sql" + "fmt" + "net/http" + "net/url" + "os" - "arimelody-web/controller" - "arimelody-web/log" - "arimelody-web/model" + "arimelody-web/admin/templates" + "arimelody-web/controller" + "arimelody-web/log" + "arimelody-web/model" - "golang.org/x/crypto/bcrypt" + "golang.org/x/crypto/bcrypt" ) func accountHandler(app *model.AppState) http.Handler { @@ -64,7 +65,7 @@ func accountIndexHandler(app *model.AppState) http.Handler { session.Message = sessionMessage session.Error = sessionError - err = accountTemplate.Execute(w, accountResponse{ + err = templates.AccountTemplate.Execute(w, accountResponse{ Session: session, TOTPs: totps, }) @@ -184,7 +185,7 @@ func totpSetupHandler(app *model.AppState) http.Handler { session := r.Context().Value("session").(*model.Session) - err := totpSetupTemplate.Execute(w, totpSetupData{ Session: session }) + err := templates.TOTPSetupTemplate.Execute(w, totpSetupData{ Session: session }) if err != nil { fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -221,7 +222,7 @@ func totpSetupHandler(app *model.AppState) http.Handler { if err != nil { fmt.Printf("WARN: Failed to create TOTP method: %s\n", err) controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") - err := totpSetupTemplate.Execute(w, totpConfirmData{ Session: session }) + err := templates.TOTPSetupTemplate.Execute(w, totpConfirmData{ Session: session }) if err != nil { fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -235,7 +236,7 @@ func totpSetupHandler(app *model.AppState) http.Handler { fmt.Fprintf(os.Stderr, "WARN: Failed to generate TOTP QR code: %v\n", err) } - err = totpConfirmTemplate.Execute(w, totpConfirmData{ + err = templates.TOTPConfirmTemplate.Execute(w, totpConfirmData{ Session: session, TOTP: &totp, NameEscaped: url.PathEscape(totp.Name), @@ -296,7 +297,7 @@ func totpConfirmHandler(app *model.AppState) http.Handler { confirmCodeOffset := controller.GenerateTOTP(totp.Secret, 1) if code != confirmCodeOffset { session.Error = sql.NullString{ Valid: true, String: "Incorrect TOTP code. Please try again." } - err = totpConfirmTemplate.Execute(w, totpConfirmData{ + err = templates.TOTPConfirmTemplate.Execute(w, totpConfirmData{ Session: session, TOTP: totp, NameEscaped: url.PathEscape(totp.Name), diff --git a/admin/artisthttp.go b/admin/artisthttp.go index 9fa6bb2..67ea7d2 100644 --- a/admin/artisthttp.go +++ b/admin/artisthttp.go @@ -1,12 +1,13 @@ package admin import ( - "fmt" - "net/http" - "strings" + "fmt" + "net/http" + "strings" - "arimelody-web/model" - "arimelody-web/controller" + "arimelody-web/admin/templates" + "arimelody-web/controller" + "arimelody-web/model" ) func serveArtist(app *model.AppState) http.Handler { @@ -39,7 +40,7 @@ func serveArtist(app *model.AppState) http.Handler { session := r.Context().Value("session").(*model.Session) - err = artistTemplate.Execute(w, ArtistResponse{ + err = templates.EditArtistTemplate.Execute(w, ArtistResponse{ Session: session, Artist: artist, Credits: credits, diff --git a/admin/http.go b/admin/http.go index 245a152..05e181d 100644 --- a/admin/http.go +++ b/admin/http.go @@ -1,20 +1,24 @@ package admin import ( - "context" - "database/sql" - "fmt" - "net/http" - "os" - "path/filepath" - "strings" - "time" + "context" + "database/sql" + "embed" + "fmt" + "mime" + "net/http" + "os" + "path" + "path/filepath" + "strings" + "time" - "arimelody-web/controller" - "arimelody-web/log" - "arimelody-web/model" + "arimelody-web/admin/templates" + "arimelody-web/controller" + "arimelody-web/log" + "arimelody-web/model" - "golang.org/x/crypto/bcrypt" + "golang.org/x/crypto/bcrypt" ) func Handler(app *model.AppState) http.Handler { @@ -91,7 +95,7 @@ func AdminIndexHandler(app *model.AppState) http.Handler { Tracks []*model.Track } - err = indexTemplate.Execute(w, IndexData{ + err = templates.IndexTemplate.Execute(w, IndexData{ Session: session, Releases: releases, Artists: artists, @@ -120,7 +124,7 @@ func registerAccountHandler(app *model.AppState) http.Handler { } render := func() { - err := registerTemplate.Execute(w, registerData{ Session: session }) + err := templates.RegisterTemplate.Execute(w, registerData{ Session: session }) if err != nil { fmt.Printf("WARN: Error rendering create account page: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -230,7 +234,7 @@ func loginHandler(app *model.AppState) http.Handler { } render := func() { - err := loginTemplate.Execute(w, loginData{ Session: session }) + err := templates.LoginTemplate.Execute(w, loginData{ Session: session }) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Error rendering admin login page: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -346,7 +350,7 @@ func loginTOTPHandler(app *model.AppState) http.Handler { } render := func() { - err := loginTOTPTemplate.Execute(w, loginTOTPData{ Session: session }) + err := templates.LoginTOTPTemplate.Execute(w, loginTOTPData{ Session: session }) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to render login TOTP page: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -440,7 +444,7 @@ func logoutHandler(app *model.AppState) http.Handler { Path: "/", }) - err = logoutTemplate.Execute(w, nil) + err = templates.LogoutTemplate.Execute(w, nil) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to render logout page: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -460,24 +464,21 @@ func requireAccount(next http.Handler) http.HandlerFunc { }) } +//go:embed "static" +var staticFS embed.FS + func staticHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - info, err := os.Stat(filepath.Join("admin", "static", filepath.Clean(r.URL.Path))) - // does the file exist? + file, err := staticFS.ReadFile(filepath.Join("static", filepath.Clean(r.URL.Path))) if err != nil { - if os.IsNotExist(err) { - http.NotFound(w, r) - return - } - } - - // is thjs a directory? (forbidden) - if info.IsDir() { http.NotFound(w, r) return } - http.FileServer(http.Dir(filepath.Join("admin", "static"))).ServeHTTP(w, r) + w.Header().Set("Content-Type", mime.TypeByExtension(path.Ext(r.URL.Path))) + w.WriteHeader(http.StatusOK) + + w.Write(file) }) } diff --git a/admin/logshttp.go b/admin/logshttp.go index 7249b16..99f0ef1 100644 --- a/admin/logshttp.go +++ b/admin/logshttp.go @@ -1,12 +1,13 @@ package admin import ( - "arimelody-web/log" - "arimelody-web/model" - "fmt" - "net/http" - "os" - "strings" + "arimelody-web/admin/templates" + "arimelody-web/log" + "arimelody-web/model" + "fmt" + "net/http" + "os" + "strings" ) func logsHandler(app *model.AppState) http.Handler { @@ -54,7 +55,7 @@ func logsHandler(app *model.AppState) http.Handler { Logs []*log.Log } - err = logsTemplate.Execute(w, LogsResponse{ + err = templates.LogsTemplate.Execute(w, LogsResponse{ Session: session, Logs: logs, }) diff --git a/admin/releasehttp.go b/admin/releasehttp.go index c6b68ab..30c967b 100644 --- a/admin/releasehttp.go +++ b/admin/releasehttp.go @@ -1,12 +1,13 @@ package admin import ( - "fmt" - "net/http" - "strings" + "fmt" + "net/http" + "strings" - "arimelody-web/controller" - "arimelody-web/model" + "arimelody-web/admin/templates" + "arimelody-web/controller" + "arimelody-web/model" ) func serveRelease(app *model.AppState) http.Handler { @@ -60,7 +61,7 @@ func serveRelease(app *model.AppState) http.Handler { Release *model.Release } - err = releaseTemplate.Execute(w, ReleaseResponse{ + err = templates.EditReleaseTemplate.Execute(w, ReleaseResponse{ Session: session, Release: release, }) @@ -74,7 +75,7 @@ func serveRelease(app *model.AppState) http.Handler { func serveEditCredits(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") - err := editCreditsTemplate.Execute(w, release) + err := templates.EditCreditsTemplate.Execute(w, release) if err != nil { fmt.Printf("Error rendering edit credits component for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -97,7 +98,7 @@ func serveAddCredit(app *model.AppState, release *model.Release) http.Handler { } w.Header().Set("Content-Type", "text/html") - err = addCreditTemplate.Execute(w, response{ + err = templates.AddCreditTemplate.Execute(w, response{ ReleaseID: release.ID, Artists: artists, }) @@ -123,7 +124,7 @@ func serveNewCredit(app *model.AppState) http.Handler { } w.Header().Set("Content-Type", "text/html") - err = newCreditTemplate.Execute(w, artist) + err = templates.NewCreditTemplate.Execute(w, artist) if err != nil { fmt.Printf("Error rendering new credit component for %s: %s\n", artist.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -134,7 +135,7 @@ func serveNewCredit(app *model.AppState) http.Handler { func serveEditLinks(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") - err := editLinksTemplate.Execute(w, release) + err := templates.EditCreditsTemplate.Execute(w, release) if err != nil { fmt.Printf("Error rendering edit links component for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -151,7 +152,7 @@ func serveEditTracks(release *model.Release) http.Handler { Add func(a int, b int) int } - err := editTracksTemplate.Execute(w, editTracksData{ + err := templates.EditTracksTemplate.Execute(w, editTracksData{ Release: release, Add: func(a, b int) int { return a + b }, }) @@ -177,7 +178,7 @@ func serveAddTrack(app *model.AppState, release *model.Release) http.Handler { } w.Header().Set("Content-Type", "text/html") - err = addTrackTemplate.Execute(w, response{ + err = templates.AddTrackTemplate.Execute(w, response{ ReleaseID: release.ID, Tracks: tracks, }) @@ -185,7 +186,6 @@ func serveAddTrack(app *model.AppState, release *model.Release) http.Handler { fmt.Printf("Error rendering add tracks component for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } - return }) } @@ -204,11 +204,10 @@ func serveNewTrack(app *model.AppState) http.Handler { } w.Header().Set("Content-Type", "text/html") - err = newTrackTemplate.Execute(w, track) + err = templates.NewTrackTemplate.Execute(w, track) if err != nil { fmt.Printf("Error rendering new track component for %s: %s\n", track.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } - return }) } diff --git a/admin/templates.go b/admin/templates.go deleted file mode 100644 index 606d569..0000000 --- a/admin/templates.go +++ /dev/null @@ -1,125 +0,0 @@ -package admin - -import ( - "arimelody-web/log" - "fmt" - "html/template" - "path/filepath" - "strings" - "time" -) - -var indexTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("admin", "components", "release", "release-list-item.html"), - filepath.Join("admin", "views", "index.html"), -)) - -var loginTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("admin", "views", "login.html"), -)) -var loginTOTPTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("admin", "views", "login-totp.html"), -)) -var registerTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("admin", "views", "register.html"), -)) -var logoutTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("admin", "views", "logout.html"), -)) -var accountTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("admin", "views", "edit-account.html"), -)) -var totpSetupTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("admin", "views", "totp-setup.html"), -)) -var totpConfirmTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("admin", "views", "totp-confirm.html"), -)) - -var logsTemplate = template.Must(template.New("layout.html").Funcs(template.FuncMap{ - "parseLevel": func(level log.LogLevel) string { - switch level { - case log.LEVEL_INFO: - return "INFO" - case log.LEVEL_WARN: - return "WARN" - } - return fmt.Sprintf("%d?", level) - }, - "titleCase": func(logType string) string { - runes := []rune(logType) - for i, r := range runes { - if (i == 0 || runes[i - 1] == ' ') && r >= 'a' && r <= 'z' { - runes[i] = r + ('A' - 'a') - } - } - return string(runes) - }, - "lower": func(str string) string { return strings.ToLower(str) }, - "prettyTime": func(t time.Time) string { - // return t.Format("2006-01-02 15:04:05") - // return t.Format("15:04:05, 2 Jan 2006") - return t.Format("02 Jan 2006, 15:04:05") - }, -}).ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("admin", "views", "logs.html"), -)) - -var releaseTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("admin", "views", "edit-release.html"), -)) -var artistTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("admin", "views", "edit-artist.html"), -)) -var trackTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("admin", "components", "release", "release-list-item.html"), - filepath.Join("admin", "views", "edit-track.html"), -)) - -var editCreditsTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "components", "credits", "editcredits.html"), -)) -var addCreditTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "components", "credits", "addcredit.html"), -)) -var newCreditTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "components", "credits", "newcredit.html"), -)) - -var editLinksTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "components", "links", "editlinks.html"), -)) - -var editTracksTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "components", "tracks", "edittracks.html"), -)) -var addTrackTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "components", "tracks", "addtrack.html"), -)) -var newTrackTemplate = template.Must(template.ParseFiles( - filepath.Join("admin", "components", "tracks", "newtrack.html"), -)) diff --git a/admin/components/credits/addcredit.html b/admin/templates/html/components/credits/addcredit.html similarity index 100% rename from admin/components/credits/addcredit.html rename to admin/templates/html/components/credits/addcredit.html diff --git a/admin/components/credits/editcredits.html b/admin/templates/html/components/credits/editcredits.html similarity index 100% rename from admin/components/credits/editcredits.html rename to admin/templates/html/components/credits/editcredits.html diff --git a/admin/components/credits/newcredit.html b/admin/templates/html/components/credits/newcredit.html similarity index 100% rename from admin/components/credits/newcredit.html rename to admin/templates/html/components/credits/newcredit.html diff --git a/admin/components/links/editlinks.html b/admin/templates/html/components/links/editlinks.html similarity index 100% rename from admin/components/links/editlinks.html rename to admin/templates/html/components/links/editlinks.html diff --git a/admin/components/release/release-list-item.html b/admin/templates/html/components/release/release-list-item.html similarity index 100% rename from admin/components/release/release-list-item.html rename to admin/templates/html/components/release/release-list-item.html diff --git a/admin/components/tracks/addtrack.html b/admin/templates/html/components/tracks/addtrack.html similarity index 100% rename from admin/components/tracks/addtrack.html rename to admin/templates/html/components/tracks/addtrack.html diff --git a/admin/components/tracks/edittracks.html b/admin/templates/html/components/tracks/edittracks.html similarity index 100% rename from admin/components/tracks/edittracks.html rename to admin/templates/html/components/tracks/edittracks.html diff --git a/admin/components/tracks/newtrack.html b/admin/templates/html/components/tracks/newtrack.html similarity index 100% rename from admin/components/tracks/newtrack.html rename to admin/templates/html/components/tracks/newtrack.html diff --git a/admin/views/edit-account.html b/admin/templates/html/edit-account.html similarity index 100% rename from admin/views/edit-account.html rename to admin/templates/html/edit-account.html diff --git a/admin/views/edit-artist.html b/admin/templates/html/edit-artist.html similarity index 100% rename from admin/views/edit-artist.html rename to admin/templates/html/edit-artist.html diff --git a/admin/views/edit-release.html b/admin/templates/html/edit-release.html similarity index 100% rename from admin/views/edit-release.html rename to admin/templates/html/edit-release.html diff --git a/admin/views/edit-track.html b/admin/templates/html/edit-track.html similarity index 100% rename from admin/views/edit-track.html rename to admin/templates/html/edit-track.html diff --git a/admin/views/index.html b/admin/templates/html/index.html similarity index 100% rename from admin/views/index.html rename to admin/templates/html/index.html diff --git a/admin/views/layout.html b/admin/templates/html/layout.html similarity index 100% rename from admin/views/layout.html rename to admin/templates/html/layout.html diff --git a/admin/views/login-totp.html b/admin/templates/html/login-totp.html similarity index 100% rename from admin/views/login-totp.html rename to admin/templates/html/login-totp.html diff --git a/admin/views/login.html b/admin/templates/html/login.html similarity index 100% rename from admin/views/login.html rename to admin/templates/html/login.html diff --git a/admin/views/logout.html b/admin/templates/html/logout.html similarity index 100% rename from admin/views/logout.html rename to admin/templates/html/logout.html diff --git a/admin/views/logs.html b/admin/templates/html/logs.html similarity index 100% rename from admin/views/logs.html rename to admin/templates/html/logs.html diff --git a/views/prideflag.html b/admin/templates/html/prideflag.html similarity index 100% rename from views/prideflag.html rename to admin/templates/html/prideflag.html diff --git a/admin/views/register.html b/admin/templates/html/register.html similarity index 100% rename from admin/views/register.html rename to admin/templates/html/register.html diff --git a/admin/views/totp-confirm.html b/admin/templates/html/totp-confirm.html similarity index 100% rename from admin/views/totp-confirm.html rename to admin/templates/html/totp-confirm.html diff --git a/admin/views/totp-setup.html b/admin/templates/html/totp-setup.html similarity index 100% rename from admin/views/totp-setup.html rename to admin/templates/html/totp-setup.html diff --git a/admin/templates/templates.go b/admin/templates/templates.go new file mode 100644 index 0000000..58cd1d0 --- /dev/null +++ b/admin/templates/templates.go @@ -0,0 +1,128 @@ +package templates + +import ( + "arimelody-web/log" + "fmt" + "html/template" + "strings" + "time" + _ "embed" +) + +//go:embed "html/layout.html" +var layoutHTML string +//go:embed "html/prideflag.html" +var prideflagHTML string + +//go:embed "html/index.html" +var indexHTML string + +//go:embed "html/register.html" +var registerHTML string +//go:embed "html/login.html" +var loginHTML string +//go:embed "html/login-totp.html" +var loginTotpHTML string +//go:embed "html/totp-confirm.html" +var totpConfirmHTML string +//go:embed "html/totp-setup.html" +var totpSetupHTML string +//go:embed "html/logout.html" +var logoutHTML string + +//go:embed "html/logs.html" +var logsHTML string + +//go:embed "html/edit-account.html" +var editAccountHTML string +//go:embed "html/edit-artist.html" +var editArtistHTML string +//go:embed "html/edit-release.html" +var editReleaseHTML string +//go:embed "html/edit-track.html" +var editTrackHTML string + +//go:embed "html/components/credits/newcredit.html" +var componentNewCreditHTML string +//go:embed "html/components/credits/addcredit.html" +var componentAddCreditHTML string +//go:embed "html/components/credits/editcredits.html" +var componentEditCreditsHTML string + +//go:embed "html/components/links/editlinks.html" +var componentEditLinksHTML string + +//go:embed "html/components/release/release-list-item.html" +var componentReleaseListItemHTML string + +//go:embed "html/components/tracks/newtrack.html" +var componentNewTrackHTML string +//go:embed "html/components/tracks/addtrack.html" +var componentAddTrackHTML string +//go:embed "html/components/tracks/edittracks.html" +var componentEditTracksHTML string + +var BaseTemplate = template.Must(template.New("base").Parse( + strings.Join([]string{ layoutHTML, prideflagHTML }, "\n"), +)) + +var IndexTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse( + strings.Join([]string{ + indexHTML, + componentReleaseListItemHTML, + }, "\n"), +)) + +var LoginTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(loginHTML)) +var LoginTOTPTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(loginTotpHTML)) +var RegisterTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(registerHTML)) +var LogoutTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(logoutHTML)) +var AccountTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(editAccountHTML)) +var TOTPSetupTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(totpSetupHTML)) +var TOTPConfirmTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(totpConfirmHTML)) + +var LogsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Funcs(template.FuncMap{ + "parseLevel": func(level log.LogLevel) string { + switch level { + case log.LEVEL_INFO: + return "INFO" + case log.LEVEL_WARN: + return "WARN" + } + return fmt.Sprintf("%d?", level) + }, + "titleCase": func(logType string) string { + runes := []rune(logType) + for i, r := range runes { + if (i == 0 || runes[i - 1] == ' ') && r >= 'a' && r <= 'z' { + runes[i] = r + ('A' - 'a') + } + } + return string(runes) + }, + "lower": func(str string) string { return strings.ToLower(str) }, + "prettyTime": func(t time.Time) string { + // return t.Format("2006-01-02 15:04:05") + // return t.Format("15:04:05, 2 Jan 2006") + return t.Format("02 Jan 2006, 15:04:05") + }, +}).Parse(logsHTML)) + +var EditReleaseTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(editReleaseHTML)) +var EditArtistTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(editArtistHTML)) +var EditTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse( + strings.Join([]string{ + editTrackHTML, + componentReleaseListItemHTML, + }, "\n"), +)) + +var EditCreditsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditCreditsHTML)) +var AddCreditTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentAddCreditHTML)) +var NewCreditTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentNewCreditHTML)) + +var EditLinksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditLinksHTML)) + +var EditTracksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditTracksHTML)) +var AddTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentAddTrackHTML)) +var NewTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentNewTrackHTML)) diff --git a/admin/trackhttp.go b/admin/trackhttp.go index 93eacdb..e93d1bb 100644 --- a/admin/trackhttp.go +++ b/admin/trackhttp.go @@ -1,12 +1,13 @@ package admin import ( - "fmt" - "net/http" - "strings" + "fmt" + "net/http" + "strings" - "arimelody-web/model" - "arimelody-web/controller" + "arimelody-web/admin/templates" + "arimelody-web/controller" + "arimelody-web/model" ) func serveTrack(app *model.AppState) http.Handler { @@ -39,7 +40,7 @@ func serveTrack(app *model.AppState) http.Handler { session := r.Context().Value("session").(*model.Session) - err = trackTemplate.Execute(w, TrackResponse{ + err = templates.EditTrackTemplate.Execute(w, TrackResponse{ Session: session, Track: track, Releases: releases, diff --git a/main.go b/main.go index eaed7c6..553f109 100644 --- a/main.go +++ b/main.go @@ -1,32 +1,33 @@ package main import ( - "bufio" - "errors" - "fmt" - stdLog "log" - "math" - "math/rand" - "net" - "net/http" - "os" - "path/filepath" - "strconv" - "strings" - "time" + "bufio" + "embed" + "errors" + "fmt" + stdLog "log" + "math" + "math/rand" + "net" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" - "arimelody-web/admin" - "arimelody-web/api" - "arimelody-web/colour" - "arimelody-web/controller" - "arimelody-web/cursor" - "arimelody-web/log" - "arimelody-web/model" - "arimelody-web/view" + "arimelody-web/admin" + "arimelody-web/api" + "arimelody-web/colour" + "arimelody-web/controller" + "arimelody-web/cursor" + "arimelody-web/log" + "arimelody-web/model" + "arimelody-web/view" - "github.com/jmoiron/sqlx" - _ "github.com/lib/pq" - "golang.org/x/crypto/bcrypt" + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" + "golang.org/x/crypto/bcrypt" ) // used for database migrations @@ -35,12 +36,16 @@ const DB_VERSION = 1 const DEFAULT_PORT int64 = 8080 const HRT_DATE int64 = 1756478697 +//go:embed "public" +var publicFS embed.FS + func main() { fmt.Printf("made with <3 by ari melody\n\n") app := model.AppState{ Config: controller.GetConfig(), Twitch: nil, + PublicFS: publicFS, } // initialise database connection @@ -526,7 +531,7 @@ func createServeMux(app *model.AppState) *http.ServeMux { 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", view.StaticHandler(filepath.Join(app.Config.DataDirectory, "uploads")))) + mux.Handle("/uploads/", http.StripPrefix("/uploads", view.ServeFiles(filepath.Join(app.Config.DataDirectory, "uploads")))) mux.Handle("/cursor-ws", cursor.Handler(app)) mux.Handle("/", view.IndexHandler(app)) diff --git a/model/appstate.go b/model/appstate.go index 3a1c230..1a13be9 100644 --- a/model/appstate.go +++ b/model/appstate.go @@ -1,9 +1,11 @@ package model import ( - "github.com/jmoiron/sqlx" + "embed" - "arimelody-web/log" + "github.com/jmoiron/sqlx" + + "arimelody-web/log" ) type ( @@ -43,5 +45,6 @@ type ( Config Config Log log.Logger Twitch *TwitchState + PublicFS embed.FS } ) diff --git a/views/404.html b/templates/html/404.html similarity index 100% rename from views/404.html rename to templates/html/404.html diff --git a/views/footer.html b/templates/html/footer.html similarity index 100% rename from views/footer.html rename to templates/html/footer.html diff --git a/views/header.html b/templates/html/header.html similarity index 100% rename from views/header.html rename to templates/html/header.html diff --git a/views/index.html b/templates/html/index.html similarity index 100% rename from views/index.html rename to templates/html/index.html diff --git a/views/layout.html b/templates/html/layout.html similarity index 100% rename from views/layout.html rename to templates/html/layout.html diff --git a/views/music-gateway.html b/templates/html/music-gateway.html similarity index 100% rename from views/music-gateway.html rename to templates/html/music-gateway.html diff --git a/views/music.html b/templates/html/music.html similarity index 100% rename from views/music.html rename to templates/html/music.html diff --git a/templates/html/prideflag.html b/templates/html/prideflag.html new file mode 100644 index 0000000..47ce4c7 --- /dev/null +++ b/templates/html/prideflag.html @@ -0,0 +1,21 @@ +{{define "prideflag"}} + + + + + + + + + + + + + + + + + + + +{{end}} diff --git a/templates/templates.go b/templates/templates.go index 752c78d..c6ab41e 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -1,28 +1,36 @@ package templates import ( - "html/template" - "path/filepath" + _ "embed" + "html/template" + "strings" ) -var IndexTemplate = template.Must(template.ParseFiles( - filepath.Join("views", "layout.html"), - filepath.Join("views", "header.html"), - filepath.Join("views", "footer.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("views", "index.html"), -)) -var MusicTemplate = template.Must(template.ParseFiles( - filepath.Join("views", "layout.html"), - filepath.Join("views", "header.html"), - filepath.Join("views", "footer.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("views", "music.html"), -)) -var MusicGatewayTemplate = template.Must(template.ParseFiles( - filepath.Join("views", "layout.html"), - filepath.Join("views", "header.html"), - filepath.Join("views", "footer.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("views", "music-gateway.html"), +//go:embed "html/layout.html" +var layoutHTML string +//go:embed "html/header.html" +var headerHTML string +//go:embed "html/footer.html" +var footerHTML string +//go:embed "html/prideflag.html" +var prideflagHTML string +//go:embed "html/index.html" +var indexHTML string +//go:embed "html/music.html" +var musicHTML string +//go:embed "html/music-gateway.html" +var musicGatewayHTML string +// //go:embed "html/404.html" +// var error404HTML string + +var BaseTemplate = template.Must(template.New("base").Parse( + strings.Join([]string{ + layoutHTML, + headerHTML, + footerHTML, + prideflagHTML, + }, "\n"), )) +var IndexTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(indexHTML)) +var MusicTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(musicHTML)) +var MusicGatewayTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(musicGatewayHTML)) diff --git a/view/static.go b/view/files.go similarity index 54% rename from view/static.go rename to view/files.go index 52263a2..c1b0e29 100644 --- a/view/static.go +++ b/view/files.go @@ -1,13 +1,31 @@ package view import ( + "embed" "errors" + "mime" "net/http" "os" + "path" "path/filepath" ) -func StaticHandler(directory string) http.Handler { +func ServeEmbedFS(fs embed.FS, dir string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + file, err := fs.ReadFile(filepath.Join(dir, filepath.Clean(r.URL.Path))) + if err != nil { + http.NotFound(w, r) + return + } + + w.Header().Set("Content-Type", mime.TypeByExtension(path.Ext(r.URL.Path))) + w.WriteHeader(http.StatusOK) + + w.Write(file) + }) +} + +func ServeFiles(directory string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { info, err := os.Stat(filepath.Join(directory, filepath.Clean(r.URL.Path))) @@ -28,4 +46,3 @@ func StaticHandler(directory string) http.Handler { http.FileServer(http.Dir(directory)).ServeHTTP(w, r) }) } - diff --git a/view/index.go b/view/index.go index b6e3891..bcd0d06 100644 --- a/view/index.go +++ b/view/index.go @@ -40,6 +40,6 @@ func IndexHandler(app *model.AppState) http.Handler { return } - StaticHandler("public").ServeHTTP(w, r) + ServeEmbedFS(app.PublicFS, "public").ServeHTTP(w, r) }) } From 1999ab7d2c493f9440f5b50950e665059d33f6a2 Mon Sep 17 00:00:00 2001 From: ari melody Date: Tue, 30 Sep 2025 22:29:37 +0100 Subject: [PATCH 75/99] embed schema migration scripts --- controller/migrator.go | 20 +++++++++++++------ .../schema-migration}/000-init.sql | 0 .../schema-migration}/001-pre-versioning.sql | 0 .../schema-migration}/002-audit-logs.sql | 0 .../schema-migration}/003-fail-lock.sql | 0 controller/schema-migration/004-test.sql | 1 + 6 files changed, 15 insertions(+), 6 deletions(-) rename {schema-migration => controller/schema-migration}/000-init.sql (100%) rename {schema-migration => controller/schema-migration}/001-pre-versioning.sql (100%) rename {schema-migration => controller/schema-migration}/002-audit-logs.sql (100%) rename {schema-migration => controller/schema-migration}/003-fail-lock.sql (100%) create mode 100644 controller/schema-migration/004-test.sql diff --git a/controller/migrator.go b/controller/migrator.go index 4b99b9c..1a70e62 100644 --- a/controller/migrator.go +++ b/controller/migrator.go @@ -1,14 +1,15 @@ package controller import ( - "fmt" - "os" - "time" + "embed" + "fmt" + "os" + "time" - "github.com/jmoiron/sqlx" + "github.com/jmoiron/sqlx" ) -const DB_VERSION int = 4 +const DB_VERSION int = 5 func CheckDBVersionAndMigrate(db *sqlx.DB) { db.MustExec("CREATE SCHEMA IF NOT EXISTS arimelody") @@ -49,16 +50,23 @@ func CheckDBVersionAndMigrate(db *sqlx.DB) { ApplyMigration(db, "003-fail-lock") oldDBVersion = 4 + case 4: + ApplyMigration(db, "004-test") + oldDBVersion = 5 + } } fmt.Printf("Database schema up to date.\n") } +//go:embed "schema-migration" +var schemaFS embed.FS + func ApplyMigration(db *sqlx.DB, scriptFile string) { fmt.Printf("Applying schema migration %s...\n", scriptFile) - bytes, err := os.ReadFile("schema-migration/" + scriptFile + ".sql") + bytes, err := schemaFS.ReadFile("schema-migration/" + scriptFile + ".sql") if err != nil { fmt.Fprintf(os.Stderr, "FATAL: Failed to open schema file \"%s\": %v\n", scriptFile, err) os.Exit(1) diff --git a/schema-migration/000-init.sql b/controller/schema-migration/000-init.sql similarity index 100% rename from schema-migration/000-init.sql rename to controller/schema-migration/000-init.sql diff --git a/schema-migration/001-pre-versioning.sql b/controller/schema-migration/001-pre-versioning.sql similarity index 100% rename from schema-migration/001-pre-versioning.sql rename to controller/schema-migration/001-pre-versioning.sql diff --git a/schema-migration/002-audit-logs.sql b/controller/schema-migration/002-audit-logs.sql similarity index 100% rename from schema-migration/002-audit-logs.sql rename to controller/schema-migration/002-audit-logs.sql diff --git a/schema-migration/003-fail-lock.sql b/controller/schema-migration/003-fail-lock.sql similarity index 100% rename from schema-migration/003-fail-lock.sql rename to controller/schema-migration/003-fail-lock.sql diff --git a/controller/schema-migration/004-test.sql b/controller/schema-migration/004-test.sql new file mode 100644 index 0000000..3733de2 --- /dev/null +++ b/controller/schema-migration/004-test.sql @@ -0,0 +1 @@ +INSERT INTO arimelody.auditlog (level, type, content) VALUES (0, "test", "this is a db schema migration test!"); From 42c6540ac3adbed4ab889d60bedf5805fd22ab7d Mon Sep 17 00:00:00 2001 From: ari melody Date: Tue, 30 Sep 2025 22:30:06 +0100 Subject: [PATCH 76/99] fix upload info log line --- api/uploads.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/uploads.go b/api/uploads.go index 4678f22..3c3c58a 100644 --- a/api/uploads.go +++ b/api/uploads.go @@ -50,7 +50,7 @@ func HandleImageUpload(app *model.AppState, data *string, directory string, file return "", nil } - app.Log.Info(log.TYPE_FILES, "\"%s/%s.%s\" created.", directory, filename, ext) + app.Log.Info(log.TYPE_FILES, "\"%s\" created.", imagePath) return filename, nil } From ef655744bb36e2b93c315accdf7e8b15824b06ea Mon Sep 17 00:00:00 2001 From: ari melody Date: Tue, 30 Sep 2025 22:30:31 +0100 Subject: [PATCH 77/99] comment out deprecated QR code. uh. code --- controller/qr.go | 69 ++++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/controller/qr.go b/controller/qr.go index dd08637..21ed3e6 100644 --- a/controller/qr.go +++ b/controller/qr.go @@ -2,8 +2,8 @@ package controller import ( "encoding/base64" - "image" - "image/color" + // "image" + // "image/color" "github.com/skip2/go-qrcode" ) @@ -18,36 +18,35 @@ func GenerateQRCode(data string) (string, error) { } // vvv DEPRECATED vvv - -const margin = 4 - -type QRCodeECCLevel int64 -const ( - LOW QRCodeECCLevel = iota - MEDIUM - QUARTILE - HIGH -) - -func drawLargeAlignmentSquare(x int, y int, img *image.Gray) { - for yi := range 7 { - for xi := range 7 { - if (xi == 0 || xi == 6) || (yi == 0 || yi == 6) { - img.Set(x + xi, y + yi, color.Black) - } else if (xi > 1 && xi < 5) && (yi > 1 && yi < 5) { - img.Set(x + xi, y + yi, color.Black) - } - } - } -} - -func drawSmallAlignmentSquare(x int, y int, img *image.Gray) { - for yi := range 5 { - for xi := range 5 { - if (xi == 0 || xi == 4) || (yi == 0 || yi == 4) { - img.Set(x + xi, y + yi, color.Black) - } - } - } - img.Set(x + 2, y + 2, color.Black) -} +// const margin = 4 +// +// type QRCodeECCLevel int64 +// const ( +// LOW QRCodeECCLevel = iota +// MEDIUM +// QUARTILE +// HIGH +// ) +// +// func drawLargeAlignmentSquare(x int, y int, img *image.Gray) { +// for yi := range 7 { +// for xi := range 7 { +// if (xi == 0 || xi == 6) || (yi == 0 || yi == 6) { +// img.Set(x + xi, y + yi, color.Black) +// } else if (xi > 1 && xi < 5) && (yi > 1 && yi < 5) { +// img.Set(x + xi, y + yi, color.Black) +// } +// } +// } +// } +// +// func drawSmallAlignmentSquare(x int, y int, img *image.Gray) { +// for yi := range 5 { +// for xi := range 5 { +// if (xi == 0 || xi == 4) || (yi == 0 || yi == 4) { +// img.Set(x + xi, y + yi, color.Black) +// } +// } +// } +// img.Set(x + 2, y + 2, color.Black) +// } From 419781988aede87258eb06cb702db88c7c591dec Mon Sep 17 00:00:00 2001 From: ari melody Date: Tue, 30 Sep 2025 22:30:52 +0100 Subject: [PATCH 78/99] refactor errors.New to fmt.Errorf --- api/api.go | 5 ++--- controller/release.go | 13 ++++++------- controller/session.go | 5 ++--- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/api/api.go b/api/api.go index 398db4b..c5156c2 100644 --- a/api/api.go +++ b/api/api.go @@ -2,7 +2,6 @@ package api import ( "context" - "errors" "fmt" "net/http" "os" @@ -173,7 +172,7 @@ func getSession(app *model.AppState, r *http.Request) (*model.Session, error) { // check cookies first sessionCookie, err := r.Cookie(model.COOKIE_TOKEN) if err != nil && err != http.ErrNoCookie { - return nil, errors.New(fmt.Sprintf("Failed to retrieve session cookie: %v\n", err)) + return nil, fmt.Errorf("Failed to retrieve session cookie: %v\n", err) } if sessionCookie != nil { token = sessionCookie.Value @@ -188,7 +187,7 @@ func getSession(app *model.AppState, r *http.Request) (*model.Session, error) { session, err := controller.GetSession(app.DB, token) if err != nil && !strings.Contains(err.Error(), "no rows") { - return nil, errors.New(fmt.Sprintf("Failed to retrieve session: %v\n", err)) + return nil, fmt.Errorf("Failed to retrieve session: %v\n", err) } if session != nil { diff --git a/controller/release.go b/controller/release.go index 3dcad26..afd09fb 100644 --- a/controller/release.go +++ b/controller/release.go @@ -1,7 +1,6 @@ package controller import ( - "errors" "fmt" "arimelody-web/model" @@ -21,7 +20,7 @@ func GetRelease(db *sqlx.DB, id string, full bool) (*model.Release, error) { // get credits credits, err := GetReleaseCredits(db, id) if err != nil { - return nil, errors.New(fmt.Sprintf("Credits: %s", err)) + return nil, fmt.Errorf("Credits: %s", err) } for _, credit := range credits { release.Credits = append(release.Credits, credit) @@ -30,7 +29,7 @@ func GetRelease(db *sqlx.DB, id string, full bool) (*model.Release, error) { // get tracks tracks, err := GetReleaseTracks(db, id) if err != nil { - return nil, errors.New(fmt.Sprintf("Tracks: %s", err)) + return nil, fmt.Errorf("Tracks: %s", err) } for _, track := range tracks { release.Tracks = append(release.Tracks, track) @@ -39,7 +38,7 @@ func GetRelease(db *sqlx.DB, id string, full bool) (*model.Release, error) { // get links links, err := GetReleaseLinks(db, id) if err != nil { - return nil, errors.New(fmt.Sprintf("Links: %s", err)) + return nil, fmt.Errorf("Links: %s", err) } for _, link := range links { release.Links = append(release.Links, link) @@ -71,7 +70,7 @@ func GetAllReleases(db *sqlx.DB, onlyVisible bool, limit int, full bool) ([]*mod // get credits credits, err := GetReleaseCredits(db, release.ID) if err != nil { - return nil, errors.New(fmt.Sprintf("Credits: %s", err)) + return nil, fmt.Errorf("Credits: %s", err) } for _, credit := range credits { release.Credits = append(release.Credits, credit) @@ -81,7 +80,7 @@ func GetAllReleases(db *sqlx.DB, onlyVisible bool, limit int, full bool) ([]*mod // get tracks tracks, err := GetReleaseTracks(db, release.ID) if err != nil { - return nil, errors.New(fmt.Sprintf("Tracks: %s", err)) + return nil, fmt.Errorf("Tracks: %s", err) } for _, track := range tracks { release.Tracks = append(release.Tracks, track) @@ -90,7 +89,7 @@ func GetAllReleases(db *sqlx.DB, onlyVisible bool, limit int, full bool) ([]*mod // get links links, err := GetReleaseLinks(db, release.ID) if err != nil { - return nil, errors.New(fmt.Sprintf("Links: %s", err)) + return nil, fmt.Errorf("Links: %s", err) } for _, link := range links { release.Links = append(release.Links, link) diff --git a/controller/session.go b/controller/session.go index 5028789..dfae551 100644 --- a/controller/session.go +++ b/controller/session.go @@ -2,7 +2,6 @@ package controller import ( "database/sql" - "errors" "fmt" "net/http" "strings" @@ -19,7 +18,7 @@ const TOKEN_LEN = 64 func GetSessionFromRequest(app *model.AppState, r *http.Request) (*model.Session, error) { sessionCookie, err := r.Cookie(model.COOKIE_TOKEN) if err != nil && err != http.ErrNoCookie { - return nil, errors.New(fmt.Sprintf("Failed to retrieve session cookie: %v", err)) + return nil, fmt.Errorf("Failed to retrieve session cookie: %v", err) } var session *model.Session @@ -29,7 +28,7 @@ func GetSessionFromRequest(app *model.AppState, r *http.Request) (*model.Session session, err = GetSession(app.DB, sessionCookie.Value) if err != nil && !strings.Contains(err.Error(), "no rows") { - return nil, errors.New(fmt.Sprintf("Failed to retrieve session: %v", err)) + return nil, fmt.Errorf("Failed to retrieve session: %v", err) } if session != nil { From 028ed6029354a394e68b602aac8635dd3f24702c Mon Sep 17 00:00:00 2001 From: ari melody Date: Tue, 30 Sep 2025 22:34:46 +0100 Subject: [PATCH 79/99] oops --- controller/controller.go | 2 +- controller/migrator.go | 6 +----- controller/schema-migration/004-test.sql | 1 - 3 files changed, 2 insertions(+), 7 deletions(-) delete mode 100644 controller/schema-migration/004-test.sql diff --git a/controller/controller.go b/controller/controller.go index 44194e4..95615fb 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -5,7 +5,7 @@ import "math/rand" func GenerateAlnumString(length int) []byte { const CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" res := []byte{} - for i := 0; i < length; i++ { + for range length { res = append(res, CHARS[rand.Intn(len(CHARS))]) } return res diff --git a/controller/migrator.go b/controller/migrator.go index 1a70e62..3be8c21 100644 --- a/controller/migrator.go +++ b/controller/migrator.go @@ -9,7 +9,7 @@ import ( "github.com/jmoiron/sqlx" ) -const DB_VERSION int = 5 +const DB_VERSION int = 4 func CheckDBVersionAndMigrate(db *sqlx.DB) { db.MustExec("CREATE SCHEMA IF NOT EXISTS arimelody") @@ -50,10 +50,6 @@ func CheckDBVersionAndMigrate(db *sqlx.DB) { ApplyMigration(db, "003-fail-lock") oldDBVersion = 4 - case 4: - ApplyMigration(db, "004-test") - oldDBVersion = 5 - } } diff --git a/controller/schema-migration/004-test.sql b/controller/schema-migration/004-test.sql deleted file mode 100644 index 3733de2..0000000 --- a/controller/schema-migration/004-test.sql +++ /dev/null @@ -1 +0,0 @@ -INSERT INTO arimelody.auditlog (level, type, content) VALUES (0, "test", "this is a db schema migration test!"); From 13a84f7f25b8208bcccf40faf1208e27a964c5cd Mon Sep 17 00:00:00 2001 From: ari melody Date: Sun, 19 Oct 2025 05:01:55 +0100 Subject: [PATCH 80/99] admin dashboard early UI refresh --- .air.toml | 4 +- admin/static/admin.css | 160 ++++++++++++++++++++--------- admin/static/admin.js | 17 +++ admin/static/edit-artist.css | 18 ++-- admin/static/edit-release.css | 67 +++++++----- admin/static/edit-track.css | 9 +- admin/static/index.css | 11 +- admin/static/index.js | 8 ++ admin/static/logs.css | 42 +++++--- admin/static/release-list-item.css | 43 +++----- admin/templates/html/layout.html | 7 +- admin/templates/html/logs.html | 2 +- 12 files changed, 249 insertions(+), 139 deletions(-) diff --git a/.air.toml b/.air.toml index 070166a..c6d499b 100644 --- a/.air.toml +++ b/.air.toml @@ -7,14 +7,14 @@ tmp_dir = "tmp" bin = "./tmp/main" cmd = "go build -o ./tmp/main ." delay = 1000 - exclude_dir = ["admin/static", "admin\\static", "public", "uploads", "test", "db", "res"] + exclude_dir = ["uploads", "test", "db", "res"] exclude_file = [] exclude_regex = ["_test.go"] exclude_unchanged = false follow_symlink = false full_bin = "" include_dir = [] - include_ext = ["go", "tpl", "tmpl", "html"] + include_ext = ["go", "tpl", "tmpl", "html", "css"] include_file = [] kill_delay = "0s" log = "build-errors.log" diff --git a/admin/static/admin.css b/admin/static/admin.css index 877b5da..1f5a1fb 100644 --- a/admin/static/admin.css +++ b/admin/static/admin.css @@ -1,6 +1,71 @@ @import url("/style/prideflag.css"); @import url("/font/inter/inter.css"); +:root { + --bg-0: #101010; + --bg-1: #141414; + --bg-2: #181818; + --bg-3: #202020; + + --fg-0: #b0b0b0; + --fg-1: #c0c0c0; + --fg-2: #d0d0d0; + --fg-3: #e0e0e0; + + --col-shadow-0: #0002; + --col-shadow-1: #0004; + --col-shadow-2: #0006; + --col-highlight-0: #ffffff08; + --col-highlight-1: #fff1; + --col-highlight-2: #fff2; + + --col-new: #b3ee5b; + --col-on-new: #1b2013; + --col-save: #6fd7ff; + --col-on-save: #283f48; + --col-delete: #ff7171; + --col-on-delete: #371919; + + --col-warn: #ffe86a; + --col-on-warn: var(--bg-0); + --col-warn-hover: #ffec81; + + --shadow-sm: + 0 1px 2px var(--col-shadow-2), + inset 0 1px 1px var(--col-highlight-2); + --shadow-md: + 0 2px 4px var(--col-shadow-1), + inset 0 2px 2px var(--col-highlight-1); + --shadow-lg: + 0 4px 8px var(--col-shadow-0), + inset 0 4px 4px var(--col-highlight-0); +} + +@media (prefers-color-scheme: light) { + :root { + --bg-0: #e8e8e8; + --bg-1: #f0f0f0; + --bg-2: #f8f8f8; + --bg-3: #ffffff; + + --fg-0: #606060; + --fg-1: #404040; + --fg-2: #303030; + --fg-3: #202020; + + --col-shadow-0: #0002; + --col-shadow-1: #0004; + --col-shadow-2: #0008; + --col-highlight-0: #fff2; + --col-highlight-1: #fff4; + --col-highlight-2: #fff8; + + --col-warn: #ffe86a; + --col-on-warn: var(--fg-3); + --col-warn-hover: #ffec81; + } +} + body { width: 100%; height: calc(100vh - 1em); @@ -11,8 +76,12 @@ body { font-family: "Inter", sans-serif; font-size: 16px; - color: #303030; - background: #f0f0f0; + color: var(--fg-0); + background: var(--bg-0); +} + +h1, h2, h3, h4, h5, h6 { + color: var(--fg-3); } nav { @@ -22,40 +91,35 @@ nav { display: flex; flex-direction: row; justify-content: left; - - background: #f8f8f8; - border-radius: 4px; - border: 1px solid #808080; + gap: .5em; + user-select: none; } nav .icon { height: 100%; + border-radius: 100%; + box-shadow: var(--shadow-sm); + overflow: hidden; } -nav .title { - width: auto; +nav .icon img { + width: 100%; height: 100%; - - margin: 0 1em 0 0; - - display: flex; - - line-height: 2em; - text-decoration: none; - - color: inherit; } .nav-item { width: auto; height: 100%; - - margin: 0px; padding: 0 1em; - display: flex; + color: var(--fg-2); + background: var(--bg-2); + border-radius: 10em; + box-shadow: var(--shadow-sm); + line-height: 2em; + font-weight: 500; } .nav-item:hover { - background: #00000010; + background: var(--bg-1); text-decoration: none; } nav a { @@ -77,9 +141,11 @@ a { text-decoration: none; } +/* a:hover { text-decoration: underline; } +*/ a img.icon { height: .8em; @@ -133,17 +199,14 @@ code { #error { margin: 0 0 1em 0; padding: 1em; - border-radius: 4px; + border-radius: 8px; background: #ffffff; - border: 1px solid #888; } #message { background: #a9dfff; - border-color: #599fdc; } #error { background: #ffa9b8; - border-color: #dc5959; } @@ -152,52 +215,54 @@ a.delete:not(.button) { color: #d22828; } -button, .button { +.button, button { padding: .5em .8em; font-family: inherit; font-size: inherit; - border-radius: 4px; - border: 1px solid #a0a0a0; - background: #f0f0f0; + color: inherit; + background: var(--bg-2); + border: none; + border-radius: 10em; + box-shadow: var(--shadow-sm); + font-weight: 500; + transition: background .1s ease-out, color .1s ease-out; + + cursor: pointer; + user-select: none; } button:hover, .button:hover { background: #fff; - border-color: #d0d0d0; } button:active, .button:active { background: #d0d0d0; - border-color: #808080; } -.button, button { - color: inherit; -} .button.new, button.new { - background: #c4ff6a; - border-color: #84b141; + color: var(--col-on-new); + background: var(--col-new); } .button.save, button.save { - background: #6fd7ff; - border-color: #6f9eb0; + color: var(--col-on-save); + background: var(--col-save); } .button.delete, button.delete { - background: #ff7171; - border-color: #7d3535; + color: var(--col-on-delete); + background: var(--col-delete); } .button:hover, button:hover { - background: #fff; - border-color: #d0d0d0; + color: var(--bg-3); + background: var(--fg-3); } .button:active, button:active { - background: #d0d0d0; - border-color: #808080; + color: var(--bg-2); + background: var(--fg-0); } .button[disabled], button[disabled] { - background: #d0d0d0 !important; - border-color: #808080 !important; + color: var(--fg-0) !important; + background: var(--bg-3) !important; opacity: .5; - cursor: not-allowed !important; + cursor: default !important; } @@ -217,7 +282,6 @@ form input { padding: .3rem .5rem; display: block; border-radius: 4px; - border: 1px solid #808080; font-size: inherit; font-family: inherit; color: inherit; diff --git a/admin/static/admin.js b/admin/static/admin.js index 0763ab7..140efe0 100644 --- a/admin/static/admin.js +++ b/admin/static/admin.js @@ -69,3 +69,20 @@ export function makeMagicList(container, itemSelector, callback) { if (callback) callback(); }); } + +export function hijackClickEvent(container, link) { + container.addEventListener('click', event => { + if (event.target.tagName.toLowerCase() === 'a') return; + event.preventDefault(); + link.dispatchEvent(new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window, + ctrlKey: event.ctrlKey, + metaKey: event.metaKey, + shiftKey: event.shiftKey, + altKey: event.altKey, + button: event.button, + })); + }); +} diff --git a/admin/static/edit-artist.css b/admin/static/edit-artist.css index 5627e64..1bab082 100644 --- a/admin/static/edit-artist.css +++ b/admin/static/edit-artist.css @@ -9,9 +9,9 @@ h1 { flex-direction: row; gap: 1.2em; - border-radius: 8px; - background: #f8f8f8f8; - border: 1px solid #808080; + border-radius: 16px; + background: var(--bg-2); + box-shadow: var(--shadow-md); } .artist-avatar { @@ -27,6 +27,7 @@ h1 { cursor: pointer; } .artist-avatar #remove-avatar { + margin-top: .5em; padding: .3em .4em; } @@ -53,8 +54,8 @@ input[type="text"] { font-family: inherit; font-weight: inherit; color: inherit; - background: #ffffff; - border: 1px solid transparent; + background: var(--bg-0); + border: none; border-radius: 4px; outline: none; } @@ -85,9 +86,10 @@ input[type="text"]:focus { flex-direction: row; gap: 1em; align-items: center; - background: #f8f8f8; - border-radius: 8px; - border: 1px solid #808080; + + border-radius: 16px; + background: var(--bg-2); + box-shadow: var(--shadow-md); } .release-artwork { diff --git a/admin/static/edit-release.css b/admin/static/edit-release.css index aa70e34..d30db84 100644 --- a/admin/static/edit-release.css +++ b/admin/static/edit-release.css @@ -12,8 +12,8 @@ input[type="text"] { gap: 1.2em; border-radius: 8px; - background: #f8f8f8f8; - border: 1px solid #808080; + background: var(--bg-2); + box-shadow: var(--shadow-md); } .release-artwork { @@ -29,7 +29,8 @@ input[type="text"] { cursor: pointer; } .release-artwork #remove-artwork { - padding: .3em .4em; + margin-top: .5em; + padding: .3em .6em; } .release-info { @@ -54,17 +55,17 @@ input[type="text"] { background: transparent; outline: none; cursor: pointer; + transition: background .1s ease-out, border-color .1s ease-out; } #title:hover { - background: #ffffff; - border-color: #80808080; + background: var(--bg-3); + border-color: var(--fg-0); } #title:active, #title:focus { - background: #ffffff; - border-color: #808080; + background: var(--bg-3); } .release-title small { @@ -75,19 +76,21 @@ input[type="text"] { width: 100%; margin: .5em 0; border-collapse: collapse; + color: var(--fg-2); } .release-info table td { padding: .2em; - border-bottom: 1px solid #d0d0d0; + border-bottom: 1px solid color-mix(in srgb, var(--fg-0) 25%, transparent); + transition: background .1s ease-out, border-color .1s ease-out; } .release-info table tr td:first-child { vertical-align: top; - opacity: .66; + opacity: .5; } .release-info table tr td:not(:first-child) select:hover, .release-info table tr td:not(:first-child) input:hover, .release-info table tr td:not(:first-child) textarea:hover { - background: #e8e8e8; + background: var(--bg-3); cursor: pointer; } .release-info table td select, @@ -117,11 +120,19 @@ input[type="text"] { justify-content: right; } +.release-actions button, +.release-actions .button { + color: var(--fg-2); + background: var(--bg-3); +} + dialog { width: min(720px, calc(100% - 2em)); padding: 2em; border: 1px solid #101010; - border-radius: 8px; + border-radius: 16px; + color: var(--fg-0); + background: var(--bg-0); } dialog header { @@ -160,9 +171,9 @@ dialog div.dialog-actions { align-items: center; gap: 1em; - border-radius: 8px; - background: #f8f8f8f8; - border: 1px solid #808080; + border-radius: 16px; + background: var(--bg-2); + box-shadow: var(--shadow-md); } .card.credits .credit p { @@ -170,10 +181,11 @@ dialog div.dialog-actions { } .card.credits .credit .artist-avatar { - border-radius: 8px; + border-radius: 12px; } .card.credits .credit .artist-name { + color: var(--fg-3); font-weight: bold; } @@ -197,8 +209,8 @@ dialog div.dialog-actions { gap: 1em; border-radius: 8px; - background: #f8f8f8f8; - border: 1px solid #808080; + background: var(--bg-2); + box-shadow: var(--shadow-md); } #editcredits .credit { @@ -232,6 +244,7 @@ dialog div.dialog-actions { margin: 0; display: flex; align-items: center; + color: inherit; } #editcredits .credit .credit-info .credit-attribute input[type="text"] { @@ -239,15 +252,17 @@ dialog div.dialog-actions { padding: .2em .4em; flex-grow: 1; font-family: inherit; - border: 1px solid #8888; + border: none; border-radius: 4px; - color: inherit; + color: var(--fg-2); + background: var(--bg-0); } #editcredits .credit .credit-info .credit-attribute input[type="checkbox"] { margin: 0 .3em; } #editcredits .credit .artist-name { + color: var(--fg-2); font-weight: bold; } @@ -256,8 +271,12 @@ dialog div.dialog-actions { opacity: .66; } -#editcredits .credit button.delete { - margin-left: auto; +#editcredits .credit .delete { + margin-right: .5em; + cursor: pointer; +} +#editcredits .credit .delete:hover { + text-decoration: underline; } #addcredit ul { @@ -400,9 +419,9 @@ dialog div.dialog-actions { flex-direction: column; gap: .5em; - border-radius: 8px; - background: #f8f8f8f8; - border: 1px solid #808080; + border-radius: 16px; + background: var(--bg-2); + box-shadow: var(--shadow-md); } .card.tracks .track h3, diff --git a/admin/static/edit-track.css b/admin/static/edit-track.css index 600b680..1a9323f 100644 --- a/admin/static/edit-track.css +++ b/admin/static/edit-track.css @@ -11,9 +11,9 @@ h1 { flex-direction: row; gap: 1.2em; - border-radius: 8px; - background: #f8f8f8f8; - border: 1px solid #808080; + border-radius: 16px; + background: var(--bg-2); + box-shadow: var(--shadow-md); } .track-info { @@ -49,7 +49,8 @@ h1 { font-weight: inherit; font-family: inherit; font-size: inherit; - border: 1px solid transparent; + background: var(--bg-0); + border: none; border-radius: 4px; outline: none; color: inherit; diff --git a/admin/static/index.css b/admin/static/index.css index 9fcd731..278224e 100644 --- a/admin/static/index.css +++ b/admin/static/index.css @@ -7,13 +7,18 @@ flex-direction: row; align-items: center; gap: .5em; + color: var(--fg-3); - border-radius: 8px; - background: #f8f8f8f8; - border: 1px solid #808080; + border-radius: 16px; + background: var(--bg-2); + box-shadow: var(--shadow-md); + + transition: background .1s ease-out; + cursor: pointer; } .artist:hover { + background: var(--bg-1); text-decoration: hover; } diff --git a/admin/static/index.js b/admin/static/index.js index e251802..0091fa4 100644 --- a/admin/static/index.js +++ b/admin/static/index.js @@ -1,3 +1,5 @@ +import { hijackClickEvent } from "./admin.js"; + const newReleaseBtn = document.getElementById("create-release"); const newArtistBtn = document.getElementById("create-artist"); const newTrackBtn = document.getElementById("create-track"); @@ -72,3 +74,9 @@ newTrackBtn.addEventListener("click", event => { console.error(err); }); }); + +document.addEventListener("readystatechange", () => { + document.querySelectorAll(".card.artists .artist").forEach(el => { + hijackClickEvent(el, el.querySelector("a.artist-name")) + }); +}); diff --git a/admin/static/logs.css b/admin/static/logs.css index f0df299..4a66038 100644 --- a/admin/static/logs.css +++ b/admin/static/logs.css @@ -2,8 +2,14 @@ main { width: min(1080px, calc(100% - 2em))!important } -form { +form#search-form { + width: calc(100% - 2em); margin: 1em 0; + padding: 1em; + border-radius: 16px; + color: var(--fg-0); + background: var(--bg-2); + box-shadow: var(--shadow-md); } div#search { @@ -12,24 +18,25 @@ div#search { #search input { margin: 0; + padding: .3em .8em; flex-grow: 1; - - border-right: none; - border-top-right-radius: 0; - border-bottom-right-radius: 0; + border: none; + border-radius: 16px; + color: var(--fg-1); + background: var(--bg-0); + box-shadow: var(--shadow-sm); } #search button { - padding: 0 .5em; - - border-top-left-radius: 0; - border-bottom-left-radius: 0; + margin-left: .5em; + padding: 0 .8em; } form #filters p { margin: .5em 0 0 0; } form #filters label { + color: inherit; display: inline; } form #filters input { @@ -57,6 +64,10 @@ form #filters input { padding: .4em .8em; } +#logs .log { + color: var(--fg-2); +} + td, th { width: 1%; text-align: left; @@ -74,13 +85,14 @@ td.log-content { white-space: collapse; } -.log:hover { - background: #fff8; +#logs .log:hover { + background: color-mix(in srgb, var(--fg-3) 10%, transparent); } -.log.warn { - background: #ffe86a; +#logs .log.warn { + color: var(--col-on-warn); + background: var(--col-warn); } -.log.warn:hover { - background: #ffec81; +#logs .log.warn:hover { + background: var(--col-warn-hover); } diff --git a/admin/static/release-list-item.css b/admin/static/release-list-item.css index 638eac0..fb5d2d4 100644 --- a/admin/static/release-list-item.css +++ b/admin/static/release-list-item.css @@ -5,9 +5,9 @@ flex-direction: row; gap: 1em; - border-radius: 8px; - background: #f8f8f8f8; - border: 1px solid #808080; + border-radius: 16px; + background: var(--bg-2); + box-shadow: var(--shadow-md); } .release h3, @@ -16,11 +16,15 @@ } .release-artwork { + margin: auto 0; width: 96px; display: flex; justify-content: center; align-items: center; + border-radius: 4px; + overflow: hidden; + box-shadow: var(--shadow-sm); } .release-artwork img { @@ -42,30 +46,9 @@ gap: .5em; } -.release-links li { - flex-grow: 1; -} - -.release-links a { - padding: .5em; - display: block; - - border-radius: 8px; - text-decoration: none; - color: #f0f0f0; - background: #303030; - text-align: center; - - transition: color .1s, background .1s; -} - -.release-links a:hover { - color: #303030; - background: #f0f0f0; -} - .release-actions { margin-top: .5em; + user-select: none; } .release-actions a { @@ -74,14 +57,14 @@ display: inline-block; border-radius: 4px; - background: #e0e0e0; + background: var(--bg-3); + box-shadow: var(--shadow-sm); - transition: color .1s, background .1s; + transition: color .1s ease-out, background .1s ease-out; } .release-actions a:hover { - color: #303030; - background: #f0f0f0; - + background: var(--bg-0); + color: var(--fg-3); text-decoration: none; } diff --git a/admin/templates/html/layout.html b/admin/templates/html/layout.html index fdeda9b..4925ce9 100644 --- a/admin/templates/html/layout.html +++ b/admin/templates/html/layout.html @@ -16,10 +16,9 @@