From 7044f7344be363678a230b090cf7b31af794f119 Mon Sep 17 00:00:00 2001 From: ari melody Date: Tue, 21 Jan 2025 00:20:07 +0000 Subject: [PATCH] very rough updates to admin pages, reduced reliance on global.DB --- admin/accounthttp.go | 38 ++++++++++++++++++++++++ admin/http.go | 52 ++++++++++++++++----------------- admin/static/edit-account.css | 41 ++++++++++++++++++++++++++ admin/static/index.css | 45 ++++++++++++++++++++++++++++ admin/templates.go | 5 ++++ admin/views/create-account.html | 24 +-------------- admin/views/edit-account.html | 5 ++-- admin/views/edit-artist.html | 1 - admin/views/edit-release.html | 1 - admin/views/edit-track.html | 2 +- admin/views/login.html | 22 -------------- api/api.go | 33 +++++++++++---------- api/track.go | 4 +-- main.go | 6 ++-- view/music.go | 19 ++++++------ 15 files changed, 192 insertions(+), 106 deletions(-) create mode 100644 admin/accounthttp.go create mode 100644 admin/static/edit-account.css diff --git a/admin/accounthttp.go b/admin/accounthttp.go new file mode 100644 index 0000000..56aa247 --- /dev/null +++ b/admin/accounthttp.go @@ -0,0 +1,38 @@ +package admin + +import ( + "fmt" + "net/http" + + "arimelody-web/controller" + "arimelody-web/model" + + "github.com/jmoiron/sqlx" +) + +func AccountHandler(db *sqlx.DB) 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) + if err != nil { + fmt.Printf("WARN: Failed to fetch TOTPs: %v\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + + type AccountResponse struct { + Account *model.Account + TOTPs []model.TOTP + } + + err = pages["account"].Execute(w, AccountResponse{ + Account: account, + TOTPs: totps, + }) + if err != nil { + fmt.Printf("WARN: Failed to render admin account page: %v\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + }) +} + diff --git a/admin/http.go b/admin/http.go index 4dbc66b..9eae2b7 100644 --- a/admin/http.go +++ b/admin/http.go @@ -22,24 +22,24 @@ type TemplateData struct { Token string } -func Handler() http.Handler { +func Handler(db *sqlx.DB) http.Handler { mux := http.NewServeMux() - mux.Handle("/login", LoginHandler()) - mux.Handle("/register", createAccountHandler()) - mux.Handle("/logout", RequireAccount(global.DB, LogoutHandler())) - // TODO: /admin/account + 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("/static/", http.StripPrefix("/static", staticHandler())) - mux.Handle("/release/", RequireAccount(global.DB, http.StripPrefix("/release", serveRelease()))) - mux.Handle("/artist/", RequireAccount(global.DB, http.StripPrefix("/artist", serveArtist()))) - mux.Handle("/track/", RequireAccount(global.DB, http.StripPrefix("/track", serveTrack()))) + 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("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } - account, err := controller.GetAccountByRequest(global.DB, r) + account, err := controller.GetAccountByRequest(db, r) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %s\n", err) } @@ -48,21 +48,21 @@ func Handler() http.Handler { return } - releases, err := controller.GetAllReleases(global.DB, false, 0, true) + releases, err := controller.GetAllReleases(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(global.DB) + artists, err := controller.GetAllArtists(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(global.DB) + tracks, err := controller.GetOrphanTracks(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) @@ -112,10 +112,10 @@ func RequireAccount(db *sqlx.DB, next http.Handler) http.HandlerFunc { }) } -func LoginHandler() http.Handler { +func LoginHandler(db *sqlx.DB) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { - account, err := controller.GetAccountByRequest(global.DB, r) + account, err := controller.GetAccountByRequest(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) @@ -157,7 +157,7 @@ func LoginHandler() http.Handler { TOTP: r.Form.Get("totp"), } - account, err := controller.GetAccount(global.DB, credentials.Username) + account, err := controller.GetAccount(db, credentials.Username) if err != nil { http.Error(w, "Invalid username or password", http.StatusBadRequest) return @@ -176,7 +176,7 @@ func LoginHandler() http.Handler { // TODO: check TOTP // login success! - token, err := controller.CreateToken(global.DB, account.ID, r.UserAgent()) + token, err := controller.CreateToken(db, account.ID, r.UserAgent()) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to create token: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -206,17 +206,17 @@ func LoginHandler() http.Handler { }) } -func LogoutHandler() http.Handler { +func LogoutHandler(db *sqlx.DB) 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(global.DB, r) + tokenStr := controller.GetTokenFromRequest(db, r) if len(tokenStr) > 0 { - err := controller.DeleteToken(global.DB, tokenStr) + err := controller.DeleteToken(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) @@ -238,9 +238,9 @@ func LogoutHandler() http.Handler { }) } -func createAccountHandler() http.Handler { +func createAccountHandler(db *sqlx.DB) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - checkAccount, err := controller.GetAccountByRequest(global.DB, r) + checkAccount, err := controller.GetAccountByRequest(db, r) if err != nil { fmt.Printf("WARN: Failed to fetch account: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -297,7 +297,7 @@ func createAccountHandler() http.Handler { } // make sure code exists in DB - invite, err := controller.GetInvite(global.DB, credentials.Invite) + invite, err := controller.GetInvite(db, credentials.Invite) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve invite: %v\n", err) render(CreateAccountResponse{ @@ -307,7 +307,7 @@ func createAccountHandler() http.Handler { } if invite == nil || time.Now().After(invite.ExpiresAt) { if invite != nil { - err := controller.DeleteInvite(global.DB, invite.Code) + err := controller.DeleteInvite(db, invite.Code) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %v\n", err) } } render(CreateAccountResponse{ @@ -331,7 +331,7 @@ func createAccountHandler() http.Handler { Email: credentials.Email, AvatarURL: "/img/default-avatar.png", } - err = controller.CreateAccount(global.DB, &account) + err = controller.CreateAccount(db, &account) if err != nil { if strings.HasPrefix(err.Error(), "pq: duplicate key") { render(CreateAccountResponse{ @@ -346,11 +346,11 @@ func createAccountHandler() http.Handler { return } - err = controller.DeleteInvite(global.DB, invite.Code) + err = controller.DeleteInvite(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(global.DB, account.ID, r.UserAgent()) + token, err := controller.CreateToken(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 diff --git a/admin/static/edit-account.css b/admin/static/edit-account.css new file mode 100644 index 0000000..7c75b66 --- /dev/null +++ b/admin/static/edit-account.css @@ -0,0 +1,41 @@ +@import url("/admin/static/index.css"); + +form#change-password { + width: 100%; + display: flex; + flex-direction: column; + align-items: start; +} + +form div { + width: 20rem; +} + +form button { + margin-top: 1rem; +} + +label { + width: 100%; + margin: 1rem 0 .5rem 0; + display: block; + color: #10101080; +} +input { + width: 100%; + margin: .5rem 0; + padding: .3rem .5rem; + display: block; + border-radius: 4px; + border: 1px solid #808080; + font-size: inherit; + font-family: inherit; + color: inherit; +} + +#error { + background: #ffa9b8; + border: 1px solid #dc5959; + padding: 1em; + border-radius: 4px; +} diff --git a/admin/static/index.css b/admin/static/index.css index ec426af..1411eff 100644 --- a/admin/static/index.css +++ b/admin/static/index.css @@ -99,3 +99,48 @@ opacity: 0.75; } + + +button, .button { + padding: .5em .8em; + font-family: inherit; + font-size: inherit; + border-radius: .5em; + border: 1px solid #a0a0a0; + background: #f0f0f0; + color: inherit; +} +button:hover, .button:hover { + background: #fff; + border-color: #d0d0d0; +} +button:active, .button:active { + background: #d0d0d0; + border-color: #808080; +} + +button { + color: inherit; +} +button.save { + background: #6fd7ff; + border-color: #6f9eb0; +} +button.delete { + background: #ff7171; + border-color: #7d3535; +} +button:hover { + background: #fff; + border-color: #d0d0d0; +} +button:active { + background: #d0d0d0; + border-color: #808080; +} +button[disabled] { + background: #d0d0d0 !important; + border-color: #808080 !important; + opacity: .5; + cursor: not-allowed !important; +} diff --git a/admin/templates.go b/admin/templates.go index e91313a..1fa7a65 100644 --- a/admin/templates.go +++ b/admin/templates.go @@ -28,6 +28,11 @@ var pages = map[string]*template.Template{ filepath.Join("views", "prideflag.html"), filepath.Join("admin", "views", "logout.html"), )), + "account": template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "views", "edit-account.html"), + )), "release": template.Must(template.ParseFiles( filepath.Join("admin", "views", "layout.html"), diff --git a/admin/views/create-account.html b/admin/views/create-account.html index 5d92627..b0aff03 100644 --- a/admin/views/create-account.html +++ b/admin/views/create-account.html @@ -1,7 +1,7 @@ {{define "head"}} Register - ari melody 💫 - + {{end}} diff --git a/api/api.go b/api/api.go index 30ef73e..26e2255 100644 --- a/api/api.go +++ b/api/api.go @@ -6,11 +6,12 @@ import ( "strings" "arimelody-web/admin" - "arimelody-web/global" "arimelody-web/controller" + + "github.com/jmoiron/sqlx" ) -func Handler() http.Handler { +func Handler(db *sqlx.DB) http.Handler { mux := http.NewServeMux() // ACCOUNT ENDPOINTS @@ -31,7 +32,7 @@ func Handler() 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(global.DB, artistID) + artist, err := controller.GetArtist(db, artistID) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) @@ -48,10 +49,10 @@ func Handler() http.Handler { ServeArtist(artist).ServeHTTP(w, r) case http.MethodPut: // PUT /api/v1/artist/{id} (admin) - admin.RequireAccount(global.DB, UpdateArtist(artist)).ServeHTTP(w, r) + admin.RequireAccount(db, UpdateArtist(artist)).ServeHTTP(w, r) case http.MethodDelete: // DELETE /api/v1/artist/{id} (admin) - admin.RequireAccount(global.DB, DeleteArtist(artist)).ServeHTTP(w, r) + admin.RequireAccount(db, DeleteArtist(artist)).ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -63,7 +64,7 @@ func Handler() http.Handler { ServeAllArtists().ServeHTTP(w, r) case http.MethodPost: // POST /api/v1/artist (admin) - admin.RequireAccount(global.DB, CreateArtist()).ServeHTTP(w, r) + admin.RequireAccount(db, CreateArtist()).ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -73,7 +74,7 @@ func Handler() 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(global.DB, releaseID, true) + release, err := controller.GetRelease(db, releaseID, true) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) @@ -90,10 +91,10 @@ func Handler() http.Handler { ServeRelease(release).ServeHTTP(w, r) case http.MethodPut: // PUT /api/v1/music/{id} (admin) - admin.RequireAccount(global.DB, UpdateRelease(release)).ServeHTTP(w, r) + admin.RequireAccount(db, UpdateRelease(release)).ServeHTTP(w, r) case http.MethodDelete: // DELETE /api/v1/music/{id} (admin) - admin.RequireAccount(global.DB, DeleteRelease(release)).ServeHTTP(w, r) + admin.RequireAccount(db, DeleteRelease(release)).ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -105,7 +106,7 @@ func Handler() http.Handler { ServeCatalog().ServeHTTP(w, r) case http.MethodPost: // POST /api/v1/music (admin) - admin.RequireAccount(global.DB, CreateRelease()).ServeHTTP(w, r) + admin.RequireAccount(db, CreateRelease()).ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -115,7 +116,7 @@ func Handler() 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(global.DB, trackID) + track, err := controller.GetTrack(db, trackID) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) @@ -129,13 +130,13 @@ func Handler() http.Handler { switch r.Method { case http.MethodGet: // GET /api/v1/track/{id} (admin) - admin.RequireAccount(global.DB, ServeTrack(track)).ServeHTTP(w, r) + admin.RequireAccount(db, ServeTrack(track)).ServeHTTP(w, r) case http.MethodPut: // PUT /api/v1/track/{id} (admin) - admin.RequireAccount(global.DB, UpdateTrack(track)).ServeHTTP(w, r) + admin.RequireAccount(db, UpdateTrack(track)).ServeHTTP(w, r) case http.MethodDelete: // DELETE /api/v1/track/{id} (admin) - admin.RequireAccount(global.DB, DeleteTrack(track)).ServeHTTP(w, r) + admin.RequireAccount(db, DeleteTrack(track)).ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -144,10 +145,10 @@ func Handler() http.Handler { switch r.Method { case http.MethodGet: // GET /api/v1/track (admin) - admin.RequireAccount(global.DB, ServeAllTracks()).ServeHTTP(w, r) + admin.RequireAccount(db, ServeAllTracks()).ServeHTTP(w, r) case http.MethodPost: // POST /api/v1/track (admin) - admin.RequireAccount(global.DB, CreateTrack()).ServeHTTP(w, r) + admin.RequireAccount(db, CreateTrack()).ServeHTTP(w, r) default: http.NotFound(w, r) } diff --git a/api/track.go b/api/track.go index ebbaa10..8727b4f 100644 --- a/api/track.go +++ b/api/track.go @@ -126,7 +126,7 @@ func UpdateTrack(track *model.Track) http.Handler { err = controller.UpdateTrack(global.DB, track) if err != nil { - fmt.Printf("Failed to update track %s: %s\n", track.ID, err) + fmt.Printf("WARN: Failed to update track %s: %s\n", track.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -151,7 +151,7 @@ func DeleteTrack(track *model.Track) http.Handler { var trackID = r.URL.Path[1:] err := controller.DeleteTrack(global.DB, trackID) if err != nil { - fmt.Printf("Failed to delete track %s: %s\n", trackID, err) + fmt.Printf("WARN: Failed to delete track %s: %s\n", trackID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) diff --git a/main.go b/main.go index aa3cff6..7cf3363 100644 --- a/main.go +++ b/main.go @@ -341,9 +341,9 @@ func main() { func createServeMux() *http.ServeMux { mux := http.NewServeMux() - mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler())) - mux.Handle("/api/", http.StripPrefix("/api", api.Handler())) - mux.Handle("/music/", http.StripPrefix("/music", view.MusicHandler())) + 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("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodHead { diff --git a/view/music.go b/view/music.go index 3799182..227aa5c 100644 --- a/view/music.go +++ b/view/music.go @@ -6,37 +6,38 @@ import ( "os" "arimelody-web/controller" - "arimelody-web/global" "arimelody-web/model" "arimelody-web/templates" + + "github.com/jmoiron/sqlx" ) // HTTP HANDLER METHODS -func MusicHandler() http.Handler { +func MusicHandler(db *sqlx.DB) http.Handler { mux := http.NewServeMux() mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { - ServeCatalog().ServeHTTP(w, r) + ServeCatalog(db).ServeHTTP(w, r) return } - release, err := controller.GetRelease(global.DB, r.URL.Path[1:], true) + release, err := controller.GetRelease(db, r.URL.Path[1:], true) if err != nil { http.NotFound(w, r) return } - ServeGateway(release).ServeHTTP(w, r) + ServeGateway(db, release).ServeHTTP(w, r) })) return mux } -func ServeCatalog() http.Handler { +func ServeCatalog(db *sqlx.DB) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - releases, err := controller.GetAllReleases(global.DB, true, 0, true) + releases, err := controller.GetAllReleases(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) @@ -56,12 +57,12 @@ func ServeCatalog() http.Handler { }) } -func ServeGateway(release *model.Release) http.Handler { +func ServeGateway(db *sqlx.DB, 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(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)