diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index a882442..0000000 --- a/.editorconfig +++ /dev/null @@ -1,7 +0,0 @@ -root = true - -[*] -end_of_line = lf -insert_final_newline = true -indent_style = space -indent_size = 4 diff --git a/.gitignore b/.gitignore index 2e63958..9bdf788 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,3 @@ docker-compose*.yml !docker-compose.example.yml config*.toml arimelody-web -arimelody-web.tar.gz diff --git a/Makefile b/Makefile deleted file mode 100644 index 11e565a..0000000 --- a/Makefile +++ /dev/null @@ -1,12 +0,0 @@ -EXEC = arimelody-web - -.PHONY: $(EXEC) - -$(EXEC): - GOOS=linux GOARCH=amd64 go build -o $(EXEC) - -bundle: $(EXEC) - tar czf $(EXEC).tar.gz $(EXEC) admin/components/ admin/views/ admin/static/ views/ public/ schema-migration/ - -clean: - rm $(EXEC) $(EXEC).tar.gz diff --git a/README.md b/README.md index 464379e..e5df7f6 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,3 @@ need to be up for this, making this ideal for some offline maintenance. - `purgeInvites`: Deletes all available invite codes. - `listAccounts`: Lists all active accounts. - `deleteAccount `: Deletes an account with a given `username`. -- `lockAccount `: Locks the account under `username`. -- `unlockAccount `: Unlocks the account under `username`. -- `logs`: Shows system logs. diff --git a/admin/accounthttp.go b/admin/accounthttp.go index 945a507..fc03d77 100644 --- a/admin/accounthttp.go +++ b/admin/accounthttp.go @@ -1,17 +1,17 @@ package admin import ( - "database/sql" - "fmt" - "net/http" - "net/url" - "os" + "database/sql" + "fmt" + "net/http" + "net/url" + "os" - "arimelody-web/controller" - "arimelody-web/log" - "arimelody-web/model" + "arimelody-web/controller" + "arimelody-web/log" + "arimelody-web/model" - "golang.org/x/crypto/bcrypt" + "golang.org/x/crypto/bcrypt" ) func accountHandler(app *model.AppState) http.Handler { @@ -115,7 +115,7 @@ func changePasswordHandler(app *model.AppState) http.Handler { return } - app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" changed password by user request. (%s)", session.Account.Username, controller.ResolveIP(app, r)) + app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" changed password by user request. (%s)", session.Account.Username, controller.ResolveIP(r)) controller.SetSessionError(app.DB, session, "") controller.SetSessionMessage(app.DB, session, "Password updated successfully.") @@ -145,7 +145,7 @@ func deleteAccountHandler(app *model.AppState) http.Handler { // check password if err := bcrypt.CompareHashAndPassword([]byte(session.Account.Password), []byte(r.Form.Get("password"))); err != nil { - app.Log.Warn(log.TYPE_ACCOUNT, "Account \"%s\" attempted account deletion with incorrect password. (%s)", session.Account.Username, controller.ResolveIP(app, r)) + app.Log.Warn(log.TYPE_ACCOUNT, "Account \"%s\" attempted account deletion with incorrect password. (%s)", session.Account.Username, controller.ResolveIP(r)) controller.SetSessionError(app.DB, session, "Incorrect password.") http.Redirect(w, r, "/admin/account", http.StatusFound) return @@ -159,7 +159,7 @@ func deleteAccountHandler(app *model.AppState) http.Handler { return } - app.Log.Info(log.TYPE_ACCOUNT, "Account \"%s\" deleted by user request. (%s)", session.Account.Username, controller.ResolveIP(app, r)) + app.Log.Info(log.TYPE_ACCOUNT, "Account \"%s\" deleted by user request. (%s)", session.Account.Username, controller.ResolveIP(r)) controller.SetSessionAccount(app.DB, session, nil) controller.SetSessionError(app.DB, session, "") diff --git a/admin/artisthttp.go b/admin/artisthttp.go index 9fa6bb2..6dfbbfd 100644 --- a/admin/artisthttp.go +++ b/admin/artisthttp.go @@ -1,9 +1,9 @@ package admin import ( - "fmt" - "net/http" - "strings" + "fmt" + "net/http" + "strings" "arimelody-web/model" "arimelody-web/controller" diff --git a/admin/components/release/release-list-item.html b/admin/components/release/release-list-item.html index 4b8f41e..677318d 100644 --- a/admin/components/release/release-list-item.html +++ b/admin/components/release/release-list-item.html @@ -7,7 +7,7 @@

{{.Title}} - {{.ReleaseDate.Year}} + {{.GetReleaseYear}} {{if not .Visible}}(hidden){{end}}

diff --git a/admin/http.go b/admin/http.go index 245a152..c70dd1d 100644 --- a/admin/http.go +++ b/admin/http.go @@ -1,20 +1,20 @@ package admin import ( - "context" - "database/sql" - "fmt" - "net/http" - "os" - "path/filepath" - "strings" - "time" + "context" + "database/sql" + "fmt" + "net/http" + "os" + "path/filepath" + "strings" + "time" - "arimelody-web/controller" - "arimelody-web/log" - "arimelody-web/model" + "arimelody-web/controller" + "arimelody-web/log" + "arimelody-web/model" - "golang.org/x/crypto/bcrypt" + "golang.org/x/crypto/bcrypt" ) func Handler(app *model.AppState) http.Handler { @@ -201,7 +201,7 @@ func registerAccountHandler(app *model.AppState) http.Handler { return } - app.Log.Info(log.TYPE_ACCOUNT, "Account \"%s\" (%s) created using invite \"%s\". (%s)", account.Username, account.ID, invite.Code, controller.ResolveIP(app, r)) + app.Log.Info(log.TYPE_ACCOUNT, "Account \"%s\" (%s) created using invite \"%s\". (%s)", account.Username, account.ID, invite.Code, controller.ResolveIP(r)) err = controller.DeleteInvite(app.DB, invite.Code) if err != nil { @@ -274,20 +274,11 @@ func loginHandler(app *model.AppState) http.Handler { render() return } - if account.Locked { - controller.SetSessionError(app.DB, session, "This account is locked.") - render() - return - } err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(password)) if err != nil { - app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" attempted login with incorrect password. (%s)", account.Username, controller.ResolveIP(app, r)) - if locked := handleFailedLogin(app, account, r); locked { - controller.SetSessionError(app.DB, session, "Too many failed attempts. This account is now locked.") - } else { - controller.SetSessionError(app.DB, session, "Invalid username or password.") - } + app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" attempted login with incorrect password. (%s)", account.Username, controller.ResolveIP(r)) + controller.SetSessionError(app.DB, session, "Invalid username or password.") render() return } @@ -308,15 +299,13 @@ func loginHandler(app *model.AppState) http.Handler { render() return } - controller.SetSessionMessage(app.DB, session, "") - controller.SetSessionError(app.DB, session, "") http.Redirect(w, r, "/admin/totp", http.StatusFound) return } // login success! // TODO: log login activity to user - app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" logged in. (%s)", account.Username, controller.ResolveIP(app, r)) + app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" logged in. (%s)", account.Username, controller.ResolveIP(r)) app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" does not have any TOTP methods assigned.", account.Username) err = controller.SetSessionAccount(app.DB, session, account) @@ -374,7 +363,7 @@ func loginTOTPHandler(app *model.AppState) http.Handler { totpCode := r.FormValue("totp") if len(totpCode) != controller.TOTP_CODE_LENGTH { - app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" failed login (Invalid TOTP). (%s)", session.AttemptAccount.Username, controller.ResolveIP(app, r)) + app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" failed login (Invalid TOTP). (%s)", session.AttemptAccount.Username, controller.ResolveIP(r)) controller.SetSessionError(app.DB, session, "Invalid TOTP.") render() return @@ -388,19 +377,13 @@ func loginTOTPHandler(app *model.AppState) http.Handler { return } if totpMethod == nil { - app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" failed login (Incorrect TOTP). (%s)", session.AttemptAccount.Username, controller.ResolveIP(app, r)) - if locked := handleFailedLogin(app, session.AttemptAccount, r); locked { - controller.SetSessionError(app.DB, session, "Too many failed attempts. This account is now locked.") - controller.SetSessionAttemptAccount(app.DB, session, nil) - http.Redirect(w, r, "/admin", http.StatusFound) - } else { - controller.SetSessionError(app.DB, session, "Incorrect TOTP.") - } + app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" failed login (Invalid TOTP). (%s)", session.AttemptAccount.Username, controller.ResolveIP(r)) + controller.SetSessionError(app.DB, session, "Invalid TOTP.") render() return } - app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" logged in with TOTP method \"%s\". (%s)", session.AttemptAccount.Username, totpMethod.Name, controller.ResolveIP(app, r)) + app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" logged in with TOTP method \"%s\". (%s)", session.AttemptAccount.Username, totpMethod.Name, controller.ResolveIP(r)) err = controller.SetSessionAccount(app.DB, session, session.AttemptAccount) if err != nil { @@ -483,7 +466,7 @@ func staticHandler() http.Handler { func enforceSession(app *model.AppState, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - session, err := controller.GetSessionFromRequest(app, r) + session, err := controller.GetSessionFromRequest(app.DB, r) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve session: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -513,30 +496,3 @@ func enforceSession(app *model.AppState, next http.Handler) http.Handler { next.ServeHTTP(w, r.WithContext(ctx)) }) } - -func handleFailedLogin(app *model.AppState, account *model.Account, r *http.Request) bool { - locked, err := controller.IncrementAccountFails(app.DB, account.ID) - if err != nil { - fmt.Fprintf( - os.Stderr, - "WARN: Failed to increment login failures for \"%s\": %v\n", - account.Username, - err, - ) - app.Log.Warn( - log.TYPE_ACCOUNT, - "Failed to increment login failures for \"%s\"", - account.Username, - ) - } - if locked { - app.Log.Warn( - log.TYPE_ACCOUNT, - "Account \"%s\" was locked: %d failed login attempts (IP: %s)", - account.Username, - model.MAX_LOGIN_FAIL_ATTEMPTS, - controller.ResolveIP(app, r), - ) - } - return locked -} diff --git a/admin/logshttp.go b/admin/logshttp.go index 7249b16..93dc5b7 100644 --- a/admin/logshttp.go +++ b/admin/logshttp.go @@ -1,12 +1,12 @@ package admin import ( - "arimelody-web/log" - "arimelody-web/model" - "fmt" - "net/http" - "os" - "strings" + "arimelody-web/log" + "arimelody-web/model" + "fmt" + "net/http" + "os" + "strings" ) func logsHandler(app *model.AppState) http.Handler { diff --git a/admin/releasehttp.go b/admin/releasehttp.go index c6b68ab..7ef4d37 100644 --- a/admin/releasehttp.go +++ b/admin/releasehttp.go @@ -1,12 +1,12 @@ package admin import ( - "fmt" - "net/http" - "strings" + "fmt" + "net/http" + "strings" - "arimelody-web/controller" - "arimelody-web/model" + "arimelody-web/controller" + "arimelody-web/model" ) func serveRelease(app *model.AppState) http.Handler { diff --git a/admin/static/logs.css b/admin/static/logs.css index f0df299..6ed91b5 100644 --- a/admin/static/logs.css +++ b/admin/static/logs.css @@ -71,7 +71,6 @@ th.log-type { td.log-content, td.log-content { width: 100%; - white-space: collapse; } .log:hover { diff --git a/admin/templates.go b/admin/templates.go index 606d569..12cdf08 100644 --- a/admin/templates.go +++ b/admin/templates.go @@ -1,12 +1,12 @@ package admin import ( - "arimelody-web/log" - "fmt" - "html/template" - "path/filepath" - "strings" - "time" + "arimelody-web/log" + "fmt" + "html/template" + "path/filepath" + "strings" + "time" ) var indexTemplate = template.Must(template.ParseFiles( diff --git a/admin/trackhttp.go b/admin/trackhttp.go index 93eacdb..a92f81a 100644 --- a/admin/trackhttp.go +++ b/admin/trackhttp.go @@ -1,9 +1,9 @@ package admin import ( - "fmt" - "net/http" - "strings" + "fmt" + "net/http" + "strings" "arimelody-web/model" "arimelody-web/controller" diff --git a/api/api.go b/api/api.go index 398db4b..d3c83ce 100644 --- a/api/api.go +++ b/api/api.go @@ -1,15 +1,15 @@ package api import ( - "context" - "errors" - "fmt" - "net/http" - "os" - "strings" + "context" + "errors" + "fmt" + "net/http" + "os" + "strings" - "arimelody-web/controller" - "arimelody-web/model" + "arimelody-web/controller" + "arimelody-web/model" ) func Handler(app *model.AppState) http.Handler { diff --git a/api/artist.go b/api/artist.go index 01899a6..9006cc3 100644 --- a/api/artist.go +++ b/api/artist.go @@ -1,66 +1,66 @@ package api import ( - "encoding/json" - "fmt" - "io/fs" - "net/http" - "os" - "path/filepath" - "strings" - "time" + "encoding/json" + "fmt" + "io/fs" + "net/http" + "os" + "path/filepath" + "strings" + "time" - "arimelody-web/controller" - "arimelody-web/log" - "arimelody-web/model" + "arimelody-web/controller" + "arimelody-web/log" + "arimelody-web/model" ) 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(app.DB) - if err != nil { + if err != nil { fmt.Printf("WARN: Failed to serve all artists: %s\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } - w.Header().Add("Content-Type", "application/json") + w.Header().Add("Content-Type", "application/json") encoder := json.NewEncoder(w) encoder.SetIndent("", "\t") err = encoder.Encode(artists) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } }) } func ServeArtist(app *model.AppState, artist *model.Artist) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - type ( - creditJSON struct { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + type ( + creditJSON struct { ID string `json:"id"` Title string `json:"title"` ReleaseDate time.Time `json:"releaseDate" db:"release_date"` Artwork string `json:"artwork"` - Role string `json:"role"` - Primary bool `json:"primary"` - } - artistJSON struct { - *model.Artist - Credits map[string]creditJSON `json:"credits"` - } - ) + Role string `json:"role"` + Primary bool `json:"primary"` + } + artistJSON struct { + *model.Artist + Credits map[string]creditJSON `json:"credits"` + } + ) session := r.Context().Value("session").(*model.Session) show_hidden_releases := session != nil && session.Account != nil dbCredits, err := controller.GetArtistCredits(app.DB, artist.ID, show_hidden_releases) - if err != nil { + 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) - return - } + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } var credits = map[string]creditJSON{} for _, credit := range dbCredits { @@ -74,17 +74,17 @@ func ServeArtist(app *model.AppState, artist *model.Artist) http.Handler { } } - w.Header().Add("Content-Type", "application/json") + w.Header().Add("Content-Type", "application/json") encoder := json.NewEncoder(w) encoder.SetIndent("", "\t") err = encoder.Encode(artistJSON{ Artist: artist, Credits: credits, }) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } - }) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + }) } func CreateArtist(app *model.AppState) http.Handler { diff --git a/api/release.go b/api/release.go index e07f0d7..efed8dd 100644 --- a/api/release.go +++ b/api/release.go @@ -1,26 +1,26 @@ package api import ( - "encoding/json" - "fmt" - "io/fs" - "net/http" - "os" - "path/filepath" - "strings" - "time" + "encoding/json" + "fmt" + "io/fs" + "net/http" + "os" + "path/filepath" + "strings" + "time" - "arimelody-web/controller" - "arimelody-web/log" - "arimelody-web/model" + "arimelody-web/controller" + "arimelody-web/log" + "arimelody-web/model" ) func ServeRelease(app *model.AppState, release *model.Release) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // only allow authorised users to view hidden releases privileged := false if !release.Visible { - session, err := controller.GetSessionFromRequest(app, r) + session, err := controller.GetSessionFromRequest(app.DB, r) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve session: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -116,15 +116,15 @@ func ServeRelease(app *model.AppState, release *model.Release) http.Handler { } } - w.Header().Add("Content-Type", "application/json") + w.Header().Add("Content-Type", "application/json") encoder := json.NewEncoder(w) encoder.SetIndent("", "\t") err := encoder.Encode(response) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - }) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + }) } func ServeCatalog(app *model.AppState) http.Handler { diff --git a/api/track.go b/api/track.go index 4e48418..e7d7c07 100644 --- a/api/track.go +++ b/api/track.go @@ -1,13 +1,13 @@ package api import ( - "encoding/json" - "fmt" - "net/http" + "encoding/json" + "fmt" + "net/http" - "arimelody-web/controller" - "arimelody-web/log" - "arimelody-web/model" + "arimelody-web/controller" + "arimelody-web/log" + "arimelody-web/model" ) type ( @@ -29,7 +29,7 @@ func ServeAllTracks(app *model.AppState) http.Handler { 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) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } for _, track := range dbTracks { @@ -39,23 +39,23 @@ func ServeAllTracks(app *model.AppState) http.Handler { }) } - w.Header().Add("Content-Type", "application/json") + w.Header().Add("Content-Type", "application/json") encoder := json.NewEncoder(w) encoder.SetIndent("", "\t") err = encoder.Encode(tracks) - if err != nil { + if err != nil { fmt.Printf("WARN: Failed to serve all tracks: %s\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } }) } func ServeTrack(app *model.AppState, track *model.Track) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } releases := []string{} @@ -63,15 +63,15 @@ func ServeTrack(app *model.AppState, track *model.Track) http.Handler { releases = append(releases, release.ID) } - w.Header().Add("Content-Type", "application/json") + w.Header().Add("Content-Type", "application/json") encoder := json.NewEncoder(w) encoder.SetIndent("", "\t") err = encoder.Encode(Track{ track, releases }) - if err != nil { + if err != nil { fmt.Printf("WARN: Failed to serve track %s: %s\n", track.ID, err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } - }) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + }) } func CreateTrack(app *model.AppState) http.Handler { diff --git a/api/uploads.go b/api/uploads.go index 4678f22..60ab7dd 100644 --- a/api/uploads.go +++ b/api/uploads.go @@ -1,56 +1,56 @@ package api import ( - "arimelody-web/log" - "arimelody-web/model" - "bufio" - "encoding/base64" - "errors" - "fmt" - "os" - "path/filepath" - "strings" + "arimelody-web/log" + "arimelody-web/model" + "bufio" + "encoding/base64" + "errors" + "fmt" + "os" + "path/filepath" + "strings" ) 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/") + split := strings.Split(*data, ";base64,") + header := split[0] + imageData, err := base64.StdEncoding.DecodeString(split[1]) + ext, _ := strings.CutPrefix(header, "data:image/") directory = filepath.Join(app.Config.DataDirectory, directory) - switch ext { - case "png": - case "jpg": - case "jpeg": - default: - return "", errors.New("Invalid image type. Allowed: .png, .jpg, .jpeg") - } + switch ext { + case "png": + case "jpg": + case "jpeg": + default: + return "", errors.New("Invalid image type. Allowed: .png, .jpg, .jpeg") + } filename = fmt.Sprintf("%s.%s", filename, ext) - // ensure directory exists - os.MkdirAll(directory, os.ModePerm) + // ensure directory exists + os.MkdirAll(directory, os.ModePerm) - imagePath := filepath.Join(directory, filename) - file, err := os.Create(imagePath) - if err != nil { - return "", err - } - defer file.Close() + imagePath := filepath.Join(directory, filename) + file, err := os.Create(imagePath) + if err != nil { + return "", err + } + defer file.Close() // TODO: generate compressed versions of image (512x512?) - buffer := bufio.NewWriter(file) - _, err = buffer.Write(imageData) - if err != nil { + buffer := bufio.NewWriter(file) + _, err = buffer.Write(imageData) + if err != nil { return "", nil - } + } - if err := buffer.Flush(); err != nil { + if err := buffer.Flush(); err != nil { return "", nil - } + } app.Log.Info(log.TYPE_FILES, "\"%s/%s.%s\" created.", directory, filename, ext) - return filename, nil + return filename, nil } diff --git a/bundle.sh b/bundle.sh new file mode 100755 index 0000000..dc7c023 --- /dev/null +++ b/bundle.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# simple script to pack up arimelody.me for production distribution + +if [ ! -f arimelody-web ]; then + echo "[FATAL] ./arimelody-web not found! please run \`go build -o arimelody-web\` first." + exit 1 +fi + +tar czvf arimelody-web.tar.gz arimelody-web admin/components/ admin/views/ admin/static/ views/ public/ schema-migration/ diff --git a/controller/account.go b/controller/account.go index ab64ca5..9c7c1e1 100644 --- a/controller/account.go +++ b/controller/account.go @@ -1,10 +1,10 @@ package controller import ( - "arimelody-web/model" - "strings" + "arimelody-web/model" + "strings" - "github.com/jmoiron/sqlx" + "github.com/jmoiron/sqlx" ) func GetAllAccounts(db *sqlx.DB) ([]model.Account, error) { @@ -110,26 +110,3 @@ func DeleteAccount(db *sqlx.DB, accountID string) error { _, err := db.Exec("DELETE FROM account WHERE id=$1", accountID) return err } - -func IncrementAccountFails(db *sqlx.DB, accountID string) (bool, error) { - failAttempts := 0 - err := db.Get(&failAttempts, "UPDATE account SET fail_attempts = fail_attempts + 1 WHERE id=$1 RETURNING fail_attempts", accountID) - if err != nil { return false, err } - locked := false - if failAttempts >= model.MAX_LOGIN_FAIL_ATTEMPTS { - err = LockAccount(db, accountID) - if err != nil { return false, err } - locked = true - } - return locked, err -} - -func LockAccount(db *sqlx.DB, accountID string) error { - _, err := db.Exec("UPDATE account SET locked = true WHERE id=$1", accountID) - return err -} - -func UnlockAccount(db *sqlx.DB, accountID string) error { - _, err := db.Exec("UPDATE account SET locked = false, fail_attempts = 0 WHERE id=$1", accountID) - return err -} diff --git a/controller/artist.go b/controller/artist.go index f086778..1a613aa 100644 --- a/controller/artist.go +++ b/controller/artist.go @@ -1,48 +1,48 @@ package controller import ( - "arimelody-web/model" + "arimelody-web/model" - "github.com/jmoiron/sqlx" + "github.com/jmoiron/sqlx" ) // DATABASE func GetArtist(db *sqlx.DB, id string) (*model.Artist, error) { - var artist = model.Artist{} + var artist = model.Artist{} - err := db.Get(&artist, "SELECT * FROM artist WHERE id=$1", id) - if err != nil { - return nil, err - } + err := db.Get(&artist, "SELECT * FROM artist WHERE id=$1", id) + if err != nil { + return nil, err + } - return &artist, nil + return &artist, nil } func GetAllArtists(db *sqlx.DB) ([]*model.Artist, error) { - var artists = []*model.Artist{} + var artists = []*model.Artist{} - err := db.Select(&artists, "SELECT * FROM artist") - if err != nil { - return nil, err - } + err := db.Select(&artists, "SELECT * FROM artist") + if err != nil { + return nil, err + } - return artists, nil + return artists, nil } func GetArtistsNotOnRelease(db *sqlx.DB, releaseID string) ([]*model.Artist, error) { - var artists = []*model.Artist{} + var artists = []*model.Artist{} - err := db.Select(&artists, + err := db.Select(&artists, "SELECT * FROM artist "+ "WHERE id NOT IN "+ "(SELECT artist FROM musiccredit WHERE release=$1)", releaseID) - if err != nil { - return nil, err - } + if err != nil { + return nil, err + } - return artists, nil + return artists, nil } func GetArtistCredits(db *sqlx.DB, artistID string, show_hidden bool) ([]*model.Credit, error) { @@ -54,9 +54,9 @@ func GetArtistCredits(db *sqlx.DB, artistID string, show_hidden bool) ([]*model. if !show_hidden { query += "AND visible=true " } query += "ORDER BY release_date DESC" rows, err := db.Query(query, artistID) - if err != nil { - return nil, err - } + if err != nil { + return nil, err + } defer rows.Close() type NamePrimary struct { @@ -102,13 +102,13 @@ func GetArtistCredits(db *sqlx.DB, artistID string, show_hidden bool) ([]*model. func CreateArtist(db *sqlx.DB, artist *model.Artist) error { _, err := db.Exec( - "INSERT INTO artist (id, name, website, avatar) "+ + "INSERT INTO artist (id, name, website, avatar) "+ "VALUES ($1, $2, $3, $4)", - artist.ID, - artist.Name, - artist.Website, + artist.ID, + artist.Name, + artist.Website, artist.Avatar, - ) + ) if err != nil { return err } diff --git a/controller/config.go b/controller/config.go index fdfa756..8772b6b 100644 --- a/controller/config.go +++ b/controller/config.go @@ -1,14 +1,14 @@ package controller import ( - "errors" - "fmt" - "os" - "strconv" + "errors" + "fmt" + "os" + "strconv" - "arimelody-web/model" + "arimelody-web/model" - "github.com/pelletier/go-toml/v2" + "github.com/pelletier/go-toml/v2" ) func GetConfig() model.Config { @@ -21,7 +21,6 @@ func GetConfig() model.Config { BaseUrl: "https://arimelody.me", Host: "0.0.0.0", Port: 8080, - TrustedProxies: []string{ "127.0.0.1" }, DB: model.DBConfig{ Host: "127.0.0.1", Port: 5432, @@ -77,9 +76,5 @@ func handleConfigOverrides(config *model.Config) error { if env, has := os.LookupEnv("ARIMELODY_DISCORD_CLIENT_ID"); has { config.Discord.ClientID = env } if env, has := os.LookupEnv("ARIMELODY_DISCORD_SECRET"); has { config.Discord.Secret = env } - if env, has := os.LookupEnv("ARIMELODY_TWITCH_BROADCASTER"); has { config.Twitch.Broadcaster = env } - if env, has := os.LookupEnv("ARIMELODY_TWITCH_CLIENT_ID"); has { config.Twitch.ClientID = env } - if env, has := os.LookupEnv("ARIMELODY_TWITCH_SECRET"); has { config.Twitch.Secret = env } - return nil } diff --git a/controller/invite.go b/controller/invite.go index a7bde40..f30db64 100644 --- a/controller/invite.go +++ b/controller/invite.go @@ -1,12 +1,12 @@ package controller import ( - "arimelody-web/model" - "math/rand" - "strings" - "time" + "arimelody-web/model" + "math/rand" + "strings" + "time" - "github.com/jmoiron/sqlx" + "github.com/jmoiron/sqlx" ) var inviteChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") diff --git a/controller/ip.go b/controller/ip.go index cbc3054..4b1126d 100644 --- a/controller/ip.go +++ b/controller/ip.go @@ -1,23 +1,19 @@ package controller import ( - "arimelody-web/model" - "net/http" - "slices" - "strings" + "net/http" + "slices" ) // Returns the request's original IP address, resolving the `x-forwarded-for` // header if the request originates from a trusted proxy. -func ResolveIP(app *model.AppState, r *http.Request) string { - addr := strings.Split(r.RemoteAddr, ":")[0] - if slices.Contains(app.Config.TrustedProxies, addr) { +func ResolveIP(r *http.Request) string { + trustedProxies := []string{ "10.4.20.69" } + if slices.Contains(trustedProxies, r.RemoteAddr) { forwardedFor := r.Header.Get("x-forwarded-for") if len(forwardedFor) > 0 { - // discard extra IPs; cloudflare tends to append their nodes - forwardedFor = strings.Split(forwardedFor, ", ")[0] return forwardedFor } } - return addr + return r.RemoteAddr } diff --git a/controller/migrator.go b/controller/migrator.go index 4b99b9c..b053a27 100644 --- a/controller/migrator.go +++ b/controller/migrator.go @@ -1,14 +1,14 @@ package controller import ( - "fmt" - "os" - "time" + "fmt" + "os" + "time" - "github.com/jmoiron/sqlx" + "github.com/jmoiron/sqlx" ) -const DB_VERSION int = 4 +const DB_VERSION int = 3 func CheckDBVersionAndMigrate(db *sqlx.DB) { db.MustExec("CREATE SCHEMA IF NOT EXISTS arimelody") @@ -45,10 +45,6 @@ func CheckDBVersionAndMigrate(db *sqlx.DB) { ApplyMigration(db, "002-audit-logs") oldDBVersion = 3 - case 3: - ApplyMigration(db, "003-fail-lock") - oldDBVersion = 4 - } } diff --git a/controller/qr.go b/controller/qr.go index dd08637..7ada0f8 100644 --- a/controller/qr.go +++ b/controller/qr.go @@ -1,9 +1,13 @@ package controller import ( - "encoding/base64" - "image" - "image/color" + "bytes" + "encoding/base64" + "errors" + "fmt" + "image" + "image/color" + "image/png" "github.com/skip2/go-qrcode" ) @@ -29,6 +33,69 @@ const ( HIGH ) +func noDepsGenerateQRCode() (string, error) { + version := 1 + + size := 0 + size = 21 + version * 4 + if version > 10 { + return "", errors.New(fmt.Sprintf("QR version %d not supported", version)) + } + + img := image.NewGray(image.Rect(0, 0, size + margin * 2, size + margin * 2)) + + // fill white + for y := range size + margin * 2 { + for x := range size + margin * 2 { + img.Set(x, y, color.White) + } + } + + // draw alignment squares + drawLargeAlignmentSquare(margin, margin, img) + drawLargeAlignmentSquare(margin, margin + size - 7, img) + drawLargeAlignmentSquare(margin + size - 7, margin, img) + drawSmallAlignmentSquare(size - 5, size - 5, img) + /* + if version > 4 { + space := version * 3 - 2 + end := size / space + for y := range size / space + 1 { + for x := range size / space + 1 { + if x == 0 && y == 0 { continue } + if x == 0 && y == end { continue } + if x == end && y == 0 { continue } + if x == end && y == end { continue } + drawSmallAlignmentSquare( + x * space + margin + 4, + y * space + margin + 4, + img, + ) + } + } + } + */ + + // draw timing bits + for i := margin + 6; i < size - 4; i++ { + if (i % 2 == 0) { + img.Set(i, margin + 6, color.Black) + img.Set(margin + 6, i, color.Black) + } + } + img.Set(margin + 8, size - 4, color.Black) + + var imgBuf bytes.Buffer + err := png.Encode(&imgBuf, img) + if err != nil { + return "", err + } + + base64Img := base64.StdEncoding.EncodeToString(imgBuf.Bytes()) + + return "data:image/png;base64," + base64Img, nil +} + func drawLargeAlignmentSquare(x int, y int, img *image.Gray) { for yi := range 7 { for xi := range 7 { diff --git a/controller/release.go b/controller/release.go index 3dcad26..362669a 100644 --- a/controller/release.go +++ b/controller/release.go @@ -1,12 +1,12 @@ package controller import ( - "errors" - "fmt" + "errors" + "fmt" - "arimelody-web/model" + "arimelody-web/model" - "github.com/jmoiron/sqlx" + "github.com/jmoiron/sqlx" ) func GetRelease(db *sqlx.DB, id string, full bool) (*model.Release, error) { diff --git a/controller/session.go b/controller/session.go index 5028789..cf423fe 100644 --- a/controller/session.go +++ b/controller/session.go @@ -1,22 +1,21 @@ package controller import ( - "database/sql" - "errors" - "fmt" - "net/http" - "strings" - "time" + "database/sql" + "errors" + "fmt" + "net/http" + "strings" + "time" - "arimelody-web/log" - "arimelody-web/model" + "arimelody-web/model" - "github.com/jmoiron/sqlx" + "github.com/jmoiron/sqlx" ) const TOKEN_LEN = 64 -func GetSessionFromRequest(app *model.AppState, r *http.Request) (*model.Session, error) { +func GetSessionFromRequest(db *sqlx.DB, r *http.Request) (*model.Session, error) { sessionCookie, err := r.Cookie(model.COOKIE_TOKEN) if err != nil && err != http.ErrNoCookie { return nil, errors.New(fmt.Sprintf("Failed to retrieve session cookie: %v", err)) @@ -26,26 +25,14 @@ func GetSessionFromRequest(app *model.AppState, r *http.Request) (*model.Session if sessionCookie != nil { // fetch existing session - session, err = GetSession(app.DB, sessionCookie.Value) + session, err = GetSession(db, sessionCookie.Value) if err != nil && !strings.Contains(err.Error(), "no rows") { return nil, errors.New(fmt.Sprintf("Failed to retrieve session: %v", err)) } if session != nil { - if session.UserAgent != r.UserAgent() { - msg := "Session user agent mismatch. A cookie may have been hijacked!" - if session.Account != nil { - account, _ := GetAccountByID(app.DB, session.Account.ID) - msg += " (Account \"" + account.Username + "\")" - } - app.Log.Warn(log.TYPE_ACCOUNT, msg) - err = DeleteSession(app.DB, session.Token) - if err != nil { - app.Log.Warn(log.TYPE_ACCOUNT, "Failed to delete affected session") - } - return nil, nil - } + // TODO: consider running security checks here (i.e. user agent mismatches) } } @@ -188,7 +175,3 @@ func DeleteSession(db *sqlx.DB, token string) error { return err } -func DeleteExpiredSessions(db *sqlx.DB) error { - _, err := db.Exec("DELETE FROM session WHERE expires_at 0 { - msg = args[1][0] - } - broadcast <- CursorMessage{ - []byte(fmt.Sprintf("char:%d:%c", client.ID, msg)), - client.Route, - []*CursorClient{ client }, - } - case "nochar": - broadcast <- CursorMessage{ - []byte(fmt.Sprintf("nochar:%d", client.ID)), - client.Route, - []*CursorClient{ client }, - } - case "click": - if len(args) < 2 { return } - click := 0 - if args[1][0] == '1' { - click = 1 - } - broadcast <- CursorMessage{ - []byte(fmt.Sprintf("click:%d:%d", client.ID, click)), - client.Route, - []*CursorClient{ client }, - } - case "pos": - if len(args) < 3 { return } - x, err := strconv.ParseFloat(args[1], 32) - y, err := strconv.ParseFloat(args[2], 32) - if err != nil { return } - client.X = float32(x) - client.Y = float32(y) - broadcast <- CursorMessage{ - []byte(fmt.Sprintf("pos:%d:%f:%f", client.ID, client.X, client.Y)), - client.Route, - []*CursorClient{ client }, - } - } -} - -func Handler(app *model.AppState) http.HandlerFunc { - var upgrader = websocket.Upgrader{ - CheckOrigin: func (r *http.Request) bool { - origin := r.Header.Get("Origin") - return origin == app.Config.BaseUrl - }, - } - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - log("Failed to upgrade to WebSocket connection: %v\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - defer conn.Close() - - client := CursorClient{ - ID: rand.Int31(), - Conn: conn, - X: 0.0, - Y: 0.0, - Disconnected: false, - } - - err = client.Conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("id:%d", client.ID))) - if err != nil { - client.Conn.Close() - return - } - - mutex.Lock() - clients[client.ID] = &client - mutex.Unlock() - - // log("Client connected: %s (%s)", fmt.Sprintf("0x%08x", client.ID), client.Conn.RemoteAddr().String()) - - for { - if client.Disconnected { - mutex.Lock() - delete(clients, client.ID) - client.Conn.Close() - mutex.Unlock() - return - } - handleClient(&client) - } - }) -} - -func log(format string, args ...any) { - logString := fmt.Sprintf(format, args...) - fmt.Printf("[%s] [CURSOR] %s\n", time.Now().Format(time.UnixDate), logString) -} diff --git a/discord/discord.go b/discord/discord.go index 0eb9b97..d46f32d 100644 --- a/discord/discord.go +++ b/discord/discord.go @@ -1,13 +1,13 @@ package discord import ( - "arimelody-web/model" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/url" - "strings" + "arimelody-web/model" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strings" ) const API_ENDPOINT = "https://discord.com/api/v10" diff --git a/go.mod b/go.mod index a1c6c76..f8717a2 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,6 @@ require ( require golang.org/x/crypto v0.27.0 // indirect require ( - github.com/gorilla/websocket v1.5.3 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect ) diff --git a/go.sum b/go.sum index f2ec7e7..15259a1 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,6 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= diff --git a/log/log.go b/log/log.go index 88d328b..3d023ab 100644 --- a/log/log.go +++ b/log/log.go @@ -1,11 +1,11 @@ package log import ( - "fmt" - "os" - "time" + "fmt" + "os" + "time" - "github.com/jmoiron/sqlx" + "github.com/jmoiron/sqlx" ) type ( @@ -30,7 +30,6 @@ const ( TYPE_ARTWORK string = "artwork" TYPE_FILES string = "files" TYPE_MISC string = "misc" - TYPE_CURSOR string = "cursor" ) type LogLevel int diff --git a/main.go b/main.go index 7bcd5ac..03e9d77 100644 --- a/main.go +++ b/main.go @@ -1,32 +1,30 @@ package main import ( - "bufio" - "errors" - "fmt" - stdLog "log" - "math" - "math/rand" - "net" - "net/http" - "os" - "path/filepath" - "strconv" - "strings" - "time" + "errors" + "fmt" + stdLog "log" + "math" + "math/rand" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" - "arimelody-web/admin" - "arimelody-web/api" - "arimelody-web/colour" - "arimelody-web/controller" - "arimelody-web/cursor" + "arimelody-web/admin" + "arimelody-web/api" + "arimelody-web/colour" + "arimelody-web/controller" + "arimelody-web/model" + "arimelody-web/templates" "arimelody-web/log" - "arimelody-web/model" - "arimelody-web/view" + "arimelody-web/view" - "github.com/jmoiron/sqlx" - _ "github.com/lib/pq" - "golang.org/x/crypto/bcrypt" + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" + "golang.org/x/crypto/bcrypt" ) // used for database migrations @@ -39,7 +37,6 @@ func main() { app := model.AppState{ Config: controller.GetConfig(), - Twitch: nil, } // initialise database connection @@ -223,7 +220,7 @@ func main() { code := controller.GenerateTOTP(totp.Secret, 0) fmt.Printf("%s\n", code) return - + case "cleanTOTP": err := controller.DeleteUnconfirmedTOTPs(app.DB) if err != nil { @@ -276,13 +273,11 @@ func main() { "User: %s\n" + "\tID: %s\n" + "\tEmail: %s\n" + - "\tCreated: %s\n" + - "\tLocked: %t\n", + "\tCreated: %s\n", account.Username, account.ID, email, account.CreatedAt, - account.Locked, ) } return @@ -346,7 +341,7 @@ func main() { if !strings.HasPrefix(res, "y") { return } - + err = controller.DeleteAccount(app.DB, account.ID) if err != nil { fmt.Fprintf(os.Stderr, "FATAL: Failed to delete account: %v\n", err) @@ -357,64 +352,6 @@ func main() { fmt.Printf("Account \"%s\" deleted successfully.\n", account.Username) return - case "lockAccount": - if len(os.Args) < 3 { - fmt.Fprintf(os.Stderr, "FATAL: `username` must be specified for lockAccount\n") - os.Exit(1) - } - username := os.Args[2] - fmt.Printf("Unlocking account \"%s\"...\n", username) - - account, err := controller.GetAccountByUsername(app.DB, username) - if err != nil { - fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err) - os.Exit(1) - } - - if account == nil { - fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username) - os.Exit(1) - } - - err = controller.LockAccount(app.DB, account.ID) - if err != nil { - fmt.Fprintf(os.Stderr, "FATAL: Failed to lock account: %v\n", err) - os.Exit(1) - } - - app.Log.Info(log.TYPE_ACCOUNT, "Account '%s' locked via config utility.", account.Username) - fmt.Printf("Account \"%s\" locked successfully.\n", account.Username) - return - - case "unlockAccount": - if len(os.Args) < 3 { - fmt.Fprintf(os.Stderr, "FATAL: `username` must be specified for unlockAccount\n") - os.Exit(1) - } - username := os.Args[2] - fmt.Printf("Unlocking account \"%s\"...\n", username) - - account, err := controller.GetAccountByUsername(app.DB, username) - if err != nil { - fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err) - os.Exit(1) - } - - if account == nil { - fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username) - os.Exit(1) - } - - err = controller.UnlockAccount(app.DB, account.ID) - if err != nil { - fmt.Fprintf(os.Stderr, "FATAL: Failed to unlock account: %v\n", err) - os.Exit(1) - } - - app.Log.Info(log.TYPE_ACCOUNT, "Account '%s' unlocked via config utility.", account.Username) - fmt.Printf("Account \"%s\" unlocked successfully.\n", account.Username) - return - case "logs": // TODO: add log search parameters logs, err := app.Log.Search([]log.LogLevel{}, []string{}, "", 100, 0) @@ -449,10 +386,7 @@ func main() { "createInvite:\n\tCreates an invite code to register new accounts.\n" + "purgeInvites:\n\tDeletes all available invite codes.\n" + "listAccounts:\n\tLists all active accounts.\n", - "deleteAccount :\n\tDeletes the account under `username`.\n", - "lockAccount :\n\tLocks the account under `username`.\n", - "unlockAccount :\n\tUnlocks the account under `username`.\n", - "logs:\n\tShows system logs.\n", + "deleteAccount :\n\tDeletes an account with a given `username`.\n", ) return } @@ -460,13 +394,6 @@ func main() { // handle DB migrations controller.CheckDBVersionAndMigrate(app.DB) - if app.Config.Twitch != nil { - err = controller.TwitchSetup(&app) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to set up Twitch integration: %v\n", err) - } - } - // initial invite code accountsCount := 0 err = app.DB.Get(&accountsCount, "SELECT count(*) FROM account") @@ -487,13 +414,6 @@ func main() { fmt.Printf("No accounts exist! Generated invite code: %s\n", invite.Code) } - // delete expired sessions - err = controller.DeleteExpiredSessions(app.DB) - if err != nil { - fmt.Fprintf(os.Stderr, "FATAL: Failed to clear expired sessions: %v\n", err) - os.Exit(1) - } - // delete expired invites err = controller.DeleteExpiredInvites(app.DB) if err != nil { @@ -508,8 +428,6 @@ func main() { os.Exit(1) } - go cursor.StartCursor(&app) - // start the web server! mux := createServeMux(&app) fmt.Printf("Now serving at http://%s:%d\n", app.Config.Host, app.Config.Port) @@ -525,13 +443,48 @@ func createServeMux(app *model.AppState) *http.ServeMux { mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler(app))) mux.Handle("/api/", http.StripPrefix("/api", api.Handler(app))) mux.Handle("/music/", http.StripPrefix("/music", view.MusicHandler(app))) - mux.Handle("/uploads/", http.StripPrefix("/uploads", view.StaticHandler(filepath.Join(app.Config.DataDirectory, "uploads")))) - mux.Handle("/cursor-ws", cursor.Handler(app)) - mux.Handle("/", view.IndexHandler(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) + return + } + + if r.URL.Path == "/" || r.URL.Path == "/index.html" { + err := templates.IndexTemplate.Execute(w, nil) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + return + } + staticHandler("public").ServeHTTP(w, r) + })) return mux } +func staticHandler(directory string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + info, err := os.Stat(filepath.Join(directory, filepath.Clean(r.URL.Path))) + + // does the file exist? + if err != nil { + if errors.Is(err, os.ErrNotExist) { + http.NotFound(w, r) + return + } + } + + // is thjs a directory? (forbidden) + if info.IsDir() { + http.NotFound(w, r) + return + } + + http.FileServer(http.Dir(directory)).ServeHTTP(w, r) + }) +} + var PoweredByStrings = []string{ "nerd rage", "estrogen", @@ -583,14 +536,6 @@ type LoggingResponseWriter struct { Status int } -func (lrw *LoggingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { - hijack, ok := lrw.ResponseWriter.(http.Hijacker) - if !ok { - return nil, nil, errors.New("Server does not support hijacking\n") - } - return hijack.Hijack() -} - func (lrw *LoggingResponseWriter) WriteHeader(status int) { lrw.Status = status lrw.ResponseWriter.WriteHeader(status) diff --git a/model/account.go b/model/account.go index 67424b7..166e880 100644 --- a/model/account.go +++ b/model/account.go @@ -1,23 +1,20 @@ package model import ( - "database/sql" - "time" + "database/sql" + "time" ) const COOKIE_TOKEN string = "AM_SESSION" -const MAX_LOGIN_FAIL_ATTEMPTS int = 3 type ( Account struct { - ID string `json:"id" db:"id"` - Username string `json:"username" db:"username"` - Password string `json:"password" db:"password"` - Email sql.NullString `json:"email" db:"email"` - AvatarURL sql.NullString `json:"avatar_url" db:"avatar_url"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - FailAttempts int `json:"fail_attempts" db:"fail_attempts"` - Locked bool `json:"locked" db:"locked"` + ID string `json:"id" db:"id"` + Username string `json:"username" db:"username"` + Password string `json:"password" db:"password"` + Email sql.NullString `json:"email" db:"email"` + AvatarURL sql.NullString `json:"avatar_url" db:"avatar_url"` + CreatedAt time.Time `json:"created_at" db:"created_at"` Privileges []AccountPrivilege `json:"privileges"` } diff --git a/model/appstate.go b/model/appstate.go index 3a1c230..2516b6e 100644 --- a/model/appstate.go +++ b/model/appstate.go @@ -1,7 +1,7 @@ package model import ( - "github.com/jmoiron/sqlx" + "github.com/jmoiron/sqlx" "arimelody-web/log" ) @@ -21,27 +21,18 @@ type ( Secret string `toml:"secret"` } - TwitchConfig struct { - Broadcaster string `toml:"broadcaster"` - ClientID string `toml:"client_id"` - Secret string `toml:"secret"` - } - Config struct { BaseUrl string `toml:"base_url" comment:"Used for OAuth redirects."` Host string `toml:"host"` Port int64 `toml:"port"` DataDirectory string `toml:"data_dir"` - TrustedProxies []string `toml:"trusted_proxies"` DB DBConfig `toml:"db"` - Discord *DiscordConfig `toml:"discord"` - Twitch *TwitchConfig `toml:"twitch"` + Discord DiscordConfig `toml:"discord"` } AppState struct { DB *sqlx.DB Config Config Log log.Logger - Twitch *TwitchState } ) diff --git a/model/artist.go b/model/artist.go index 746a7dd..733e537 100644 --- a/model/artist.go +++ b/model/artist.go @@ -1,17 +1,21 @@ package model type ( - Artist struct { - ID string `json:"id"` - Name string `json:"name"` - Website string `json:"website"` - Avatar string `json:"avatar"` - } + Artist struct { + ID string `json:"id"` + Name string `json:"name"` + Website string `json:"website"` + Avatar string `json:"avatar"` + } ) -func (artist Artist) GetAvatar() string { - if artist.Avatar == "" { - return "/img/default-avatar.png" - } - return artist.Avatar +func (artist Artist) GetWebsite() string { + return artist.Website +} + +func (artist Artist) GetAvatar() string { + if artist.Avatar == "" { + return "/img/default-avatar.png" + } + return artist.Avatar } diff --git a/model/artist_test.go b/model/artist_test.go deleted file mode 100644 index feb9a18..0000000 --- a/model/artist_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package model - -import ( - "testing" -) - -func Test_Artist_GetAvatar(t *testing.T) { - want := "testavatar.png" - artist := Artist{ Avatar: want } - - got := artist.GetAvatar() - if want != got { - t.Errorf(`correct value not returned when avatar is populated (want "%s", got "%s")`, want, got) - } - - artist = Artist{} - - want = "/img/default-avatar.png" - got = artist.GetAvatar() - if want != got { - t.Errorf(`default value not returned when avatar is empty (want "%s", got "%s")`, want, got) - } -} diff --git a/model/link.go b/model/link.go index ba83e22..8b48ced 100644 --- a/model/link.go +++ b/model/link.go @@ -1,16 +1,16 @@ package model import ( - "regexp" - "strings" + "regexp" + "strings" ) type Link struct { - Name string `json:"name"` - URL string `json:"url"` + Name string `json:"name"` + URL string `json:"url"` } func (link Link) NormaliseName() string { - rgx := regexp.MustCompile(`[^a-z0-9\-]`) - return rgx.ReplaceAllString(strings.ToLower(link.Name), "") + rgx := regexp.MustCompile(`[^a-z0-9]`) + return strings.ToLower(rgx.ReplaceAllString(link.Name, "")) } diff --git a/model/link_test.go b/model/link_test.go deleted file mode 100644 index b368094..0000000 --- a/model/link_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package model - -import ( - "testing" -) - -func Test_Link_NormaliseName(t *testing.T) { - link := Link{ - Name: "!c@o#o$l%-^a&w*e(s)o_m=e+-[l{i]n}k-0123456789ABCDEF", - } - - want := "cool-awesome-link-0123456789abcdef" - got := link.NormaliseName() - if want != got { - t.Errorf(`name with invalid characters not properly formatted (want "%s", got "%s")`, want, got) - } - - link.Name = want - got = link.NormaliseName() - if want != got { - t.Errorf(`valid name mangled by formatter (want "%s", got "%s")`, want, got) - } -} diff --git a/model/release.go b/model/release.go index e64317b..42d6fba 100644 --- a/model/release.go +++ b/model/release.go @@ -1,9 +1,9 @@ package model import ( - "html/template" - "strings" - "time" + "html/template" + "strings" + "time" ) type ( @@ -50,6 +50,10 @@ func (release Release) PrintReleaseDate() string { return release.ReleaseDate.Format("02 January 2006") } +func (release Release) GetReleaseYear() int { + return release.ReleaseDate.Year() +} + func (release Release) GetArtwork() string { if release.Artwork == "" { return "/img/default-cover-art.png" @@ -73,23 +77,23 @@ func (release Release) GetUniqueArtistNames(only_primary bool) []string { names = append(names, credit.Artist.Name) } - return names + return names } func (release Release) PrintArtists(only_primary bool, ampersand bool) string { names := release.GetUniqueArtistNames(only_primary) - if len(names) == 0 { - return "Unknown Artist" - } else if len(names) == 1 { - return names[0] - } + if len(names) == 0 { + return "Unknown Artist" + } else if len(names) == 1 { + return names[0] + } - if ampersand { - res := strings.Join(names[:len(names)-1], ", ") - res += " & " + names[len(names)-1] - return res - } else { - return strings.Join(names[:], ", ") - } + if ampersand { + res := strings.Join(names[:len(names)-1], ", ") + res += " & " + names[len(names)-1] + return res + } else { + return strings.Join(names[:], ", ") + } } diff --git a/model/release_test.go b/model/release_test.go deleted file mode 100644 index b0ddaf5..0000000 --- a/model/release_test.go +++ /dev/null @@ -1,157 +0,0 @@ -package model - -import ( - "testing" - "time" -) - -func Test_Release_DescriptionHTML(t *testing.T) { - release := Release{ - Description: "this is\na test\ndescription!", - } - - // descriptions are set by privileged users, - // so we'll allow HTML injection here - want := "this is
a test
description!" - got := release.GetDescriptionHTML() - if want != string(got) { - t.Errorf(`release description incorrectly formatted (want "%s", got "%s")`, want, got) - } -} - -func Test_Release_ReleaseDate(t *testing.T) { - release := Release{ - ReleaseDate: time.Date(2025, time.July, 26, 16, 0, 0, 0, time.UTC), - } - - want := "2025-07-26T16:00" - got := release.TextReleaseDate() - if want != got { - t.Errorf(`release date incorrectly formatted (want "%s", got "%s")`, want, got) - } - - want = "26 July 2025" - got = release.PrintReleaseDate() - if want != got { - t.Errorf(`release date (print) incorrectly formatted (want "%s", got "%s")`, want, got) - } -} - -func Test_Release_Artwork(t *testing.T) { - want := "testartwork.png" - release := Release{ Artwork: want } - - got := release.GetArtwork() - if want != got { - t.Errorf(`correct value not returned when artwork is populated (want "%s", got "%s")`, want, got) - } - - release = Release{} - - want = "/img/default-cover-art.png" - got = release.GetArtwork() - if want != got { - t.Errorf(`default value not returned when artwork is empty (want "%s", got "%s")`, want, got) - } -} - -func Test_Release_IsSingle(t *testing.T) { - release := Release{ - Tracks: []*Track{}, - } - - if release.IsSingle() { - t.Errorf("IsSingle() == true when no tracks are present") - } - - release.Tracks = append(release.Tracks, &Track{}) - if !release.IsSingle() { - t.Errorf("IsSingle() == false when one track is present") - } - - release.Tracks = append(release.Tracks, &Track{}) - if release.IsSingle() { - t.Errorf("IsSingle() == true when >1 tracks are present") - } -} - -func Test_Release_IsReleased(t *testing.T) { - release := Release { - ReleaseDate: time.Now(), - } - - if !release.IsReleased() { - t.Errorf("IsRelease() == false when release date in the past") - } - - release.ReleaseDate = time.Now().Add(time.Hour) - if release.IsReleased() { - t.Errorf("IsRelease() == true when release date in the future") - } -} - -func Test_Release_PrintArtists(t *testing.T) { - artist1 := "ari melody" - artist2 := "aridoodle" - artist3 := "idk" - artist4 := "guest" - - release := Release { - Credits: []*Credit{ - { Artist: Artist{ Name: artist1 }, Primary: true }, - { Artist: Artist{ Name: artist2 }, Primary: true }, - { Artist: Artist{ Name: artist3 }, Primary: false }, - { Artist: Artist{ Name: artist4 }, Primary: true }, - }, - } - - { - want := []string{ artist1, artist2, artist4 } - got := release.GetUniqueArtistNames(true) - if len(want) != len(got) { - t.Errorf(`len(GetUniqueArtistNames) (primary only) == %d, want %d`, len(got), len(want)) - } - for i := range got { - if want[i] != got[i] { - t.Errorf(`GetUniqueArtistNames[%d] (primary only) == %s, want %s`, i, got[i], want[i]) - } - } - - want = []string{ artist1, artist2, artist3, artist4 } - got = release.GetUniqueArtistNames(false) - if len(want) != len(got) { - t.Errorf(`len(GetUniqueArtistNames) == %d, want %d`, len(got), len(want)) - } - for i := range got { - if want[i] != got[i] { - t.Errorf(`GetUniqueArtistNames[%d] == %s, want %s`, i, got[i], want[i]) - } - } - } - - { - want := "ari melody, aridoodle & guest" - got := release.PrintArtists(true, true) - if want != got { - t.Errorf(`PrintArtists (primary only, ampersand) == "%s", want "%s"`, want, got) - } - - want = "ari melody, aridoodle, guest" - got = release.PrintArtists(true, false) - if want != got { - t.Errorf(`PrintArtists (primary only) == "%s", want "%s"`, want, got) - } - - want = "ari melody, aridoodle, idk & guest" - got = release.PrintArtists(false, true) - if want != got { - t.Errorf(`PrintArtists (all, ampersand) == "%s", want "%s"`, want, got) - } - - want = "ari melody, aridoodle, idk, guest" - got = release.PrintArtists(false, false) - if want != got { - t.Errorf(`PrintArtists (all) == "%s", want "%s"`, want, got) - } - } -} diff --git a/model/session.go b/model/session.go index 7382de3..de016e1 100644 --- a/model/session.go +++ b/model/session.go @@ -1,8 +1,8 @@ package model import ( - "database/sql" - "time" + "database/sql" + "time" ) type Session struct { diff --git a/model/totp.go b/model/totp.go index 108dae5..cfad10a 100644 --- a/model/totp.go +++ b/model/totp.go @@ -1,7 +1,7 @@ package model import ( - "time" + "time" ) type TOTP struct { diff --git a/model/track.go b/model/track.go index deaf086..ca54ddd 100644 --- a/model/track.go +++ b/model/track.go @@ -1,20 +1,20 @@ package model import ( - "html/template" - "strings" + "html/template" + "strings" ) type ( - Track struct { - ID string `json:"id"` - Title string `json:"title"` - Description string `json:"description"` + Track struct { + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` Lyrics string `json:"lyrics" db:"lyrics"` - PreviewURL string `json:"previewURL" db:"preview_url"` + PreviewURL string `json:"previewURL" db:"preview_url"` Number int - } + } ) func (track Track) GetDescriptionHTML() template.HTML { diff --git a/model/track_test.go b/model/track_test.go deleted file mode 100644 index fd500d7..0000000 --- a/model/track_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package model - -import ( - "testing" -) - -func Test_Track_DescriptionHTML(t *testing.T) { - track := Track{ - Description: "this is\na test\ndescription!", - } - - // descriptions are set by privileged users, - // so we'll allow HTML injection here - want := "this is
a test
description!" - got := track.GetDescriptionHTML() - if want != string(got) { - t.Errorf(`track description incorrectly formatted (want "%s", got "%s")`, want, got) - } -} - -func Test_Track_LyricsHTML(t *testing.T) { - track := Track{ - Lyrics: "these are\ntest\nlyrics!", - } - - // lyrics are set by privileged users, - // so we'll allow HTML injection here - want := "these are
test
lyrics!" - got := track.GetLyricsHTML() - if want != string(got) { - t.Errorf(`track lyrics incorrectly formatted (want "%s", got "%s")`, want, got) - } -} - -func Test_Track_Add(t *testing.T) { - track := Track{} - - want := 4 - got := track.Add(2, 2) - if want != got { - t.Errorf(`somehow, we screwed up addition. (want %d, got %d)`, want, got) - } -} diff --git a/model/twitch.go b/model/twitch.go deleted file mode 100644 index 6bca17d..0000000 --- a/model/twitch.go +++ /dev/null @@ -1,43 +0,0 @@ -package model - -import ( - "fmt" - "strings" - "time" -) - -type ( - TwitchOAuthToken struct { - AccessToken string - ExpiresAt time.Time - TokenType string - } - - TwitchState struct { - Token *TwitchOAuthToken - } - - TwitchStreamInfo struct { - ID string `json:"id"` - UserID string `json:"user_id"` - UserLogin string `json:"user_login"` - UserName string `json:"user_name"` - GameID string `json:"game_id"` - GameName string `json:"game_name"` - Type string `json:"type"` - Title string `json:"title"` - ViewerCount int `json:"viewer_count"` - StartedAt string `json:"started_at"` - Language string `json:"language"` - ThumbnailURL string `json:"thumbnail_url"` - TagIDs []string `json:"tag_ids"` - Tags []string `json:"tags"` - IsMature bool `json:"is_mature"` - } -) - -func (info *TwitchStreamInfo) Thumbnail(width int, height int) string { - res := strings.Replace(info.ThumbnailURL, "{width}", fmt.Sprintf("%d", width), 1) - res = strings.Replace(res, "{height}", fmt.Sprintf("%d", height), 1) - return res -} diff --git a/public/img/brand/bandcamp.svg b/public/img/brand/bandcamp.svg deleted file mode 100644 index 9623ec2..0000000 --- a/public/img/brand/bandcamp.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/public/img/brand/bluesky.svg b/public/img/brand/bluesky.svg deleted file mode 100644 index d77fafe..0000000 --- a/public/img/brand/bluesky.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/public/img/brand/codeberg.svg b/public/img/brand/codeberg.svg deleted file mode 100644 index 028b729..0000000 --- a/public/img/brand/codeberg.svg +++ /dev/null @@ -1,164 +0,0 @@ - - - Codeberg logo - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - Codeberg logo - - - - Robert Martinez - - - - - Codeberg and the Codeberg Logo are trademarks of Codeberg e.V. - - - 2020-04-09 - - - Codeberg e.V. - - - codeberg.org - - - - - - - - - - - - - diff --git a/public/img/brand/discord.svg b/public/img/brand/discord.svg deleted file mode 100644 index b636d15..0000000 --- a/public/img/brand/discord.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/img/brand/twitch.svg b/public/img/brand/twitch.svg deleted file mode 100644 index 3120fea..0000000 --- a/public/img/brand/twitch.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - -Asset 2 - - - - - - - - - - - diff --git a/public/img/brand/youtube.svg b/public/img/brand/youtube.svg deleted file mode 100644 index 3286071..0000000 --- a/public/img/brand/youtube.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/public/img/buttons/girlonthemoon.png b/public/img/buttons/girlonthemoon.png deleted file mode 100644 index 4e98a12..0000000 Binary files a/public/img/buttons/girlonthemoon.png and /dev/null differ diff --git a/public/img/buttons/thermia.gif b/public/img/buttons/thermia.gif deleted file mode 100644 index a7ee70a..0000000 Binary files a/public/img/buttons/thermia.gif and /dev/null differ diff --git a/public/keys/ari melody_0x92678188_public.asc b/public/keys/ari melody_0x92678188_public.asc index 80a4676..2b37d88 100644 --- a/public/keys/ari melody_0x92678188_public.asc +++ b/public/keys/ari melody_0x92678188_public.asc @@ -1,26 +1,13 @@ -----BEGIN PGP PUBLIC KEY BLOCK----- mDMEZNW03RYJKwYBBAHaRw8BAQdAuMUNVjXT7m/YisePPnSYY6lc1Xmm3oS79ZEO -JriRCZy0HWFyaSBtZWxvZHkgPGFyaUBhcmltZWxvZHkubWU+iJkEExYKAEECGwMF -CwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AWIQTujeuNYocuegkeKt/PmYKckmeB -iAUCZ7UqUAUJCIMP8wAKCRDPmYKckmeBiO/NAP0SoJL4aKZqCeYiSoDF/Uw6nMmZ -+oR1Uig41wQ/IDbhCAEApP2vbjSIu6pcp0AQlL7qcoyPWv+XkqPSFqW9KEZZVwqI -kwQTFgoAOxYhBO6N641ihy56CR4q38+ZgpySZ4GIBQJk1bTdAhsDBQsJCAcCAiIC -BhUKCQgLAgQWAgMBAh4HAheAAAoJEM+ZgpySZ4GIYJsA/jBNwsJTlmV9JMmsW0aF -ApYDoPG1Q7sJ6CRW7xKCRjcqAQDX9iqNnW9Jqo8M3jXfu+aGSF926hg6M3SKm02P -f27bAbgzBGe1JooWCSsGAQQB2kcPAQEHQJbfh5iLHEpZndMgekqYzqTrUoAJ8ZIL -d4WH0dcw9tOaiPUEGBYKACYCGwIWIQTujeuNYocuegkeKt/PmYKckmeBiAUCZ7Uq -VgUJBaOeTACBdiAEGRYKAB0WIQQlu5dWmBR/P3ZxngxgtfA4bj3bfgUCZ7UmigAK -CRBgtfA4bj3bfux+AP4y5ydrjnGBMX7GuB2nh55SRdscSiXsZ66ntnjXyQcbWgEA -pDuu7FqXzXcnluuZxNFDT740Rnzs60tTeplDqGGWcAQJEM+ZgpySZ4GIc0kA/iSw -Nw+r3FC75omwrPpJF13B5fq93FweFx+oSaES6qzkAQDvgCK77qKKbvCju0g8zSsK -EZnv6xR4uvtGdVkvLpBdC7gzBGe1JpkWCSsGAQQB2kcPAQEHQGnU4lXFLchhKYkC -PshP+jvuRsNoedaDOK2p4dkQC8JuiH4EGBYKACYCGyAWIQTujeuNYocuegkeKt/P -mYKckmeBiAUCZ7UqXgUJBaOeRQAKCRDPmYKckmeBiL9KAQCJZIBhuSsoYa61I0XZ -cKzGZbB0h9pD6eg1VRswNIgHtQEAwu9Hgs1rs9cySvKbO7WgK6Qh6EfrvGgGOXCO -m3wVsg24OARntSo5EgorBgEEAZdVAQUBAQdA+/k586W1OHxndzDJNpbd+wqjyjr0 -D5IXxfDs00advB0DAQgHiH4EGBYKACYWIQTujeuNYocuegkeKt/PmYKckmeBiAUC -Z7UqOQIbDAUJBaOagAAKCRDPmYKckmeBiEFxAQCgziQt2l3u7jnZVij4zop+K2Lv -TVFtkbG61tf6brRzBgD/X6c6X5BRyQC51JV1I1RFRBdeMAIXzcLFg2v3WUMccQs= -=YmHI +JriRCZy0HWFyaSBtZWxvZHkgPGFyaUBhcmltZWxvZHkubWU+iJMEExYKADsWIQTu +jeuNYocuegkeKt/PmYKckmeBiAUCZNW03QIbAwULCQgHAgIiAgYVCgkICwIEFgID +AQIeBwIXgAAKCRDPmYKckmeBiGCbAP4wTcLCU5ZlfSTJrFtGhQKWA6DxtUO7Cegk +Vu8SgkY3KgEA1/YqjZ1vSaqPDN4137vmhkhfduoYOjN0iptNj39u2wG4OARk1bTd +EgorBgEEAZdVAQUBAQdAnA2drPzQBoXNdwIrFnovuF0CjX+8+8QSugCF4a5ZEXED +AQgHiHgEGBYKACAWIQTujeuNYocuegkeKt/PmYKckmeBiAUCZNW03QIbDAAKCRDP +mYKckmeBiC/xAQD1hu4WcstR40lkUxMqhZ44wmizrDA+eGCdh7Ge3Gy79wEAx385 +GnYoNplMTA4BTGs7orV4WSfSkoBx0+px1UOewgs= +=M1Bp -----END PGP PUBLIC KEY BLOCK----- diff --git a/public/script/config.js b/public/script/config.js index 402a74b..2bb33a6 100644 --- a/public/script/config.js +++ b/public/script/config.js @@ -1,106 +1,33 @@ -const ARIMELODY_CONFIG_NAME = "arimelody.me-config"; - -class Config { - _crt = false; - _cursor = false; - _cursorFunMode = false; - - /** @type Map> */ - #listeners = new Map(); - - constructor(values) { - function thisOrElse(values, name, defaultValue) { - if (values === null) return defaultValue; - if (values[name] === undefined) return defaultValue; - return values[name]; - } - - this.#listeners.set('crt', new Array()); - this.crt = thisOrElse(values, 'crt', false); - this.#listeners.set('cursor', new Array()); - this.cursor = thisOrElse(values, 'cursor', false); - this.#listeners.set('cursorFunMode', new Array()); - this.cursorFunMode = thisOrElse(values, 'cursorFunMode', false); - this.save(); - } - - /** - * Appends a listener function to be called when the config value of `name` - * is changed. - */ - addListener(name, callback) { - const callbacks = this.#listeners.get(name); - if (!callbacks) return; - callbacks.push(callback); - } - - /** - * Removes the listener function `callback` from the list of callbacks when - * the config value of `name` is changed. - */ - removeListener(name, callback) { - const callbacks = this.#listeners.get(name); - if (!callbacks) return; - callbacks.set(name, callbacks.filter(c => c !== callback)); - } - - save() { - localStorage.setItem(ARIMELODY_CONFIG_NAME, JSON.stringify({ - crt: this.crt, - cursor: this.cursor, - cursorFunMode: this.cursorFunMode - })); - } - - get crt() { return this._crt } - set crt(/** @type boolean */ enabled) { - this._crt = enabled; - this.save(); - - if (enabled) { - document.body.classList.add("crt"); - } else { - document.body.classList.remove("crt"); - } - document.getElementById('toggle-crt').className = enabled ? "" : "disabled"; - - this.#listeners.get('crt').forEach(callback => { - callback(this._crt); - }) - } - - get cursor() { return this._cursor } - set cursor(/** @type boolean */ value) { - this._cursor = value; - this.save(); - this.#listeners.get('cursor').forEach(callback => { - callback(this._cursor); - }) - } - - get cursorFunMode() { return this._cursorFunMode } - set cursorFunMode(/** @type boolean */ value) { - this._cursorFunMode = value; - this.save(); - this.#listeners.get('cursorFunMode').forEach(callback => { - callback(this._cursorFunMode); - }) - } -} - +const DEFAULT_CONFIG = { + crt: false +}; const config = (() => { - let values = null; + let saved = localStorage.getItem("config"); + if (saved) { + const config = JSON.parse(saved); + setCRT(config.crt || DEFAULT_CONFIG.crt); + return config; + } - const saved = localStorage.getItem(ARIMELODY_CONFIG_NAME); - if (saved) - values = JSON.parse(saved); - - return new Config(values); + localStorage.setItem("config", JSON.stringify(DEFAULT_CONFIG)); + return DEFAULT_CONFIG; })(); +function saveConfig() { + localStorage.setItem("config", JSON.stringify(config)); +} + document.getElementById("toggle-crt").addEventListener("click", () => { config.crt = !config.crt; + setCRT(config.crt); + saveConfig(); }); -window.config = config; -export default config; +function setCRT(/** @type boolean */ enabled) { + if (enabled) { + document.body.classList.add("crt"); + } else { + document.body.classList.remove("crt"); + } + document.getElementById('toggle-crt').className = enabled ? "" : "disabled"; +} diff --git a/public/script/cursor.js b/public/script/cursor.js deleted file mode 100644 index ff87068..0000000 --- a/public/script/cursor.js +++ /dev/null @@ -1,403 +0,0 @@ -import config from './config.js'; - -const CURSOR_LERP_RATE = 1/100; -const CURSOR_FUNCHAR_RATE = 20; -const CURSOR_CHAR_MAX_LIFE = 5000; -const CURSOR_MAX_CHARS = 64; - -/** @type HTMLCanvasElement */ -let canvas; -/** @type CanvasRenderingContext2D */ -let ctx; -/** @type Cursor */ -let myCursor; -/** @type Map */ -let cursors = new Map(); - -/** @type WebSocket */ -let ws; - -let running = false; -let lastUpdate = 0; - -let cursorBoxHeight = 0; -let cursorBoxRadius = 0; -let cursorIDFontSize = 0; -let cursorCharFontSize = 0; - -class Cursor { - #funCharCooldown = CURSOR_FUNCHAR_RATE; - - /** - * @param {string} id - * @param {number} x - * @param {number} y - */ - constructor(id, x, y) { - this.id = id; - - // real coordinates (canonical) - this.x = x; - this.y = y; - // render coordinates (interpolated) - this.rx = x; - this.ry = y; - - this.msg = ''; - /** @type Array */ - this.funChars = new Array(); - this.colour = randomColour(); - this.click = false; - } - - /** - * @param {number} deltaTime - */ - update(deltaTime) { - this.rx += (this.x - this.rx) * CURSOR_LERP_RATE * deltaTime; - this.ry += (this.y - this.ry) * CURSOR_LERP_RATE * deltaTime; - - if (this.#funCharCooldown > 0) - this.#funCharCooldown -= deltaTime; - - const x = this.rx * innerWidth - scrollX; - const y = this.ry * innerHeight - scrollY; - const onBackground = ctx.fillStyle = getComputedStyle(document.body).getPropertyValue('--on-background'); - - if (config.cursorFunMode === true) { - if (this.msg.length > 0) { - if (this.#funCharCooldown <= 0) { - this.#funCharCooldown = CURSOR_FUNCHAR_RATE; - if (this.funChars.length >= CURSOR_MAX_CHARS) { - this.funChars.shift(); - } - const yOffset = -10 / innerHeight; - const accelMultiplier = 0.002; - this.funChars.push(new FunChar( - this.x, this.y + yOffset, - (this.x - this.rx) * accelMultiplier, (this.y - this.ry) * accelMultiplier, - this.msg)); - } - } - - this.funChars.forEach(char => { - if (char.life > CURSOR_CHAR_MAX_LIFE || - char.y - scrollY > innerHeight || - char.x < 0 || - char.x * innerWidth - scrollX > innerWidth - ) { - this.funChars = this.funChars.filter(c => c !== this); - return; - } - char.update(deltaTime); - }); - } else if (this.msg.length > 0) { - ctx.font = 'normal bold ' + cursorCharFontSize + 'px monospace'; - ctx.fillStyle = onBackground; - ctx.fillText( - this.msg, - (x + 6) * devicePixelRatio, - (y + -8) * devicePixelRatio); - } - - const lightTheme = matchMedia && matchMedia('(prefers-color-scheme: light)').matches; - - if (lightTheme) - ctx.filter = 'saturate(5) brightness(0.8)'; - - const idText = '0x' + this.id.toString(16).padStart(8, '0'); - const colour = this.click ? onBackground : this.colour; - - ctx.beginPath(); - ctx.roundRect( - (x) * devicePixelRatio, - (y) * devicePixelRatio, - (12 + 7.2 * idText.length) * devicePixelRatio, - cursorBoxHeight, - cursorBoxRadius); - ctx.closePath(); - ctx.fillStyle = lightTheme ? '#fff8' : '#0008'; - ctx.fill(); - ctx.strokeStyle = colour; - ctx.lineWidth = devicePixelRatio; - ctx.stroke(); - - ctx.font = cursorIDFontSize + 'px monospace'; - ctx.fillStyle = colour; - ctx.fillText( - idText, - (x + 6) * devicePixelRatio, - (y + 14) * devicePixelRatio); - - ctx.filter = ''; - } -} - -class FunChar { - /** - * @param {number} x - * @param {number} y - * @param {number} xa - * @param {number} ya - * @param {string} text - */ - constructor(x, y, xa, ya, text) { - this.x = x; - this.y = y; - this.xa = xa + Math.random() * .0005 - .00025; - this.ya = ya + Math.random() * -.00025; - this.r = this.xa * 1000; - this.ra = this.r * 0.01; - this.text = text; - this.life = 0; - } - - /** - * @param {number} deltaTime - */ - update(deltaTime) { - this.life += deltaTime; - - this.x += this.xa * deltaTime; - this.y += this.ya * deltaTime; - this.r += this.ra * deltaTime; - this.ya = Math.min(this.ya + 0.000001 * deltaTime, 10); - - const x = this.x * innerWidth - scrollX; - const y = this.y * innerHeight - scrollY; - - const translateOffset = { - x: (x + 7.2) * devicePixelRatio, - y: (y - 7.2) * devicePixelRatio, - }; - ctx.translate(translateOffset.x, translateOffset.y); - ctx.rotate(this.r); - ctx.translate(-translateOffset.x, -translateOffset.y); - - ctx.font = 'normal bold ' + cursorCharFontSize + 'px monospace'; - ctx.fillStyle = getComputedStyle(document.body).getPropertyValue('--on-background'); - ctx.fillText( - this.text, - x * devicePixelRatio, - y * devicePixelRatio); - - ctx.resetTransform(); - } -} - -/** - * @returns string - */ -function randomColour() { - const min = 128; - const range = 100; - const red = Math.round((min + Math.random() * range)).toString(16); - const green = Math.round((min + Math.random() * range)).toString(16); - const blue = Math.round((min + Math.random() * range)).toString(16); - - return '#' + red + green + blue; -} - -/** - * @param {MouseEvent} event - */ -let mouseMoveLock = false; -const mouseMoveCooldown = 1000/30; -function handleMouseMove(event) { - if (!myCursor) return; - - const x = event.pageX / innerWidth; - const y = event.pageY / innerHeight; - const f = 10000; // four digit floating-point precision - - if (!mouseMoveLock) { - mouseMoveLock = true; - if (ws && ws.readyState == WebSocket.OPEN) - ws.send(`pos:${Math.round(x * f) / f}:${Math.round(y * f) / f}`); - setTimeout(() => { - mouseMoveLock = false; - }, mouseMoveCooldown); - } - - myCursor.x = x; - myCursor.y = y; -} - -function handleMouseDown() { - myCursor.click = true; - if (ws && ws.readyState == WebSocket.OPEN) - ws.send('click:1'); -} -function handleMouseUp() { - myCursor.click = false; - if (ws && ws.readyState == WebSocket.OPEN) - ws.send('click:0'); -} - -/** - * @param {KeyboardEvent} event - */ -function handleKeyPress(event) { - if (event.key.length > 1) return; - if (event.metaKey || event.ctrlKey) return; - if (myCursor.msg === event.key) return; - if (ws && ws.readyState == WebSocket.OPEN) - ws.send(`char:${event.key}`); - myCursor.msg = event.key; -} - -function handleKeyUp() { - if (ws && ws.readyState == WebSocket.OPEN) - ws.send(`nochar`); - myCursor.msg = ''; -} - -/** - * @param {number} timestamp - */ -function update(timestamp) { - if (!running) return; - - const deltaTime = timestamp - lastUpdate; - lastUpdate = timestamp; - - ctx.clearRect(0, 0, canvas.width, canvas.height); - - cursors.forEach(cursor => { - cursor.update(deltaTime); - }); - - requestAnimationFrame(update); -} - -function handleWindowResize() { - canvas.width = innerWidth * devicePixelRatio; - canvas.height = innerHeight * devicePixelRatio; - cursorBoxHeight = 20 * devicePixelRatio; - cursorBoxRadius = 4 * devicePixelRatio; - cursorIDFontSize = 12 * devicePixelRatio; - cursorCharFontSize = 20 * devicePixelRatio; -} - -function cursorSetup() { - if (running) throw new Error('Only one instance of Cursor can run at a time.'); - running = true; - - canvas = document.createElement('canvas'); - canvas.id = 'cursors'; - handleWindowResize(); - document.body.appendChild(canvas); - - ctx = canvas.getContext('2d'); - - myCursor = new Cursor('You!', innerWidth / 2, innerHeight / 2); - cursors.set(0, myCursor); - - addEventListener('resize', handleWindowResize); - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mousedown', handleMouseDown); - document.addEventListener('mouseup', handleMouseUp); - document.addEventListener('keypress', handleKeyPress); - document.addEventListener('keyup', handleKeyUp); - - requestAnimationFrame(update); - - ws = new WebSocket('/cursor-ws'); - ws.addEventListener('open', () => { - console.log('Cursor connected to server successfully.'); - - ws.send(`loc:${location.pathname}`); - }); - ws.addEventListener('error', error => { - console.error('Cursor WebSocket error:', error); - }); - ws.addEventListener('close', () => { - console.log('Cursor connection closed.'); - }); - ws.addEventListener('message', event => { - const args = String(event.data).split(':'); - if (args.length == 0) return; - - let id = 0; - /** @type Cursor */ - let cursor; - if (args.length > 1) { - id = Number(args[1]); - cursor = cursors.get(id); - } - - switch (args[0]) { - case 'id': { - myCursor.id = id; - break; - } - case 'join': { - if (id === myCursor.id) break; - cursors.set(id, new Cursor(id, 0, 0)); - break; - } - case 'leave': { - if (!cursor || cursor === myCursor) break; - cursors.delete(id); - break; - } - case 'char': { - if (!cursor || cursor === myCursor) break; - cursor.msg = args[2]; - break; - } - case 'nochar': { - if (!cursor || cursor === myCursor) break; - cursor.msg = ''; - break; - } - case 'click': { - if (!cursor || cursor === myCursor) break; - cursor.click = args[2] == '1'; - break; - } - case 'pos': { - if (!cursor || cursor === myCursor) break; - cursor.x = Number(args[2]); - cursor.y = Number(args[3]); - break; - } - default: { - console.warn('Cursor: Unknown command received from server:', args[0]); - break; - } - } - }); - - console.log(`Cursor tracking @ ${location.pathname}`); -} - -function cursorDestroy() { - if (!running) return; - - removeEventListener('resize', handleWindowResize); - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mousedown', handleMouseDown); - document.removeEventListener('mouseup', handleMouseUp); - document.removeEventListener('keypress', handleKeyPress); - document.removeEventListener('keyup', handleKeyUp); - - ctx.clearRect(0, 0, canvas.width, canvas.height); - - cursors.clear(); - myCursor = null; - - console.log(`Cursor no longer tracking.`); - running = false; -} - -if (config.cursor === true) { - cursorSetup(); -} - -config.addListener('cursor', enabled => { - if (enabled === true) - cursorSetup(); - else - cursorDestroy(); -}); diff --git a/public/script/index.js b/public/script/index.js index 2197bd4..512ed8f 100644 --- a/public/script/index.js +++ b/public/script/index.js @@ -1,5 +1,3 @@ -import { hijackClickEvent } from "./main.js"; - const hexPrimary = document.getElementById("hex-primary"); const hexSecondary = document.getElementById("hex-secondary"); const hexTertiary = document.getElementById("hex-tertiary"); @@ -16,8 +14,3 @@ updateHexColours(); window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { updateHexColours(); }); - -document.querySelectorAll("ul#projects li.project-item").forEach(projectItem => { - const link = projectItem.querySelector('a'); - hijackClickEvent(projectItem, link); -}); diff --git a/public/script/main.js b/public/script/main.js index 19eddc2..95023e5 100644 --- a/public/script/main.js +++ b/public/script/main.js @@ -1,6 +1,5 @@ import "./header.js"; import "./config.js"; -import "./cursor.js"; function type_out(e) { const text = e.innerText; @@ -45,23 +44,6 @@ function fill_list(list) { }); } -export function hijackClickEvent(container, link) { - container.addEventListener('click', event => { - if (event.target.tagName.toLowerCase() === 'a') return; - event.preventDefault(); - link.dispatchEvent(new MouseEvent('click', { - bubbles: true, - cancelable: true, - view: window, - ctrlKey: event.ctrlKey, - metaKey: event.metaKey, - shiftKey: event.shiftKey, - altKey: event.altKey, - button: event.button, - })); - }); -} - document.addEventListener("DOMContentLoaded", () => { [...document.querySelectorAll(".typeout")] .filter((e) => e.innerText != "") diff --git a/public/script/music.js b/public/script/music.js index 91f34ab..273ce2b 100644 --- a/public/script/music.js +++ b/public/script/music.js @@ -1,6 +1,12 @@ -import { hijackClickEvent } from "./main.js"; +import "./main.js"; document.querySelectorAll("div.music").forEach(container => { - const link = container.querySelector(".music-title a") - hijackClickEvent(container, link); + const link = container.querySelector(".music-title a").href + + container.addEventListener("click", event => { + if (event.target.href) return; + + event.preventDefault(); + location = link; + }); }); diff --git a/public/style/colours.css b/public/style/colours.css index de63198..2bf607d 100644 --- a/public/style/colours.css +++ b/public/style/colours.css @@ -6,7 +6,6 @@ --secondary: #f8e05b; --tertiary: #f788fe; --links: #5eb2ff; - --live: #fd3737; } @media (prefers-color-scheme: light) { diff --git a/public/style/cursor.css b/public/style/cursor.css deleted file mode 100644 index 73019ee..0000000 --- a/public/style/cursor.css +++ /dev/null @@ -1,9 +0,0 @@ -canvas#cursors { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - pointer-events: none; - z-index: 100; -} diff --git a/public/style/header.css b/public/style/header.css index f399d4d..48971b5 100644 --- a/public/style/header.css +++ b/public/style/header.css @@ -25,6 +25,7 @@ nav { flex-grow: 1; display: flex; gap: .5em; + cursor: pointer; } img#header-icon { @@ -35,19 +36,19 @@ img#header-icon { } #header-text { + width: 11em; display: flex; flex-direction: column; justify-content: center; + flex-grow: 1; } #header-text h1 { - width: fit-content; margin: 0; font-size: 1em; } #header-text h2 { - width: fit-content; height: 1.2em; line-height: 1.2em; margin: 0; @@ -153,7 +154,7 @@ header ul li a:hover { flex-direction: column; gap: 1rem; border-bottom: 1px solid #888; - background: var(--background); + background: #080808; display: none; } diff --git a/public/style/index.css b/public/style/index.css index b970c17..f3cb761 100644 --- a/public/style/index.css +++ b/public/style/index.css @@ -96,36 +96,30 @@ hr { overflow: visible; } -ul.platform-links { - padding-left: 1em; +ul.links { display: flex; - gap: .5em; + gap: 1em .5em; flex-wrap: wrap; } -ul.platform-links li { +ul.links li { list-style: none; } -ul.platform-links li a { +ul.links li a { padding: .4em .5em; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - gap: .5em; border: 1px solid var(--links); color: var(--links); border-radius: 2px; background-color: transparent; - transition-property: color, border-color, background-color, box-shadow; + transition-property: color, border-color, background-color; transition-duration: .2s; animation-delay: 0s; animation: list-item-fadein .2s forwards; opacity: 0; } -ul.platform-links li a:hover { +ul.links li a:hover { color: #eee; border-color: #eee; background-color: var(--links) !important; @@ -133,75 +127,6 @@ ul.platform-links li a:hover { box-shadow: 0 0 1em var(--links); } -ul.platform-links li a img { - height: 1em; - width: 1em; -} - -ul#projects { - padding: 0; - list-style: none; -} - -li.project-item { - padding: .5em; - border: 1px solid var(--links); - margin: 1em 0; - display: flex; - flex-direction: row; - gap: .5em; - border-radius: 2px; - transition-property: color, border-color, background-color, box-shadow; - transition-duration: .2s; - cursor: pointer; -} -li.project-item a { - transition: color .2s linear; -} - -li.project-item:hover { - color: #eee; - border-color: #eee; - background-color: var(--links) !important; - text-decoration: none; - box-shadow: 0 0 1em var(--links); -} -li.project-item:hover a { - color: #eee; -} - -li.project-item .project-info { - display: flex; - flex-direction: column; - justify-content: center; -} - -li.project-item img.project-icon { - width: 2.5em; - height: 2.5em; - object-fit: cover; - border-radius: 2px; -} - -li.project-item span.project-icon { - font-size: 2em; - display: block; - width: 45px; - height: 45px; - text-align: center; - /* background: #0004; */ - /* border: 1px solid var(--on-background); */ - border-radius: 2px; -} - -li.project-item a { - text-decoration: none; -} - -li.project-item p { - margin: 0; -} - div#web-buttons { margin: 2rem 0; } @@ -223,112 +148,3 @@ div#web-buttons { box-shadow: 1px 1px 0 #eee, 2px 2px 0 #eee; } -#live-banner { - margin: 1em 0 2em 0; - padding: 1em; - border-radius: 4px; - border: 1px solid var(--primary); - box-shadow: 0 0 8px var(--primary); -} - -#live-banner p { - margin: 0; -} - -.live-highlight { - color: var(--primary); -} - -.live-preview { - display: flex; - flex-direction: row; - justify-content: start; - gap: 1em; -} - -.live-preview div:first-of-type { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - gap: .3em; -} - -.live-thumbnail { - border-radius: 4px; -} - -.live-button { - margin: .2em; - padding: .4em .5em; - display: inline-block; - color: var(--primary); - border: 1px solid var(--primary); - border-radius: 4px; - transition: color .1s linear, background-color .1s linear, box-shadow .1s linear; -} - -.live-button:hover { - color: var(--background); - background-color: var(--primary); - box-shadow: 0 0 8px var(--primary); - text-decoration: none; -} - -.live-info { - display: flex; - flex-direction: column; - gap: .3em; - overflow-x: hidden; -} - -#live-banner h2 { - margin: 0; - color: var(--on-background); - font-family: 'Inter', sans-serif; - font-weight: 800; - font-style: italic; -} - -.live-pinger { - width: .5em; - height: .5em; - margin: .1em .2em; - display: inline-block; - border-radius: 100%; - background-color: var(--primary); - box-shadow: 0 0 4px var(--primary); - animation: live-pinger-pulse 1s infinite alternate ease-in-out; -} - -@keyframes live-pinger-pulse { - from { - opacity: .8; - transform: scale(1.0); - } - to { - opacity: 1; - transform: scale(1.1); - } -} - -.live-game { - overflow: hidden; - text-wrap: nowrap; - text-overflow: ellipsis; -} - -.live-game .live-game-prefix { - opacity: .8; -} - -.live-title { - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.live-viewers { - opacity: .5; -} diff --git a/public/style/main.css b/public/style/main.css index 680ee2b..f7f2131 100644 --- a/public/style/main.css +++ b/public/style/main.css @@ -2,8 +2,6 @@ @import url("/style/header.css"); @import url("/style/footer.css"); @import url("/style/prideflag.css"); -@import url("/style/cursor.css"); -@import url("/font/inter/inter.css"); @font-face { font-family: "Monaspace Argon"; diff --git a/schema-migration/000-init.sql b/schema-migration/000-init.sql index 90385ac..f70dee6 100644 --- a/schema-migration/000-init.sql +++ b/schema-migration/000-init.sql @@ -18,9 +18,7 @@ CREATE TABLE arimelody.account ( password TEXT NOT NULL, email TEXT, avatar_url TEXT, - created_at TIMESTAMP NOT NULL DEFAULT current_timestamp, - fail_attempts INT NOT NULL DEFAULT 0, - locked BOOLEAN DEFAULT false + created_at TIMESTAMP NOT NULL DEFAULT current_timestamp ); ALTER TABLE arimelody.account ADD CONSTRAINT account_pk PRIMARY KEY (id); @@ -86,7 +84,7 @@ CREATE TABLE arimelody.musicrelease ( buylink text, copyright text, copyrightURL text, - created_at TIMESTAMP NOT NULL DEFAULT current_timestamp + created_at TIMESTAMP NOT NULL DEFAULT current_timestamp, ); ALTER TABLE arimelody.musicrelease ADD CONSTRAINT musicrelease_pk PRIMARY KEY (id); diff --git a/schema-migration/003-fail-lock.sql b/schema-migration/003-fail-lock.sql deleted file mode 100644 index 32d19bf..0000000 --- a/schema-migration/003-fail-lock.sql +++ /dev/null @@ -1,3 +0,0 @@ --- it would be nice to prevent brute-forcing -ALTER TABLE arimelody.account ADD COLUMN fail_attempts INT NOT NULL DEFAULT 0; -ALTER TABLE arimelody.account ADD COLUMN locked BOOLEAN DEFAULT false; diff --git a/templates/templates.go b/templates/templates.go index 752c78d..8d1a5ca 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -1,8 +1,8 @@ package templates import ( - "html/template" - "path/filepath" + "html/template" + "path/filepath" ) var IndexTemplate = template.Must(template.ParseFiles( diff --git a/view/index.go b/view/index.go deleted file mode 100644 index b6e3891..0000000 --- a/view/index.go +++ /dev/null @@ -1,45 +0,0 @@ -package view - -import ( - "arimelody-web/controller" - "arimelody-web/model" - "arimelody-web/templates" - "fmt" - "net/http" - "os" -) - -func IndexHandler(app *model.AppState) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodHead { - w.WriteHeader(http.StatusOK) - return - } - - type IndexData struct { - TwitchStatus *model.TwitchStreamInfo - } - - var err error - var twitchStatus *model.TwitchStreamInfo = nil - if app.Twitch != nil && len(app.Config.Twitch.Broadcaster) > 0 { - twitchStatus, err = controller.GetTwitchStatus(app, app.Config.Twitch.Broadcaster) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to get Twitch status for %s: %v\n", app.Config.Twitch.Broadcaster, err) - } - } - - if r.URL.Path == "/" || r.URL.Path == "/index.html" { - err := templates.IndexTemplate.Execute(w, IndexData{ - TwitchStatus: twitchStatus, - }) - if err != nil { - fmt.Fprintf(os.Stderr, "WARN: Failed to render index page: %v\n", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } - return - } - - StaticHandler("public").ServeHTTP(w, r) - }) -} diff --git a/view/music.go b/view/music.go index 8ed5279..2d40ef0 100644 --- a/view/music.go +++ b/view/music.go @@ -1,13 +1,13 @@ package view import ( - "fmt" - "net/http" - "os" + "fmt" + "net/http" + "os" - "arimelody-web/controller" - "arimelody-web/model" - "arimelody-web/templates" + "arimelody-web/controller" + "arimelody-web/model" + "arimelody-web/templates" ) // HTTP HANDLER METHODS @@ -60,7 +60,7 @@ func ServeGateway(app *model.AppState, release *model.Release) http.Handler { // only allow authorised users to view hidden releases privileged := false if !release.Visible { - session, err := controller.GetSessionFromRequest(app, r) + session, err := controller.GetSessionFromRequest(app.DB, r) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve session: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) diff --git a/view/static.go b/view/static.go deleted file mode 100644 index 52263a2..0000000 --- a/view/static.go +++ /dev/null @@ -1,31 +0,0 @@ -package view - -import ( - "errors" - "net/http" - "os" - "path/filepath" -) - -func StaticHandler(directory string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - info, err := os.Stat(filepath.Join(directory, filepath.Clean(r.URL.Path))) - - // does the file exist? - if err != nil { - if errors.Is(err, os.ErrNotExist) { - http.NotFound(w, r) - return - } - } - - // is thjs a directory? (forbidden) - if info.IsDir() { - http.NotFound(w, r) - return - } - - http.FileServer(http.Dir(directory)).ServeHTTP(w, r) - }) -} - diff --git a/views/footer.html b/views/footer.html index eccf125..217c4b5 100644 --- a/views/footer.html +++ b/views/footer.html @@ -2,12 +2,7 @@
diff --git a/views/header.html b/views/header.html index 03a8384..291b8ac 100644 --- a/views/header.html +++ b/views/header.html @@ -23,6 +23,9 @@
  • music
  • +
  • + source +
  • blog diff --git a/views/index.html b/views/index.html index df4d341..636c369 100644 --- a/views/index.html +++ b/views/index.html @@ -6,8 +6,8 @@ - - + + @@ -17,28 +17,11 @@ - + {{end}} {{define "content"}}
    - {{if .TwitchStatus}} -
    -
    -
    - livestream thumbnail - join in! -
    -
    -

    ari melody LIVE

    -

    streaming: {{.TwitchStatus.GameName}}

    -

    {{.TwitchStatus.Title}}

    -

    {{.TwitchStatus.ViewerCount}} viewers

    -
    -
    -
    - {{end}} -

    # hello, world!

    @@ -50,7 +33,7 @@

    - i'm a musician, developer, + i'm a musician, developer, streamer, youtuber, and probably a bunch of other things i forgot to mention!

    @@ -61,7 +44,7 @@

    if you're looking to support me financially, that's so cool of you!! if you like, you can buy some of my music over on - bandcamp + bandcamp so you can at least get something for your money. thank you very much either way!! 💕

    @@ -101,97 +84,57 @@

    where to find me 🛰️

    -