refactored out global. long live AppState

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

1
.gitignore vendored
View file

@ -7,3 +7,4 @@ uploads/
docker-compose*.yml
!docker-compose.example.yml
config*.toml
arimelody-web

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@ package controller
import (
"arimelody-web/model"
"github.com/jmoiron/sqlx"
)

View file

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

View file

@ -5,6 +5,7 @@ import (
"fmt"
"arimelody-web/model"
"github.com/jmoiron/sqlx"
)

View file

@ -2,6 +2,7 @@ package controller
import (
"arimelody-web/model"
"github.com/jmoiron/sqlx"
)

View file

@ -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),
)
}

View file

@ -1,3 +0,0 @@
package global
const COOKIE_TOKEN string = "AM_TOKEN"

View file

@ -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])
})
}

182
main.go
View file

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

View file

@ -2,6 +2,8 @@ package model
import "time"
const COOKIE_TOKEN string = "AM_TOKEN"
type (
Account struct {
ID string `json:"id" db:"id"`

32
model/appstate.go Normal file
View file

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

View file

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