diff --git a/.dockerignore b/.dockerignore index e2d5600..3ba6b11 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,6 +6,7 @@ uploads/ test/ tmp/ +db/ res/ docker-compose.yml docker-compose-test.yml diff --git a/.gitignore b/.gitignore index 025d915..cccde2b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,3 @@ uploads/ docker-compose*.yml !docker-compose.example.yml config*.toml ->>>>>>> dev diff --git a/Dockerfile b/Dockerfile index 0e0d2a2..278f01a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ RUN CGO_ENABLED=0 GOOS=linux go build -o /arimelody-web # --- -FROM scratch +FROM build-stage AS build-release-stage WORKDIR /app diff --git a/README.md b/README.md index 0873ff6..df0c351 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ library for others to use in their own sites. exciting stuff! ## running the server should be run once to generate a default `config.toml` file. -configure as needed. a valid DB connection is required to run this website. -if no admin users exist, an invite code will be provided. invite codes are -the only way to create admin accounts at this time. +configure as needed. note that a valid DB connection is required, and the admin +panel will be disabled without valid discord app credentials (this can however +be bypassed by running the server with `-adminBypass`). the configuration may be overridden using environment variables in the format `ARIMELODY__`. for example, `db.host` in the config may @@ -32,16 +32,6 @@ be overridden with `ARIMELODY_DB_HOST`. the location of the configuration file can also be overridden with `ARIMELODY_CONFIG`. -## command arguments - -by default, `arimelody-web` will spin up a web server as usual. instead, -arguments may be supplied to run administrative actions. the web server doesn't -need to be up for this, making this ideal for some offline maintenance. - -- `createInvite`: Creates an invite code to register new accounts. -- `purgeInvites`: Deletes all available invite codes. -- `deleteAccount `: Deletes an account with a given `username`. - ## database the server requires a postgres database to run. you can use the diff --git a/admin/admin.go b/admin/admin.go new file mode 100644 index 0000000..a12e85b --- /dev/null +++ b/admin/admin.go @@ -0,0 +1,48 @@ +package admin + +import ( + "fmt" + "math/rand" + "time" + + "arimelody-web/global" +) + +type ( + Session struct { + Token string + UserID string + Expires time.Time + } +) + +const TOKEN_LENGTH = 64 +const TOKEN_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +var ADMIN_BYPASS = func() bool { + if global.Args["adminBypass"] == "true" { + fmt.Println("WARN: Admin login is currently BYPASSED. (-adminBypass)") + return true + } + return false +}() + +var sessions []*Session + +func createSession(userID string, expires time.Time) Session { + return Session{ + Token: string(generateToken()), + UserID: userID, + Expires: expires, + } +} + +func generateToken() string { + var token []byte + + for i := 0; i < TOKEN_LENGTH; i++ { + token = append(token, TOKEN_CHARS[rand.Intn(len(TOKEN_CHARS))]) + } + + return string(token) +} diff --git a/admin/artisthttp.go b/admin/artisthttp.go index af42cb1..ba26240 100644 --- a/admin/artisthttp.go +++ b/admin/artisthttp.go @@ -32,19 +32,12 @@ func serveArtist() http.Handler { return } - type ArtistResponse struct { - Account *model.Account - Artist *model.Artist - Credits []*model.Credit + type Artist struct { + *model.Artist + Credits []*model.Credit } - account := r.Context().Value("account").(*model.Account) - - err = pages["artist"].Execute(w, ArtistResponse{ - Account: account, - Artist: artist, - Credits: credits, - }) + err = pages["artist"].Execute(w, Artist{ Artist: artist, Credits: credits }) 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/components/credits/editcredits.html b/admin/components/credits/editcredits.html index 94dc268..999740a 100644 --- a/admin/components/credits/editcredits.html +++ b/admin/components/credits/editcredits.html @@ -52,6 +52,7 @@ makeMagicList(creditList, ".credit"); function rigCredit(el) { + console.log(el); const artistID = el.dataset.artist; const deleteBtn = el.querySelector("a.delete"); diff --git a/admin/components/tracks/edittracks.html b/admin/components/tracks/edittracks.html index 0500532..dba57a6 100644 --- a/admin/components/tracks/edittracks.html +++ b/admin/components/tracks/edittracks.html @@ -12,12 +12,12 @@
    - {{range $i, $track := .Tracks}} -
  • + {{range .Tracks}} +
  • - {{$track.Add $i 1}} - {{$track.Title}} + {{.Number}} + {{.Title}}

    Delete
    diff --git a/admin/http.go b/admin/http.go index 4dbc66b..b69342f 100644 --- a/admin/http.go +++ b/admin/http.go @@ -8,17 +8,17 @@ import ( "path/filepath" "strings" "time" + "encoding/json" - "arimelody-web/controller" "arimelody-web/global" + "arimelody-web/controller" "arimelody-web/model" - "github.com/jmoiron/sqlx" "golang.org/x/crypto/bcrypt" ) -type TemplateData struct { - Account *model.Account +type loginData struct { + DiscordURI string Token string } @@ -26,64 +26,57 @@ func Handler() 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("/logout", MustAuthorise(LogoutHandler())) 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/", MustAuthorise(http.StripPrefix("/release", serveRelease()))) + mux.Handle("/artist/", MustAuthorise(http.StripPrefix("/artist", serveArtist()))) + mux.Handle("/track/", MustAuthorise(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) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %s\n", err) - } - if account == nil { + session := GetSession(r) + if session == nil { http.Redirect(w, r, "/admin/login", http.StatusFound) return } releases, err := controller.GetAllReleases(global.DB, false, 0, true) if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases: %s\n", err) + fmt.Printf("FATAL: Failed to pull releases: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } artists, err := controller.GetAllArtists(global.DB) if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to pull artists: %s\n", err) + fmt.Printf("FATAL: Failed to pull artists: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } tracks, err := controller.GetOrphanTracks(global.DB) if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to pull orphan tracks: %s\n", err) + fmt.Printf("FATAL: Failed to pull orphan tracks: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } type IndexData struct { - Account *model.Account Releases []*model.Release Artists []*model.Artist Tracks []*model.Track } err = pages["index"].Execute(w, IndexData{ - Account: account, Releases: releases, Artists: artists, Tracks: tracks, }) if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to render admin index: %s\n", err) + fmt.Printf("Error executing template: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -92,43 +85,76 @@ func Handler() http.Handler { return mux } -func RequireAccount(db *sqlx.DB, next http.Handler) http.HandlerFunc { +func MustAuthorise(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - 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) - return - } - if account == nil { - // TODO: include context in redirect - http.Redirect(w, r, "/admin/login", http.StatusFound) + session := GetSession(r) + if session == nil { + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } - ctx := context.WithValue(r.Context(), "account", account) - + ctx := context.WithValue(r.Context(), "session", session) next.ServeHTTP(w, r.WithContext(ctx)) }) } +func GetSession(r *http.Request) *Session { + if ADMIN_BYPASS { + return &Session{} + } + + var token = "" + // is the session token in context? + var ctx_session = r.Context().Value("session") + if ctx_session != nil { + token = ctx_session.(*Session).Token + } + + // okay, is it in the auth header? + if token == "" { + if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") { + token = r.Header.Get("Authorization")[7:] + } + } + // finally, is it in the cookie? + if token == "" { + cookie, err := r.Cookie("token") + if err != nil { + return nil + } + token = cookie.Value + } + + var session *Session = nil + for _, s := range sessions { + if s.Expires.Before(time.Now()) { + // expired session. remove it from the list! + new_sessions := []*Session{} + for _, ns := range sessions { + if ns.Token == s.Token { + continue + } + new_sessions = append(new_sessions, ns) + } + sessions = new_sessions + continue + } + + if s.Token == token { + session = s + break + } + } + + return session +} + func LoginHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { - account, err := controller.GetAccountByRequest(global.DB, r) + err := pages["login"].Execute(w, nil) if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %v\n", err) - return - } - if account != nil { - http.Redirect(w, r, "/admin", http.StatusFound) - return - } - - err = pages["login"].Execute(w, TemplateData{}) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Error rendering admin login page: %s\n", err) + fmt.Printf("Error rendering admin login page: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -140,53 +166,41 @@ func LoginHandler() http.Handler { return } - err := r.ParseForm() - if err != nil { - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) - return - } - type LoginRequest struct { Username string `json:"username"` Password string `json:"password"` TOTP string `json:"totp"` } - credentials := LoginRequest{ - Username: r.Form.Get("username"), - Password: r.Form.Get("password"), - TOTP: r.Form.Get("totp"), - } - account, err := controller.GetAccount(global.DB, credentials.Username) + data := LoginRequest{} + err := json.NewDecoder(r.Body).Decode(&data) if err != nil { - http.Error(w, "Invalid username or password", http.StatusBadRequest) - return - } - if account == nil { - http.Error(w, "Invalid username or password", http.StatusBadRequest) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } - err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(credentials.Password)) + account, err := controller.GetAccount(global.DB, data.Username) if err != nil { - http.Error(w, "Invalid username or password", http.StatusBadRequest) + http.Error(w, "No account exists with this username and password.", http.StatusBadRequest) + return + } + + err = bcrypt.CompareHashAndPassword(account.Password, []byte(data.Password)) + if err != nil { + http.Error(w, "No account exists with this username and password.", http.StatusBadRequest) return } // TODO: check TOTP // login success! - token, err := controller.CreateToken(global.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) - return - } + session := createSession(account.ID, time.Now().Add(24 * time.Hour)) + sessions = append(sessions, &session) cookie := http.Cookie{} - cookie.Name = global.COOKIE_TOKEN - cookie.Value = token.Token - cookie.Expires = token.ExpiresAt + cookie.Name = "token" + cookie.Value = session.Token + cookie.Expires = time.Now().Add(24 * time.Hour) if strings.HasPrefix(global.Config.BaseUrl, "https") { cookie.Secure = true } @@ -194,10 +208,7 @@ func LoginHandler() http.Handler { cookie.Path = "/" http.SetCookie(w, &cookie) - err = pages["login"].Execute(w, TemplateData{ - Account: account, - Token: token.Token, - }) + err = pages["login"].Execute(w, loginData{Token: session.Token}) if err != nil { fmt.Printf("Error rendering admin login page: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -213,168 +224,22 @@ func LogoutHandler() http.Handler { return } - tokenStr := controller.GetTokenFromRequest(global.DB, r) + session := GetSession(r) - if len(tokenStr) > 0 { - err := controller.DeleteToken(global.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) - return + // remove this session from the list + sessions = func (token string) []*Session { + new_sessions := []*Session{} + for _, session := range sessions { + if session.Token != token { + new_sessions = append(new_sessions, session) + } } - } + return new_sessions + }(session.Token) - cookie := http.Cookie{} - cookie.Name = global.COOKIE_TOKEN - cookie.Value = "" - cookie.Expires = time.Now() - if strings.HasPrefix(global.Config.BaseUrl, "https") { - cookie.Secure = true - } - cookie.HttpOnly = true - cookie.Path = "/" - http.SetCookie(w, &cookie) - http.Redirect(w, r, "/admin/login", http.StatusFound) - }) -} - -func createAccountHandler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - checkAccount, err := controller.GetAccountByRequest(global.DB, r) + err := pages["logout"].Execute(w, nil) if err != nil { - fmt.Printf("WARN: Failed to fetch account: %s\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - if checkAccount != nil { - // user is already logged in - http.Redirect(w, r, "/admin", http.StatusFound) - return - } - - type CreateAccountResponse struct { - Account *model.Account - Message string - } - - 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) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } - } - - if r.Method == http.MethodGet { - render(CreateAccountResponse{}) - return - } - - if r.Method != http.MethodPost { - http.NotFound(w, r) - return - } - - err = r.ParseForm() - if err != nil { - render(CreateAccountResponse{ - Message: "Malformed data.", - }) - return - } - - type RegisterRequest struct { - Username string `json:"username"` - Email string `json:"email"` - Password string `json:"password"` - Invite string `json:"invite"` - } - credentials := RegisterRequest{ - Username: r.Form.Get("username"), - Email: r.Form.Get("email"), - Password: r.Form.Get("password"), - Invite: r.Form.Get("invite"), - } - - // make sure code exists in DB - invite, err := controller.GetInvite(global.DB, credentials.Invite) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve invite: %v\n", err) - render(CreateAccountResponse{ - Message: "Something went wrong. Please try again.", - }) - return - } - if invite == nil || time.Now().After(invite.ExpiresAt) { - if invite != nil { - err := controller.DeleteInvite(global.DB, invite.Code) - if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %v\n", err) } - } - render(CreateAccountResponse{ - Message: "Invalid invite code.", - }) - 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) - render(CreateAccountResponse{ - Message: "Something went wrong. Please try again.", - }) - return - } - - account := model.Account{ - Username: credentials.Username, - Password: string(hashedPassword), - Email: credentials.Email, - AvatarURL: "/img/default-avatar.png", - } - err = controller.CreateAccount(global.DB, &account) - if err != nil { - if strings.HasPrefix(err.Error(), "pq: duplicate key") { - 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{ - Message: "Something went wrong. Please try again.", - }) - return - } - - err = controller.DeleteInvite(global.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()) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to create token: %v\n", err) - // gracefully redirect user to login page - http.Redirect(w, r, "/admin/login", http.StatusFound) - return - } - - cookie := http.Cookie{} - cookie.Name = global.COOKIE_TOKEN - cookie.Value = token.Token - cookie.Expires = token.ExpiresAt - if strings.HasPrefix(global.Config.BaseUrl, "https") { - cookie.Secure = true - } - cookie.HttpOnly = true - cookie.Path = "/" - http.SetCookie(w, &cookie) - - err = pages["login"].Execute(w, TemplateData{ - Account: &account, - Token: token.Token, - }) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to render login page: %v\n", err) + fmt.Printf("Error rendering admin logout page: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } diff --git a/admin/releasehttp.go b/admin/releasehttp.go index 9132fe8..54d0fd6 100644 --- a/admin/releasehttp.go +++ b/admin/releasehttp.go @@ -15,8 +15,6 @@ func serveRelease() http.Handler { 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) if err != nil { if strings.Contains(err.Error(), "no rows") { @@ -28,6 +26,12 @@ func serveRelease() http.Handler { return } + authorised := GetSession(r) != nil + if !authorised && !release.Visible { + http.NotFound(w, r) + return + } + if len(slices) > 1 { switch slices[1] { case "editcredits": @@ -56,15 +60,7 @@ func serveRelease() http.Handler { return } - type ReleaseResponse struct { - Account *model.Account - Release *model.Release - } - - err = pages["release"].Execute(w, ReleaseResponse{ - Account: account, - Release: release, - }) + err = pages["release"].Execute(w, release) if err != nil { fmt.Printf("Error rendering admin release page for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) diff --git a/admin/static/admin.css b/admin/static/admin.css index 510ee8b..0d1269c 100644 --- a/admin/static/admin.css +++ b/admin/static/admin.css @@ -43,7 +43,7 @@ nav .title { color: inherit; } -.nav-item { +nav a { width: auto; height: 100%; @@ -53,17 +53,16 @@ nav .title { display: flex; line-height: 2em; + text-decoration: none; + + color: inherit; } -.nav-item:hover { +nav a:hover { background: #00000010; text-decoration: none; } -nav a { - text-decoration: none; - color: inherit; -} nav #logout { - /* margin-left: auto; */ + margin-left: auto; } main { @@ -115,10 +114,6 @@ a img.icon { margin: 0; } -.flex-fill { - flex-grow: 1; -} - @media screen and (max-width: 520px) { body { font-size: 12px; diff --git a/admin/templates.go b/admin/templates.go index e91313a..b7aaf9e 100644 --- a/admin/templates.go +++ b/admin/templates.go @@ -18,11 +18,6 @@ var pages = map[string]*template.Template{ filepath.Join("views", "prideflag.html"), filepath.Join("admin", "views", "login.html"), )), - "create-account": template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("admin", "views", "create-account.html"), - )), "logout": template.Must(template.ParseFiles( filepath.Join("admin", "views", "layout.html"), filepath.Join("views", "prideflag.html"), diff --git a/admin/trackhttp.go b/admin/trackhttp.go index 2cea123..148b7d8 100644 --- a/admin/trackhttp.go +++ b/admin/trackhttp.go @@ -32,19 +32,12 @@ func serveTrack() http.Handler { return } - type TrackResponse struct { - Account *model.Account - Track *model.Track + type Track struct { + *model.Track Releases []*model.Release } - account := r.Context().Value("account").(*model.Account) - - err = pages["track"].Execute(w, TrackResponse{ - Account: account, - Track: track, - Releases: releases, - }) + err = pages["track"].Execute(w, Track{ Track: track, Releases: releases }) 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/views/create-account.html b/admin/views/create-account.html deleted file mode 100644 index 5d92627..0000000 --- a/admin/views/create-account.html +++ /dev/null @@ -1,102 +0,0 @@ -{{define "head"}} -Register - ari melody 💫 - - - -{{end}} - -{{define "content"}} -
    - {{if .Message}} -

    {{.Message}}

    - {{end}} - - -
    - - - - - - - - - - - -
    - - - -
    -{{end}} diff --git a/admin/views/edit-artist.html b/admin/views/edit-artist.html index 8cd88f0..b54361a 100644 --- a/admin/views/edit-artist.html +++ b/admin/views/edit-artist.html @@ -1,6 +1,5 @@ {{define "head"}} -Editing {{.Artist.Name}} - ari melody 💫 - +Editing {{.Name}} - ari melody 💫 {{end}} @@ -9,20 +8,20 @@

    Editing Artist

    -
    +
    - +

    Name

    - +

    Website

    - +
    @@ -37,13 +36,13 @@ {{if .Credits}} {{range .Credits}}
    - +
    -

    {{.Artist.Release.Title}}

    -

    {{.Artist.Release.PrintArtists true true}}

    +

    {{.Release.Title}}

    +

    {{.Release.PrintArtists true true}}

    - Role: {{.Artist.Role}} - {{if .Artist.Primary}} + Role: {{.Role}} + {{if .Primary}} (Primary) {{end}}

    diff --git a/admin/views/edit-release.html b/admin/views/edit-release.html index 9c1ba99..d106bb5 100644 --- a/admin/views/edit-release.html +++ b/admin/views/edit-release.html @@ -1,6 +1,6 @@ {{define "head"}} -Editing {{.Release.Title}} - ari melody 💫 - +Editing {{.Title}} - ari melody 💫 + {{end}} @@ -8,21 +8,21 @@ {{define "content"}}
    -
    +
    - +

    - +

    Type - {{$t := .Release.ReleaseType}} + {{$t := .ReleaseType}} + >{{.Description}}
    Release Date - +
    Buy Name - +
    Buy Link - +
    Copyright - +
    Copyright URL - +
    Visible
    -

    Credits ({{len .Release.Credits}})

    +

    Credits ({{len .Credits}})

    Edit
    - {{range .Release.Credits}} + {{range .Credits}}
    @@ -122,37 +122,37 @@
    {{end}} - {{if not .Release.Credits}} + {{if not .Credits}}

    There are no credits.

    {{end}}
    -

    Links ({{len .Release.Links}})

    +

    Links ({{len .Links}})

    Edit
    -

    Tracklist ({{len .Release.Tracks}})

    +

    Tracklist ({{len .Tracks}})

    Edit
    - {{range $i, $track := .Release.Tracks}} + {{range $i, $track := .Tracks}}

    {{.Add $i 1}} diff --git a/admin/views/edit-track.html b/admin/views/edit-track.html index 5bb74eb..2b0fed5 100644 --- a/admin/views/edit-track.html +++ b/admin/views/edit-track.html @@ -8,30 +8,30 @@

    Editing Track

    -
    +

    Title

    - +

    Description

    + >{{.Description}}

    Lyrics

    + >{{.Lyrics}}
    diff --git a/admin/views/layout.html b/admin/views/layout.html index 0a33b72..ca9ce1f 100644 --- a/admin/views/layout.html +++ b/admin/views/layout.html @@ -17,22 +17,9 @@
    diff --git a/admin/views/login.html b/admin/views/login.html index 16c0fcc..f615746 100644 --- a/admin/views/login.html +++ b/admin/views/login.html @@ -10,65 +10,6 @@ p a { a.discord { color: #5865F2; } - -form { - width: 100%; - display: flex; - flex-direction: column; - align-items: center; -} - -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; -} -input[disabled] { - opacity: .5; - cursor: not-allowed; -} - -button { - padding: .5em .8em; - font-family: inherit; - font-size: inherit; - border-radius: .5em; - border: 1px solid #a0a0a0; - background: #f0f0f0; - color: inherit; -} -button.save { - background: #6fd7ff; - border-color: #6f9eb0; -} -button:hover { - background: #fff; - border-color: #d0d0d0; -} -button:active { - background: #d0d0d0; - border-color: #808080; -} {{end}} @@ -76,28 +17,15 @@ button:active {
    {{if .Token}} - +

    Logged in successfully. - You should be redirected to /admin soon. + You should be redirected to /admin in 5 seconds.

    {{else}} -
    -
    - - - - - - - - -
    - - -
    +

    Log in with Discord.

    {{end}}
    diff --git a/api/account.go b/api/account.go index 3ce52c8..37c4c7f 100644 --- a/api/account.go +++ b/api/account.go @@ -35,35 +35,25 @@ func handleLogin() http.HandlerFunc { account, err := controller.GetAccount(global.DB, credentials.Username) if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve account: %v\n", err) + 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: %s\n", err.Error()) 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)) + err = bcrypt.CompareHashAndPassword(account.Password, []byte(credentials.Password)) if err != nil { http.Error(w, "Invalid username or password", http.StatusBadRequest) return } - token, err := controller.CreateToken(global.DB, account.ID, r.UserAgent()) - type LoginResponse struct { - Token string `json:"token"` - ExpiresAt time.Time `json:"expires_at"` - } + // TODO: sessions and tokens - 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) - } + w.WriteHeader(http.StatusOK) + w.Write([]byte("Logged in successfully. TODO: Session tokens\n")) }) } @@ -78,7 +68,7 @@ func handleAccountRegistration() http.HandlerFunc { Username string `json:"username"` Email string `json:"email"` Password string `json:"password"` - Invite string `json:"invite"` + Code string `json:"code"` } credentials := RegisterRequest{} @@ -89,65 +79,50 @@ func handleAccountRegistration() http.HandlerFunc { } // make sure code exists in DB - invite, err := controller.GetInvite(global.DB, credentials.Invite) + invite := model.Invite{} + err = global.DB.Get(&invite, "SELECT * FROM invite WHERE code=$1", credentials.Code) if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve invite: %v\n", err) + if strings.Contains(err.Error(), "no rows") { + http.Error(w, "Invalid invite code", http.StatusBadRequest) + return + } + fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve invite: %s\n", err.Error()) 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(global.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) + _, err = global.DB.Exec("DELETE FROM invite WHERE code=$1", credentials.Code) + if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %s\n", err.Error()) } 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) + fmt.Fprintf(os.Stderr, "WARN: Failed to generate password hash: %s\n", err.Error()) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } account := model.Account{ Username: credentials.Username, - Password: string(hashedPassword), + Password: hashedPassword, Email: credentials.Email, AvatarURL: "/img/default-avatar.png", } err = controller.CreateAccount(global.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) + fmt.Fprintf(os.Stderr, "WARN: Failed to create account: %s\n", err.Error()) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - err = controller.DeleteInvite(global.DB, invite.Code) - if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %v\n", err) } + _, err = global.DB.Exec("DELETE FROM invite WHERE code=$1", credentials.Code) + if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %s\n", err.Error()) } - token, err := controller.CreateToken(global.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) - } + w.WriteHeader(http.StatusCreated) + w.Write([]byte("Account created successfully\n")) }) } @@ -176,22 +151,20 @@ func handleDeleteAccount() http.HandlerFunc { http.Error(w, "Invalid username or password", http.StatusBadRequest) return } - fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve account: %v\n", err) + fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve account: %s\n", err.Error()) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(credentials.Password)) + err = bcrypt.CompareHashAndPassword(account.Password, []byte(credentials.Password)) if err != nil { - http.Error(w, "Invalid password", http.StatusBadRequest) + http.Error(w, "Invalid username or password", http.StatusBadRequest) return } - // TODO: check TOTP - - err = controller.DeleteAccount(global.DB, account.Username) + err = controller.DeleteAccount(global.DB, account.ID) if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to delete account: %v\n", err) + fmt.Fprintf(os.Stderr, "WARN: Failed to delete account: %s\n", err.Error()) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } diff --git a/api/api.go b/api/api.go index af6c6a7..f7c1402 100644 --- a/api/api.go +++ b/api/api.go @@ -40,10 +40,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.MustAuthorise(UpdateArtist(artist)).ServeHTTP(w, r) case http.MethodDelete: // DELETE /api/v1/artist/{id} (admin) - admin.RequireAccount(global.DB, DeleteArtist(artist)).ServeHTTP(w, r) + admin.MustAuthorise(DeleteArtist(artist)).ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -55,7 +55,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.MustAuthorise(CreateArtist()).ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -82,10 +82,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.MustAuthorise(UpdateRelease(release)).ServeHTTP(w, r) case http.MethodDelete: // DELETE /api/v1/music/{id} (admin) - admin.RequireAccount(global.DB, DeleteRelease(release)).ServeHTTP(w, r) + admin.MustAuthorise(DeleteRelease(release)).ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -97,7 +97,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.MustAuthorise(CreateRelease()).ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -121,13 +121,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.MustAuthorise(ServeTrack(track)).ServeHTTP(w, r) case http.MethodPut: // PUT /api/v1/track/{id} (admin) - admin.RequireAccount(global.DB, UpdateTrack(track)).ServeHTTP(w, r) + admin.MustAuthorise(UpdateTrack(track)).ServeHTTP(w, r) case http.MethodDelete: // DELETE /api/v1/track/{id} (admin) - admin.RequireAccount(global.DB, DeleteTrack(track)).ServeHTTP(w, r) + admin.MustAuthorise(DeleteTrack(track)).ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -136,10 +136,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.MustAuthorise(ServeAllTracks()).ServeHTTP(w, r) case http.MethodPost: // POST /api/v1/track (admin) - admin.RequireAccount(global.DB, CreateTrack()).ServeHTTP(w, r) + admin.MustAuthorise(CreateTrack()).ServeHTTP(w, r) default: http.NotFound(w, r) } diff --git a/api/artist.go b/api/artist.go index c46db59..c793e23 100644 --- a/api/artist.go +++ b/api/artist.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "arimelody-web/admin" "arimelody-web/global" "arimelody-web/controller" "arimelody-web/model" @@ -20,7 +21,7 @@ func ServeAllArtists() http.Handler { var artists = []*model.Artist{} artists, err := controller.GetAllArtists(global.DB) if err != nil { - fmt.Printf("WARN: Failed to serve all artists: %s\n", err) + fmt.Printf("FATAL: Failed to serve all artists: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -52,17 +53,12 @@ func ServeArtist(artist *model.Artist) http.Handler { } ) - account, err := controller.GetAccountByRequest(global.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) - return - } - show_hidden_releases := account != nil + show_hidden_releases := admin.GetSession(r) != nil + var dbCredits []*model.Credit dbCredits, err := controller.GetArtistCredits(global.DB, artist.ID, show_hidden_releases) if err != nil { - fmt.Printf("WARN: Failed to retrieve artist credits for %s: %v\n", artist.ID, err) + fmt.Printf("FATAL: Failed to retrieve artist credits for %s: %s\n", artist.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -113,7 +109,7 @@ func CreateArtist() http.Handler { http.Error(w, fmt.Sprintf("Artist %s already exists\n", artist.ID), http.StatusBadRequest) return } - fmt.Printf("WARN: Failed to create artist %s: %s\n", artist.ID, err) + fmt.Printf("FATAL: Failed to create artist %s: %s\n", artist.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -126,7 +122,7 @@ func UpdateArtist(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 { - fmt.Printf("WARN: Failed to update artist: %s\n", err) + fmt.Printf("FATAL: Failed to update artist: %s\n", err) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } @@ -161,7 +157,7 @@ func UpdateArtist(artist *model.Artist) http.Handler { http.NotFound(w, r) return } - fmt.Printf("WARN: Failed to update artist %s: %s\n", artist.ID, err) + fmt.Printf("FATAL: Failed to update artist %s: %s\n", artist.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) @@ -175,7 +171,7 @@ func DeleteArtist(artist *model.Artist) http.Handler { http.NotFound(w, r) return } - fmt.Printf("WARN: Failed to delete artist %s: %s\n", artist.ID, err) + fmt.Printf("FATAL: Failed to delete artist %s: %s\n", artist.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) diff --git a/api/release.go b/api/release.go index d17fb5f..2288153 100644 --- a/api/release.go +++ b/api/release.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "arimelody-web/admin" "arimelody-web/global" "arimelody-web/controller" "arimelody-web/model" @@ -18,23 +19,10 @@ import ( func ServeRelease(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) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %v\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - if account != nil { - // TODO: check privilege on release - privileged = true - } - - if !privileged { - http.NotFound(w, r) - return - } + authorised := admin.GetSession(r) != nil + if !authorised && !release.Visible { + http.NotFound(w, r) + return } type ( @@ -65,18 +53,18 @@ func ServeRelease(release *model.Release) http.Handler { Links: make(map[string]string), } - if release.IsReleased() || privileged { + if authorised || release.IsReleased() { // get credits credits, err := controller.GetReleaseCredits(global.DB, release.ID) if err != nil { - fmt.Printf("WARN: Failed to serve release %s: Credits: %s\n", release.ID, err) + fmt.Printf("FATAL: 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) if err != nil { - fmt.Printf("WARN: Failed to serve release %s: Artists: %s\n", release.ID, err) + fmt.Printf("FATAL: Failed to serve release %s: Artists: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -91,7 +79,7 @@ func ServeRelease(release *model.Release) http.Handler { // get tracks tracks, err := controller.GetReleaseTracks(global.DB, release.ID) if err != nil { - fmt.Printf("WARN: Failed to serve release %s: Tracks: %s\n", release.ID, err) + fmt.Printf("FATAL: Failed to serve release %s: Tracks: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -106,7 +94,7 @@ func ServeRelease(release *model.Release) http.Handler { // get links links, err := controller.GetReleaseLinks(global.DB, release.ID) if err != nil { - fmt.Printf("WARN: Failed to serve release %s: Links: %s\n", release.ID, err) + fmt.Printf("FATAL: Failed to serve release %s: Links: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -146,24 +134,11 @@ func ServeCatalog() http.Handler { } catalog := []Release{} - account, err := controller.GetAccountByRequest(global.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) - return - } + authorised := admin.GetSession(r) != nil for _, release := range releases { - if !release.Visible { - privileged := false - if account != nil { - // TODO: check privilege on release - privileged = true - } - if !privileged { - continue - } + if !release.Visible && !authorised { + continue } - artists := []string{} for _, credit := range release.Credits { if !credit.Primary { continue } @@ -226,7 +201,7 @@ func CreateRelease() http.Handler { http.Error(w, fmt.Sprintf("Release %s already exists\n", release.ID), http.StatusBadRequest) return } - fmt.Printf("WARN: Failed to create release %s: %s\n", release.ID, err) + fmt.Printf("FATAL: Failed to create release %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } @@ -306,7 +281,7 @@ func UpdateRelease(release *model.Release) http.Handler { http.NotFound(w, r) return } - fmt.Printf("WARN: Failed to update release %s: %s\n", release.ID, err) + fmt.Printf("FATAL: Failed to update release %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) @@ -327,7 +302,7 @@ func UpdateReleaseTracks(release *model.Release) http.Handler { http.NotFound(w, r) return } - fmt.Printf("WARN: Failed to update tracks for %s: %s\n", release.ID, err) + fmt.Printf("FATAL: Failed to update tracks for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) @@ -368,7 +343,7 @@ func UpdateReleaseCredits(release *model.Release) http.Handler { http.NotFound(w, r) return } - fmt.Printf("WARN: Failed to update links for %s: %s\n", release.ID, err) + fmt.Printf("FATAL: Failed to update links for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) @@ -394,7 +369,7 @@ func UpdateReleaseLinks(release *model.Release) http.Handler { http.NotFound(w, r) return } - fmt.Printf("WARN: Failed to update links for %s: %s\n", release.ID, err) + fmt.Printf("FATAL: Failed to update links for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) @@ -408,7 +383,7 @@ func DeleteRelease(release *model.Release) http.Handler { http.NotFound(w, r) return } - fmt.Printf("WARN: Failed to delete release %s: %s\n", release.ID, err) + fmt.Printf("FATAL: Failed to delete release %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) diff --git a/api/track.go b/api/track.go index ebbaa10..f6d5578 100644 --- a/api/track.go +++ b/api/track.go @@ -28,7 +28,7 @@ func ServeAllTracks() http.Handler { var dbTracks = []*model.Track{} dbTracks, err := controller.GetAllTracks(global.DB) if err != nil { - fmt.Printf("WARN: Failed to pull tracks from DB: %s\n", err) + fmt.Printf("FATAL: Failed to pull tracks from DB: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } @@ -44,7 +44,7 @@ func ServeAllTracks() http.Handler { encoder.SetIndent("", "\t") err = encoder.Encode(tracks) if err != nil { - fmt.Printf("WARN: Failed to serve all tracks: %s\n", err) + fmt.Printf("FATAL: Failed to serve all tracks: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) @@ -54,7 +54,7 @@ func ServeTrack(track *model.Track) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { dbReleases, err := controller.GetTrackReleases(global.DB, track.ID, false) if err != nil { - fmt.Printf("WARN: Failed to pull track releases for %s from DB: %s\n", track.ID, err) + fmt.Printf("FATAL: Failed to pull track releases for %s from DB: %s\n", track.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } @@ -68,7 +68,7 @@ func ServeTrack(track *model.Track) http.Handler { encoder.SetIndent("", "\t") err = encoder.Encode(Track{ track, releases }) if err != nil { - fmt.Printf("WARN: Failed to serve track %s: %s\n", track.ID, err) + fmt.Printf("FATAL: Failed to serve track %s: %s\n", track.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) @@ -95,7 +95,7 @@ func CreateTrack() http.Handler { id, err := controller.CreateTrack(global.DB, &track) if err != nil { - fmt.Printf("WARN: Failed to create track: %s\n", err) + fmt.Printf("FATAL: Failed to create track: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } diff --git a/controller/account.go b/controller/account.go index 362e297..a34cd27 100644 --- a/controller/account.go +++ b/controller/account.go @@ -1,12 +1,8 @@ package controller import ( - "arimelody-web/global" "arimelody-web/model" - "errors" - "fmt" - "net/http" - "strings" + "math/rand" "github.com/jmoiron/sqlx" ) @@ -16,9 +12,6 @@ func GetAccount(db *sqlx.DB, username string) (*model.Account, error) { err := db.Get(&account, "SELECT * FROM account WHERE username=$1", username) if err != nil { - if strings.Contains(err.Error(), "no rows") { - return nil, nil - } return nil, err } @@ -30,98 +23,49 @@ func GetAccountByEmail(db *sqlx.DB, email string) (*model.Account, error) { err := db.Get(&account, "SELECT * FROM account WHERE email=$1", email) if err != nil { - if strings.Contains(err.Error(), "no rows") { - return nil, nil - } return nil, err } return &account, nil } -func GetAccountByToken(db *sqlx.DB, token string) (*model.Account, error) { - if token == "" { return nil, nil } - - account := model.Account{} - - err := db.Get(&account, "SELECT account.* FROM account JOIN token ON id=account WHERE token=$1", token) - if err != nil { - if strings.Contains(err.Error(), "no rows") { - return nil, nil - } - return nil, err - } - - return &account, nil -} - -func GetTokenFromRequest(db *sqlx.DB, r *http.Request) string { - tokenStr := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") - if len(tokenStr) > 0 { - return tokenStr - } - - cookie, err := r.Cookie(global.COOKIE_TOKEN) - if err != nil { - return "" - } - return cookie.Value -} - -func GetAccountByRequest(db *sqlx.DB, r *http.Request) (*model.Account, error) { - tokenStr := GetTokenFromRequest(db, r) - - token, err := GetToken(db, tokenStr) - if err != nil { - if strings.Contains(err.Error(), "no rows") { - return nil, nil - } - return nil, errors.New("GetToken: " + err.Error()) - } - - // does user-agent match the token? - if r.UserAgent() != token.UserAgent { - // invalidate the token - DeleteToken(db, tokenStr) - fmt.Printf("WARN: Attempted use of token by unauthorised User-Agent (Expected `%s`, got `%s`)\n", token.UserAgent, r.UserAgent()) - // TODO: log unauthorised activity to the user - return nil, errors.New("User agent mismatch") - } - - return GetAccountByToken(db, tokenStr) -} - func CreateAccount(db *sqlx.DB, account *model.Account) error { - err := db.Get( - &account.ID, - "INSERT INTO account (username, password, email, avatar_url) " + - "VALUES ($1, $2, $3, $4) " + - "RETURNING id", - account.Username, - account.Password, - account.Email, - account.AvatarURL, - ) + _, err := db.Exec( + "INSERT INTO account (username, password, email, avatar_url) " + + "VALUES ($1, $2, $3, $4)", + account.Username, + account.Password, + account.Email, + account.AvatarURL) return err } func UpdateAccount(db *sqlx.DB, account *model.Account) error { _, err := db.Exec( - "UPDATE account " + - "SET username=$2, password=$3, email=$4, avatar_url=$5) " + - "WHERE id=$1", - account.ID, - account.Username, - account.Password, - account.Email, - account.AvatarURL, - ) + "UPDATE account " + + "SET username=$2, password=$3, email=$4, avatar_url=$5) " + + "WHERE id=$1", + account.ID, + account.Username, + account.Password, + account.Email, + account.AvatarURL) return err } -func DeleteAccount(db *sqlx.DB, username string) error { - _, err := db.Exec("DELETE FROM account WHERE username=$1", username) +func DeleteAccount(db *sqlx.DB, accountID string) error { + _, err := db.Exec("DELETE FROM account WHERE id=$1", accountID) return err } + +var inviteChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + +func GenerateInviteCode(length int) []byte { + code := []byte{} + for i := 0; i < length; i++ { + code = append(code, inviteChars[rand.Intn(len(inviteChars) - 1)]) + } + return code +} diff --git a/controller/controller.go b/controller/controller.go deleted file mode 100644 index 44194e4..0000000 --- a/controller/controller.go +++ /dev/null @@ -1,13 +0,0 @@ -package controller - -import "math/rand" - -func GenerateAlnumString(length int) []byte { - const CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - res := []byte{} - for i := 0; i < length; i++ { - res = append(res, CHARS[rand.Intn(len(CHARS))]) - } - return res -} - diff --git a/controller/invite.go b/controller/invite.go deleted file mode 100644 index f30db64..0000000 --- a/controller/invite.go +++ /dev/null @@ -1,67 +0,0 @@ -package controller - -import ( - "arimelody-web/model" - "math/rand" - "strings" - "time" - - "github.com/jmoiron/sqlx" -) - -var inviteChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") - -func GetInvite(db *sqlx.DB, code string) (*model.Invite, error) { - invite := model.Invite{} - - err := db.Get(&invite, "SELECT * FROM invite WHERE code=$1", code) - if err != nil { - if strings.Contains(err.Error(), "no rows") { - return nil, nil - } - return nil, err - } - - return &invite, nil -} - -func CreateInvite(db *sqlx.DB, length int, lifetime time.Duration) (*model.Invite, error) { - invite := model.Invite{ - CreatedAt: time.Now(), - ExpiresAt: time.Now().Add(lifetime), - } - - code := []byte{} - for i := 0; i < length; i++ { - code = append(code, inviteChars[rand.Intn(len(inviteChars) - 1)]) - } - invite.Code = string(code) - - _, err := db.Exec( - "INSERT INTO invite (code, created_at, expires_at) " + - "VALUES ($1, $2, $3)", - invite.Code, - invite.CreatedAt, - invite.ExpiresAt, - ) - if err != nil { - return nil, err - } - - return &invite, nil -} - -func DeleteInvite(db *sqlx.DB, code string) error { - _, err := db.Exec("DELETE FROM invite WHERE code=$1", code) - return err -} - -func DeleteAllInvites(db *sqlx.DB) error { - _, err := db.Exec("DELETE FROM invite") - return err -} - -func DeleteExpiredInvites(db *sqlx.DB) error { - _, err := db.Exec("DELETE FROM invite WHERE expires_atcurrent_timestamp", accountID) - return tokens, err -} - -func DeleteAllTokensForAccount(db *sqlx.DB, accountID string) error { - _, err := db.Exec("DELETE FROM token WHERE account=$1", accountID) - return err -} - -func DeleteToken(db *sqlx.DB, token string) error { - _, err := db.Exec("DELETE FROM token WHERE token=$1", token) - return err -} - diff --git a/global/config.go b/global/config.go index 0115c3f..d8c9f3d 100644 --- a/global/config.go +++ b/global/config.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "strconv" + "strings" "github.com/jmoiron/sqlx" "github.com/pelletier/go-toml/v2" @@ -56,13 +57,13 @@ 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) + fmt.Fprintf(os.Stderr, "FATAL: Failed to parse configuration file: %s\n", err.Error()) os.Exit(1) } err = handleConfigOverrides(&config) if err != nil { - fmt.Fprintf(os.Stderr, "FATAL: Failed to parse environment variable %v\n", err) + fmt.Fprintf(os.Stderr, "FATAL: Failed to parse environment variable %s\n", err.Error()) os.Exit(1) } @@ -91,4 +92,30 @@ func handleConfigOverrides(config *config) error { return nil } +var Args = func() map[string]string { + args := map[string]string{} + + index := 0 + for index < len(os.Args[1:]) { + arg := os.Args[index + 1] + if !strings.HasPrefix(arg, "-") { + fmt.Printf("FATAL: Parameters must follow an argument (%s).\n", arg) + os.Exit(1) + } + + if index + 3 > len(os.Args) || strings.HasPrefix(os.Args[index + 2], "-") { + args[arg[1:]] = "true" + index += 1 + continue + } + + val := os.Args[index + 2] + args[arg[1:]] = val + // fmt.Printf("%s: %s\n", arg[1:], val) + index += 2 + } + + return args +}() + var DB *sqlx.DB 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 index 49edb01..c0462db 100644 --- a/global/funcs.go +++ b/global/funcs.go @@ -58,12 +58,12 @@ func DefaultHeaders(next http.Handler) http.Handler { type LoggingResponseWriter struct { http.ResponseWriter - Status int + Code int } -func (lrw *LoggingResponseWriter) WriteHeader(status int) { - lrw.Status = status - lrw.ResponseWriter.WriteHeader(status) +func (lrw *LoggingResponseWriter) WriteHeader(code int) { + lrw.Code = code + lrw.ResponseWriter.WriteHeader(code) } func HTTPLog(next http.Handler) http.Handler { @@ -81,21 +81,22 @@ func HTTPLog(next http.Handler) http.Handler { elapsed = strconv.Itoa(difference) } - statusColour := colour.Reset + codeColour := 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 } + if lrw.Code - 600 <= 0 { codeColour = colour.Red } + if lrw.Code - 500 <= 0 { codeColour = colour.Yellow } + if lrw.Code - 400 <= 0 { codeColour = colour.White } + if lrw.Code - 300 <= 0 { codeColour = 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, + codeColour, + lrw.Code, colour.Reset, elapsed, r.Header["User-Agent"][0]) }) } + diff --git a/main.go b/main.go index 2f0cb43..7962869 100644 --- a/main.go +++ b/main.go @@ -7,28 +7,22 @@ import ( "net/http" "os" "path/filepath" - "strings" "time" "arimelody-web/admin" "arimelody-web/api" - "arimelody-web/controller" "arimelody-web/global" - "arimelody-web/templates" "arimelody-web/view" + "arimelody-web/controller" + "arimelody-web/templates" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" ) -// used for database migrations -const DB_VERSION = 1 - const DEFAULT_PORT int64 = 8080 func main() { - fmt.Printf("made with <3 by ari melody\n\n") - // initialise database connection if env := os.Getenv("ARIMELODY_DB_HOST"); env != "" { global.Config.DB.Host = env } if env := os.Getenv("ARIMELODY_DB_NAME"); env != "" { global.Config.DB.Name = env } @@ -71,107 +65,39 @@ func main() { global.DB.SetMaxIdleConns(10) defer global.DB.Close() - // handle command arguments - if len(os.Args) > 1 { - arg := os.Args[1] - - switch arg { - case "createInvite": - fmt.Printf("Creating invite...\n") - invite, err := controller.CreateInvite(global.DB, 16, time.Hour * 24) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to create invite code: %v\n", err) - os.Exit(1) - } - - fmt.Printf("Here you go! This code expires in 24 hours: %s\n", invite.Code) - return - - case "purgeInvites": - fmt.Printf("Deleting all invites...\n") - err := controller.DeleteAllInvites(global.DB) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to delete invites: %v\n", err) - os.Exit(1) - } - - fmt.Printf("Invites deleted successfully.\n") - return - - case "deleteAccount": - if len(os.Args) < 2 { - fmt.Fprintf(os.Stderr, "FATAL: Account name not specified for -deleteAccount\n") - os.Exit(1) - } - username := os.Args[2] - fmt.Printf("Deleting account \"%s\"...\n", username) - - account, err := controller.GetAccount(global.DB, username) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to fetch account \"%s\": %s\n", username, err.Error()) - os.Exit(1) - } - - if account == nil { - fmt.Fprintf(os.Stderr, "Account \"%s\" does not exist.\n", username) - os.Exit(1) - } - - fmt.Printf("You are about to delete \"%s\". Are you sure? (y/[N]): ", account.Username) - res := "" - fmt.Scanln(&res) - if !strings.HasPrefix(res, "y") { - return - } - - err = controller.DeleteAccount(global.DB, username) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to delete account: %v\n", err) - os.Exit(1) - } - - fmt.Printf("Account \"%s\" deleted successfully.\n", account.Username) - return - - } - - fmt.Printf( - "Available commands:\n\n" + - "createInvite:\n\tCreates an invite code to register new accounts.\n" + - "purgeInvites:\n\tDeletes all available invite codes.\n" + - "deleteAccount :\n\tDeletes an account with a given `username`.\n", - ) - return + _, err = global.DB.Exec("DELETE FROM invite WHERE expires_at < CURRENT_TIMESTAMP") + if err != nil { + fmt.Fprintf(os.Stderr, "FATAL: Failed to clear expired invite codes: %v\n", err) + os.Exit(1) } - // handle DB migrations - controller.CheckDBVersionAndMigrate(global.DB) - - // initial invite code accountsCount := 0 - err = global.DB.Get(&accountsCount, "SELECT count(*) FROM account") - if err != nil { panic(err) } + global.DB.Get(&accountsCount, "SELECT count(*) FROM account") if accountsCount == 0 { - _, err := global.DB.Exec("DELETE FROM invite") + code := controller.GenerateInviteCode(8) + + tx, err := global.DB.Begin() + if err != nil { + fmt.Fprintf(os.Stderr, "FATAL: Failed to begin transaction: %v\n", err) + os.Exit(1) + } + _, err = tx.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) + _, err = tx.Exec("INSERT INTO invite (code,expires_at) VALUES ($1, $2)", code, time.Now().Add(60 * time.Minute)) if err != nil { - fmt.Fprintf(os.Stderr, "FATAL: Failed to create invite code: %v\n", err) + fmt.Fprintf(os.Stderr, "FATAL: Failed to create invite codes: %v\n", err) + os.Exit(1) + } + err = tx.Commit() + if err != nil { + fmt.Fprintf(os.Stderr, "FATAL: Failed to create invite codes: %v\n", err) os.Exit(1) } - fmt.Fprintf(os.Stdout, "No accounts exist! Generated invite code: " + string(invite.Code) + "\nUse this at %s/admin/register.\n", global.Config.BaseUrl) - } - - // delete expired invites - err = controller.DeleteExpiredInvites(global.DB) - if err != nil { - fmt.Fprintf(os.Stderr, "FATAL: Failed to clear expired invite codes: %v\n", err) - os.Exit(1) + fmt.Fprintln(os.Stdout, "INFO: No accounts exist! Generated invite code: " + string(code) + " (Use this at /register or /api/v1/register)") } // start the web server! @@ -183,6 +109,131 @@ func main() { )) } +func initDB(driverName string, dataSourceName string) (*sqlx.DB, error) { + db, err := sqlx.Connect(driverName, dataSourceName) + if err != nil { return nil, err } + + // ensure tables exist + // account + _, err = db.Exec( + "CREATE TABLE IF NOT EXISTS account (" + + "id uuid PRIMARY KEY DEFAULT gen_random_uuid(), " + + "username text NOT NULL UNIQUE, " + + "password text NOT NULL, " + + "email text, " + + "avatar_url text)", + ) + if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create account table: %s", err.Error())) } + + // privilege + _, err = db.Exec( + "CREATE TABLE IF NOT EXISTS privilege (" + + "account uuid NOT NULL, " + + "privilege text NOT NULL, " + + "CONSTRAINT privilege_pk PRIMARY KEY (account, privilege), " + + "CONSTRAINT privilege_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE)", + ) + if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create privilege table: %s", err.Error())) } + + // totp + _, err = db.Exec( + "CREATE TABLE IF NOT EXISTS totp (" + + "account uuid NOT NULL, " + + "name text NOT NULL, " + + "created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + "CONSTRAINT totp_pk PRIMARY KEY (account, name), " + + "CONSTRAINT totp_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE)", + ) + if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create TOTP table: %s", err.Error())) } + + // invites + _, err = db.Exec( + "CREATE TABLE IF NOT EXISTS invite (" + + "code text NOT NULL PRIMARY KEY, " + + "created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + + "expires_at TIMESTAMP NOT NULL)", + ) + if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create TOTP table: %s", err.Error())) } + + // artist + _, err = db.Exec( + "CREATE TABLE IF NOT EXISTS artist (" + + "id character varying(64) PRIMARY KEY, " + + "name text NOT NULL, " + + "website text, " + + "avatar text)", + ) + if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create artist table: %s", err.Error())) } + + // musicrelease + _, err = db.Exec( + "CREATE TABLE IF NOT EXISTS musicrelease (" + + "id character varying(64) PRIMARY KEY, " + + "visible bool DEFAULT false, " + + "title text NOT NULL, " + + "description text, " + + "type text, " + + "release_date TIMESTAMP NOT NULL, " + + "artwork text, " + + "buyname text, " + + "buylink text, " + + "copyright text, " + + "copyrightURL text)", + ) + if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create musicrelease table: %s", err.Error())) } + + // musiclink + _, err = db.Exec( + "CREATE TABLE IF NOT EXISTS public.musiclink (" + + "release character varying(64) NOT NULL, " + + "name text NOT NULL, " + + "url text NOT NULL, " + + "CONSTRAINT musiclink_pk PRIMARY KEY (release, name), " + + "CONSTRAINT musiclink_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE)", + ) + if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create musiclink table: %s", err.Error())) } + + // musiccredit + _, err = db.Exec( + "CREATE TABLE IF NOT EXISTS public.musiccredit (" + + "release character varying(64) NOT NULL, " + + "artist character varying(64) NOT NULL, " + + "role text NOT NULL, " + + "is_primary boolean DEFAULT false, " + + "CONSTRAINT musiccredit_pk PRIMARY KEY (release, artist), " + + "CONSTRAINT musiccredit_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE, " + + "CONSTRAINT musiccredit_artist_fk FOREIGN KEY (artist) REFERENCES artist(id) ON DELETE CASCADE)", + ) + if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create musiccredit table: %s", err.Error())) } + + // musictrack + _, err = db.Exec( + "CREATE TABLE IF NOT EXISTS public.musictrack (" + + "id uuid DEFAULT gen_random_uuid() PRIMARY KEY, " + + "title text NOT NULL, " + + "description text, " + + "lyrics text, " + + "preview_url text)", + ) + if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create musictrack table: %s", err.Error())) } + + // musicreleasetrack + _, err = db.Exec( + "CREATE TABLE IF NOT EXISTS public.musicreleasetrack (" + + "release character varying(64) NOT NULL, " + + "track uuid NOT NULL, " + + "number integer NOT NULL, " + + "CONSTRAINT musicreleasetrack_pk PRIMARY KEY (release, track), " + + "CONSTRAINT musicreleasetrack_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE, " + + "CONSTRAINT musicreleasetrack_artist_fk FOREIGN KEY (track) REFERENCES track(id) ON DELETE CASCADE)", + ) + if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create musicreleasetrack table: %s", err.Error())) } + + // TODO: automatic database migration + + return db, nil +} + func createServeMux() *http.ServeMux { mux := http.NewServeMux() diff --git a/model/account.go b/model/account.go index 03e95c5..16b0e3b 100644 --- a/model/account.go +++ b/model/account.go @@ -1,16 +1,27 @@ package model +import ( + "time" +) + type ( Account struct { ID string `json:"id" db:"id"` Username string `json:"username" db:"username"` - Password string `json:"password" db:"password"` + Password []byte `json:"password" db:"password"` Email string `json:"email" db:"email"` AvatarURL string `json:"avatar_url" db:"avatar_url"` Privileges []AccountPrivilege `json:"privileges"` } AccountPrivilege string + + Invite struct { + Code string `db:"code"` + CreatedByID string `db:"created_by"` + CreatedAt time.Time `db:"created_at"` + ExpiresAt time.Time `db:"expires_at"` + } ) const ( diff --git a/model/invite.go b/model/invite.go deleted file mode 100644 index b7a66ae..0000000 --- a/model/invite.go +++ /dev/null @@ -1,10 +0,0 @@ -package model - -import "time" - -type Invite struct { - Code string `db:"code"` - CreatedByID string `db:"created_by"` - CreatedAt time.Time `db:"created_at"` - ExpiresAt time.Time `db:"expires_at"` -} diff --git a/model/token.go b/model/token.go deleted file mode 100644 index 31beb72..0000000 --- a/model/token.go +++ /dev/null @@ -1,11 +0,0 @@ -package model - -import "time" - -type Token struct { - Token string `json:"token" db:"token"` - AccountID string `json:"-" db:"account"` - UserAgent string `json:"user_agent" db:"user_agent"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - ExpiresAt time.Time `json:"expires_at" db:"expires_at"` -} diff --git a/schema_migration/000-init.sql b/schema.sql similarity index 86% rename from schema_migration/000-init.sql rename to schema.sql index cd11a5e..b5d92a3 100644 --- a/schema_migration/000-init.sql +++ b/schema.sql @@ -1,16 +1,8 @@ -CREATE SCHEMA arimelody; - --- Schema verison -CREATE TABLE arimelody.schema_version ( - version INTEGER PRIMARY KEY, - applied_at TIMESTAMP DEFAULT current_timestamp -); +CREATE SCHEMA arimelody AUTHORIZATION arimelody; -- --- Tables +-- Acounts -- - --- Accounts CREATE TABLE arimelody.account ( id uuid DEFAULT gen_random_uuid(), username text NOT NULL UNIQUE, @@ -20,14 +12,18 @@ CREATE TABLE arimelody.account ( ); ALTER TABLE arimelody.account ADD CONSTRAINT account_pk PRIMARY KEY (id); +-- -- Privilege +-- CREATE TABLE arimelody.privilege ( account uuid NOT NULL, privilege text NOT NULL ); ALTER TABLE arimelody.privilege ADD CONSTRAINT privilege_pk PRIMARY KEY (account, privilege); +-- -- TOTP +-- CREATE TABLE arimelody.totp ( account uuid NOT NULL, name text NOT NULL, @@ -35,7 +31,9 @@ CREATE TABLE arimelody.totp ( ); ALTER TABLE arimelody.totp ADD CONSTRAINT totp_pk PRIMARY KEY (account, name); +-- -- Invites +-- CREATE TABLE arimelody.invite ( code text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -43,18 +41,9 @@ CREATE TABLE arimelody.invite ( ); ALTER TABLE arimelody.invite ADD CONSTRAINT invite_pk PRIMARY KEY (code); --- Tokens -CREATE TABLE arimelody.token ( - token TEXT, - account UUID NOT NULL, - user_agent TEXT NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT current_timestamp, - expires_at TIMESTAMP DEFAULT NULL -); -ALTER TABLE arimelody.token ADD CONSTRAINT token_pk PRIMARY KEY (token); - - +-- -- Artists (should be applicable to all art) +-- CREATE TABLE arimelody.artist ( id character varying(64), name text NOT NULL, @@ -63,7 +52,9 @@ CREATE TABLE arimelody.artist ( ); ALTER TABLE arimelody.artist ADD CONSTRAINT artist_pk PRIMARY KEY (id); +-- -- Music releases +-- CREATE TABLE arimelody.musicrelease ( id character varying(64) NOT NULL, visible bool DEFAULT false, @@ -79,7 +70,9 @@ CREATE TABLE arimelody.musicrelease ( ); ALTER TABLE arimelody.musicrelease ADD CONSTRAINT musicrelease_pk PRIMARY KEY (id); +-- -- Music links (external platform links under a release) +-- CREATE TABLE arimelody.musiclink ( release character varying(64) NOT NULL, name text NOT NULL, @@ -87,7 +80,9 @@ CREATE TABLE arimelody.musiclink ( ); ALTER TABLE arimelody.musiclink ADD CONSTRAINT musiclink_pk PRIMARY KEY (release, name); +-- -- Music credits (artist credits under a release) +-- CREATE TABLE arimelody.musiccredit ( release character varying(64) NOT NULL, artist character varying(64) NOT NULL, @@ -96,7 +91,9 @@ CREATE TABLE arimelody.musiccredit ( ); ALTER TABLE arimelody.musiccredit ADD CONSTRAINT musiccredit_pk PRIMARY KEY (release, artist); +-- -- Music tracks (tracks under a release) +-- CREATE TABLE arimelody.musictrack ( id uuid DEFAULT gen_random_uuid(), title text NOT NULL, @@ -106,7 +103,9 @@ CREATE TABLE arimelody.musictrack ( ); ALTER TABLE arimelody.musictrack ADD CONSTRAINT musictrack_pk PRIMARY KEY (id); +-- -- Music release/track pairs +-- CREATE TABLE arimelody.musicreleasetrack ( release character varying(64) NOT NULL, track uuid NOT NULL, @@ -114,15 +113,11 @@ CREATE TABLE arimelody.musicreleasetrack ( ); ALTER TABLE arimelody.musicreleasetrack ADD CONSTRAINT musicreleasetrack_pk PRIMARY KEY (release, track); - - -- -- Foreign keys -- - ALTER TABLE arimelody.privilege ADD CONSTRAINT privilege_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; ALTER TABLE arimelody.totp ADD CONSTRAINT totp_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; -ALTER TABLE arimelody.token ADD CONSTRAINT token_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; ALTER TABLE arimelody.musiccredit ADD CONSTRAINT musiccredit_artist_fk FOREIGN KEY (artist) REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE arimelody.musiccredit ADD CONSTRAINT musiccredit_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE; diff --git a/schema_migration/001-pre-versioning.sql b/schema_migration/001-pre-versioning.sql deleted file mode 100644 index fc730a0..0000000 --- a/schema_migration/001-pre-versioning.sql +++ /dev/null @@ -1,65 +0,0 @@ --- --- Migration --- - --- Move existing tables to new schema -ALTER TABLE public.artist SET SCHEMA arimelody; -ALTER TABLE public.musicrelease SET SCHEMA arimelody; -ALTER TABLE public.musiclink SET SCHEMA arimelody; -ALTER TABLE public.musiccredit SET SCHEMA arimelody; -ALTER TABLE public.musictrack SET SCHEMA arimelody; -ALTER TABLE public.musicreleasetrack SET SCHEMA arimelody; - - - --- --- New items --- - --- Acounts -CREATE TABLE arimelody.account ( - id uuid DEFAULT gen_random_uuid(), - username text NOT NULL UNIQUE, - password text NOT NULL, - email text, - avatar_url text -); -ALTER TABLE arimelody.account ADD CONSTRAINT account_pk PRIMARY KEY (id); - --- Privilege -CREATE TABLE arimelody.privilege ( - account uuid NOT NULL, - privilege text NOT NULL -); -ALTER TABLE arimelody.privilege ADD CONSTRAINT privilege_pk PRIMARY KEY (account, privilege); - --- TOTP -CREATE TABLE arimelody.totp ( - account uuid NOT NULL, - name text NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); -ALTER TABLE arimelody.totp ADD CONSTRAINT totp_pk PRIMARY KEY (account, name); - --- Invites -CREATE TABLE arimelody.invite ( - code text NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - expires_at TIMESTAMP NOT NULL -); -ALTER TABLE arimelody.invite ADD CONSTRAINT invite_pk PRIMARY KEY (code); - --- Tokens -CREATE TABLE arimelody.token ( - token TEXT, - account UUID NOT NULL, - user_agent TEXT NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT current_timestamp, - expires_at TIMESTAMP DEFAULT NULL -); -ALTER TABLE arimelody.token ADD CONSTRAINT token_pk PRIMARY KEY (token); - --- Foreign keys -ALTER TABLE arimelody.privilege ADD CONSTRAINT privilege_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; -ALTER TABLE arimelody.totp ADD CONSTRAINT totp_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; -ALTER TABLE arimelody.token ADD CONSTRAINT token_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; diff --git a/view/music.go b/view/music.go index 3799182..f38e1c2 100644 --- a/view/music.go +++ b/view/music.go @@ -3,8 +3,8 @@ package view import ( "fmt" "net/http" - "os" + "arimelody-web/admin" "arimelody-web/controller" "arimelody-web/global" "arimelody-web/model" @@ -59,28 +59,15 @@ func ServeCatalog() http.Handler { func ServeGateway(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) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %v\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - if account != nil { - // TODO: check privilege on release - privileged = true - } - - if !privileged { - http.NotFound(w, r) - return - } + authorised := admin.GetSession(r) != nil + if !authorised && !release.Visible { + http.NotFound(w, r) + return } response := *release - if release.IsReleased() || privileged { + if authorised || release.IsReleased() { response.Tracks = release.Tracks response.Credits = release.Credits response.Links = release.Links