From 1edc051ae21f8cc64d2d00796ad04dc8305d8dec Mon Sep 17 00:00:00 2001 From: ari melody Date: Sun, 26 Jan 2025 00:48:19 +0000 Subject: [PATCH 01/52] fixed GetTOTP, started rough QR code implementation GetTOTP handles TOTP method retrieval for confirmation and deletion. QR code implementation looks like it's gonna suck, so might end up using a library for this later. --- admin/accounthttp.go | 11 +++-- admin/http.go | 16 +++++-- controller/qr.go | 107 +++++++++++++++++++++++++++++++++++++++++++ controller/totp.go | 9 ++-- main.go | 2 +- 5 files changed, 132 insertions(+), 13 deletions(-) create mode 100644 controller/qr.go diff --git a/admin/accounthttp.go b/admin/accounthttp.go index 9402410..aa1e042 100644 --- a/admin/accounthttp.go +++ b/admin/accounthttp.go @@ -304,6 +304,12 @@ func totpConfirmHandler(app *model.AppState) http.Handler { return } + fmt.Printf( + "TOTP:\n\tName: %s\n\tSecret: %s\n", + totp.Name, + totp.Secret, + ) + confirmCode := controller.GenerateTOTP(totp.Secret, 0) if code != confirmCode { confirmCodeOffset := controller.GenerateTOTP(totp.Secret, 1) @@ -330,12 +336,11 @@ func totpDeleteHandler(app *model.AppState) http.Handler { return } - name := r.URL.Path - fmt.Printf("%s\n", name); - if len(name) == 0 { + if len(r.URL.Path) < 2 { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } + name := r.URL.Path[1:] session := r.Context().Value("session").(*model.Session) diff --git a/admin/http.go b/admin/http.go index b6a71ee..42b7e46 100644 --- a/admin/http.go +++ b/admin/http.go @@ -19,6 +19,17 @@ import ( func Handler(app *model.AppState) http.Handler { mux := http.NewServeMux() + mux.Handle("/qr-test", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + qrB64Img, err := controller.GenerateQRCode([]byte("super epic mega gaming test message. be sure to buy free2play on bandcamp so i can put food on my family")) + if err != nil { + fmt.Fprintf(os.Stderr, "WARN: Failed to generate QR code: %v\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + w.Write([]byte("")) + })) + mux.Handle("/login", loginHandler(app)) mux.Handle("/logout", requireAccount(app, logoutHandler(app))) @@ -243,11 +254,6 @@ func loginHandler(app *model.AppState) http.Handler { return } - // new accounts won't have TOTP methods at first. there should be a - // second phase of login that prompts the user for a TOTP *only* - // if that account has a TOTP method. - // TODO: login phases (username & password -> TOTP) - type LoginRequest struct { Username string `json:"username"` Password string `json:"password"` diff --git a/controller/qr.go b/controller/qr.go new file mode 100644 index 0000000..c4c2520 --- /dev/null +++ b/controller/qr.go @@ -0,0 +1,107 @@ +package controller + +import ( + "bytes" + "encoding/base64" + "errors" + "fmt" + "image" + "image/color" + "image/png" +) + +const margin = 4 + +type QRCodeECCLevel int64 +const ( + LOW QRCodeECCLevel = iota + MEDIUM + QUARTILE + HIGH +) + +func GenerateQRCode(data []byte) (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 { + if (xi == 0 || xi == 6) || (yi == 0 || yi == 6) { + img.Set(x + xi, y + yi, color.Black) + } else if (xi > 1 && xi < 5) && (yi > 1 && yi < 5) { + img.Set(x + xi, y + yi, color.Black) + } + } + } +} + +func drawSmallAlignmentSquare(x int, y int, img *image.Gray) { + for yi := range 5 { + for xi := range 5 { + if (xi == 0 || xi == 4) || (yi == 0 || yi == 4) { + img.Set(x + xi, y + yi, color.Black) + } + } + } + img.Set(x + 2, y + 2, color.Black) +} diff --git a/controller/totp.go b/controller/totp.go index bc71747..dbbeec7 100644 --- a/controller/totp.go +++ b/controller/totp.go @@ -64,9 +64,9 @@ func GenerateTOTPURI(username string, secret string) string { query := url.Query() query.Set("secret", secret) query.Set("issuer", "arimelody.me") - query.Set("algorithm", "SHA1") - query.Set("digits", fmt.Sprintf("%d", TOTP_CODE_LENGTH)) - query.Set("period", fmt.Sprintf("%d", TOTP_TIME_STEP)) + // query.Set("algorithm", "SHA1") + // query.Set("digits", fmt.Sprintf("%d", TOTP_CODE_LENGTH)) + // query.Set("period", fmt.Sprintf("%d", TOTP_TIME_STEP)) url.RawQuery = query.Encode() return url.String() @@ -116,8 +116,9 @@ func GetTOTP(db *sqlx.DB, accountID string, name string) (*model.TOTP, error) { err := db.Get( &totp, "SELECT * FROM totp " + - "WHERE account=$1", + "WHERE account=$1 AND name=$2", accountID, + name, ) if err != nil { if strings.Contains(err.Error(), "no rows") { diff --git a/main.go b/main.go index 251d9a9..ac1de59 100644 --- a/main.go +++ b/main.go @@ -89,7 +89,6 @@ func main() { } username := os.Args[2] totpName := os.Args[3] - secret := controller.GenerateTOTPSecret(controller.TOTP_SECRET_LENGTH) account, err := controller.GetAccountByUsername(app.DB, username) if err != nil { @@ -102,6 +101,7 @@ func main() { os.Exit(1) } + secret := controller.GenerateTOTPSecret(controller.TOTP_SECRET_LENGTH) totp := model.TOTP { AccountID: account.ID, Name: totpName, From 3450d879acd0662f8bfae1d010f6e02ee9441e06 Mon Sep 17 00:00:00 2001 From: ari melody Date: Sun, 26 Jan 2025 20:09:18 +0000 Subject: [PATCH 02/52] QR codes complete, account settings finished! + refactored templates a little; this might need more work! --- admin/accounthttp.go | 31 ++++--- admin/artisthttp.go | 2 +- admin/http.go | 12 +-- admin/releasehttp.go | 16 ++-- admin/templates.go | 150 ++++++++++++++++++---------------- admin/trackhttp.go | 2 +- admin/views/totp-confirm.html | 17 ++-- controller/qr.go | 15 +++- go.mod | 6 +- go.sum | 6 +- main.go | 2 +- templates/templates.go | 47 +++++------ view/music.go | 4 +- 13 files changed, 175 insertions(+), 135 deletions(-) diff --git a/admin/accounthttp.go b/admin/accounthttp.go index aa1e042..408b4c5 100644 --- a/admin/accounthttp.go +++ b/admin/accounthttp.go @@ -63,7 +63,7 @@ func accountIndexHandler(app *model.AppState) http.Handler { session.Message = sessionMessage session.Error = sessionError - err = pages["account"].Execute(w, accountResponse{ + err = accountTemplate.Execute(w, accountResponse{ Session: session, TOTPs: totps, }) @@ -199,7 +199,7 @@ func totpSetupHandler(app *model.AppState) http.Handler { session := r.Context().Value("session").(*model.Session) - err := pages["totp-setup"].Execute(w, totpSetupData{ Session: session }) + err := totpSetupTemplate.Execute(w, totpSetupData{ Session: session }) if err != nil { fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -216,6 +216,7 @@ func totpSetupHandler(app *model.AppState) http.Handler { Session *model.Session TOTP *model.TOTP NameEscaped string + QRBase64Image string } err := r.ParseForm() @@ -242,7 +243,7 @@ func totpSetupHandler(app *model.AppState) http.Handler { if err != nil { fmt.Printf("WARN: Failed to create TOTP method: %s\n", err) controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") - err := pages["totp-setup"].Execute(w, totpSetupData{ Session: session }) + err := totpSetupTemplate.Execute(w, totpSetupData{ Session: session }) if err != nil { fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -250,10 +251,24 @@ func totpSetupHandler(app *model.AppState) http.Handler { return } - err = pages["totp-confirm"].Execute(w, totpSetupData{ + qrBase64Image, err := controller.GenerateQRCode( + controller.GenerateTOTPURI(session.Account.Username, totp.Secret)) + if err != nil { + fmt.Printf("WARN: Failed to generate TOTP setup QR code: %s\n", err) + controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") + err := totpSetupTemplate.Execute(w, totpSetupData{ Session: session }) + if err != nil { + fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + return + } + + err = totpConfirmTemplate.Execute(w, totpSetupData{ Session: session, TOTP: &totp, NameEscaped: url.PathEscape(totp.Name), + QRBase64Image: qrBase64Image, }) if err != nil { fmt.Printf("WARN: Failed to render TOTP confirm page: %s\n", err) @@ -304,18 +319,12 @@ func totpConfirmHandler(app *model.AppState) http.Handler { return } - fmt.Printf( - "TOTP:\n\tName: %s\n\tSecret: %s\n", - totp.Name, - totp.Secret, - ) - confirmCode := controller.GenerateTOTP(totp.Secret, 0) if code != confirmCode { confirmCodeOffset := controller.GenerateTOTP(totp.Secret, 1) if code != confirmCodeOffset { controller.SetSessionError(app.DB, session, "Incorrect TOTP code. Please try again.") - err = pages["totp-confirm"].Execute(w, totpConfirmData{ + err = totpConfirmTemplate.Execute(w, totpConfirmData{ Session: session, TOTP: totp, }) diff --git a/admin/artisthttp.go b/admin/artisthttp.go index 5979493..6dfbbfd 100644 --- a/admin/artisthttp.go +++ b/admin/artisthttp.go @@ -39,7 +39,7 @@ func serveArtist(app *model.AppState) http.Handler { session := r.Context().Value("session").(*model.Session) - err = pages["artist"].Execute(w, ArtistResponse{ + err = artistTemplate.Execute(w, ArtistResponse{ Session: session, Artist: artist, Credits: credits, diff --git a/admin/http.go b/admin/http.go index 42b7e46..6fd8f59 100644 --- a/admin/http.go +++ b/admin/http.go @@ -20,7 +20,7 @@ func Handler(app *model.AppState) http.Handler { mux := http.NewServeMux() mux.Handle("/qr-test", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - qrB64Img, err := controller.GenerateQRCode([]byte("super epic mega gaming test message. be sure to buy free2play on bandcamp so i can put food on my family")) + qrB64Img, err := controller.GenerateQRCode("super epic mega gaming test message. be sure to buy free2play on bandcamp so i can put food on my family") if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to generate QR code: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -87,7 +87,7 @@ func AdminIndexHandler(app *model.AppState) http.Handler { Tracks []*model.Track } - err = pages["index"].Execute(w, IndexData{ + err = indexTemplate.Execute(w, IndexData{ Session: session, Releases: releases, Artists: artists, @@ -116,7 +116,7 @@ func registerAccountHandler(app *model.AppState) http.Handler { } render := func() { - err := pages["register"].Execute(w, registerData{ Session: session }) + err := registerTemplate.Execute(w, registerData{ Session: session }) if err != nil { fmt.Printf("WARN: Error rendering create account page: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -229,7 +229,7 @@ func loginHandler(app *model.AppState) http.Handler { } render := func() { - err := pages["login"].Execute(w, loginData{ Session: session }) + err := loginTemplate.Execute(w, loginData{ Session: session }) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Error rendering admin login page: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -307,7 +307,7 @@ func loginHandler(app *model.AppState) http.Handler { Username string Password string } - err = pages["login-totp"].Execute(w, loginTOTPData{ + err = loginTOTPTemplate.Execute(w, loginTOTPData{ Session: session, Username: credentials.Username, Password: credentials.Password, @@ -379,7 +379,7 @@ func logoutHandler(app *model.AppState) http.Handler { Path: "/", }) - err = pages["logout"].Execute(w, nil) + err = logoutTemplate.Execute(w, nil) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to render logout page: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) diff --git a/admin/releasehttp.go b/admin/releasehttp.go index cd73e98..be5052b 100644 --- a/admin/releasehttp.go +++ b/admin/releasehttp.go @@ -60,7 +60,7 @@ func serveRelease(app *model.AppState) http.Handler { Release *model.Release } - err = pages["release"].Execute(w, ReleaseResponse{ + err = releaseTemplate.Execute(w, ReleaseResponse{ Session: session, Release: release, }) @@ -74,7 +74,7 @@ func serveRelease(app *model.AppState) http.Handler { func serveEditCredits(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") - err := components["editcredits"].Execute(w, release) + err := editCreditsTemplate.Execute(w, release) if err != nil { fmt.Printf("Error rendering edit credits component for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -97,7 +97,7 @@ func serveAddCredit(app *model.AppState, release *model.Release) http.Handler { } w.Header().Set("Content-Type", "text/html") - err = components["addcredit"].Execute(w, response{ + err = addCreditTemplate.Execute(w, response{ ReleaseID: release.ID, Artists: artists, }) @@ -123,7 +123,7 @@ func serveNewCredit(app *model.AppState) http.Handler { } w.Header().Set("Content-Type", "text/html") - err = components["newcredit"].Execute(w, artist) + err = newCreditTemplate.Execute(w, artist) if err != nil { fmt.Printf("Error rendering new credit component for %s: %s\n", artist.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -134,7 +134,7 @@ func serveNewCredit(app *model.AppState) http.Handler { func serveEditLinks(release *model.Release) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") - err := components["editlinks"].Execute(w, release) + err := editLinksTemplate.Execute(w, release) if err != nil { fmt.Printf("Error rendering edit links component for %s: %s\n", release.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -151,7 +151,7 @@ func serveEditTracks(release *model.Release) http.Handler { Add func(a int, b int) int } - err := components["edittracks"].Execute(w, editTracksData{ + err := editTracksTemplate.Execute(w, editTracksData{ Release: release, Add: func(a, b int) int { return a + b }, }) @@ -177,7 +177,7 @@ func serveAddTrack(app *model.AppState, release *model.Release) http.Handler { } w.Header().Set("Content-Type", "text/html") - err = components["addtrack"].Execute(w, response{ + err = addTrackTemplate.Execute(w, response{ ReleaseID: release.ID, Tracks: tracks, }) @@ -204,7 +204,7 @@ func serveNewTrack(app *model.AppState) http.Handler { } w.Header().Set("Content-Type", "text/html") - err = components["newtrack"].Execute(w, track) + err = newTrackTemplate.Execute(w, track) if err != nil { fmt.Printf("Error rendering new track component for %s: %s\n", track.ID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) diff --git a/admin/templates.go b/admin/templates.go index d9a74ca..49c118b 100644 --- a/admin/templates.go +++ b/admin/templates.go @@ -1,80 +1,90 @@ package admin import ( - "html/template" - "path/filepath" + "html/template" + "path/filepath" ) -var pages = map[string]*template.Template{ - "index": template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("admin", "components", "release", "release-list-item.html"), - filepath.Join("admin", "views", "index.html"), - )), +var indexTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "components", "release", "release-list-item.html"), + filepath.Join("admin", "views", "index.html"), +)) - "login": template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("admin", "views", "login.html"), - )), - "login-totp": template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("admin", "views", "login-totp.html"), - )), - "register": template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("admin", "views", "register.html"), - )), - "logout": template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("admin", "views", "logout.html"), - )), - "account": template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("admin", "views", "edit-account.html"), - )), - "totp-setup": template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("admin", "views", "totp-setup.html"), - )), - "totp-confirm": template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("admin", "views", "totp-confirm.html"), - )), +var loginTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "views", "login.html"), +)) +var loginTOTPTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "views", "login-totp.html"), +)) +var registerTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "views", "register.html"), +)) +var logoutTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "views", "logout.html"), +)) +var accountTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "views", "edit-account.html"), +)) +var totpSetupTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "views", "totp-setup.html"), +)) +var totpConfirmTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "views", "totp-confirm.html"), +)) - "release": template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("admin", "views", "edit-release.html"), - )), - "artist": template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("admin", "views", "edit-artist.html"), - )), - "track": template.Must(template.ParseFiles( - filepath.Join("admin", "views", "layout.html"), - filepath.Join("views", "prideflag.html"), - filepath.Join("admin", "components", "release", "release-list-item.html"), - filepath.Join("admin", "views", "edit-track.html"), - )), -} +var releaseTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "views", "edit-release.html"), +)) +var artistTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "views", "edit-artist.html"), +)) +var trackTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "views", "layout.html"), + filepath.Join("views", "prideflag.html"), + filepath.Join("admin", "components", "release", "release-list-item.html"), + filepath.Join("admin", "views", "edit-track.html"), +)) -var components = map[string]*template.Template{ - "editcredits": template.Must(template.ParseFiles(filepath.Join("admin", "components", "credits", "editcredits.html"))), - "addcredit": template.Must(template.ParseFiles(filepath.Join("admin", "components", "credits", "addcredit.html"))), - "newcredit": template.Must(template.ParseFiles(filepath.Join("admin", "components", "credits", "newcredit.html"))), +var editCreditsTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "components", "credits", "editcredits.html"), +)) +var addCreditTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "components", "credits", "addcredit.html"), +)) +var newCreditTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "components", "credits", "newcredit.html"), +)) - "editlinks": template.Must(template.ParseFiles(filepath.Join("admin", "components", "links", "editlinks.html"))), +var editLinksTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "components", "links", "editlinks.html"), +)) - "edittracks": template.Must(template.ParseFiles(filepath.Join("admin", "components", "tracks", "edittracks.html"))), - "addtrack": template.Must(template.ParseFiles(filepath.Join("admin", "components", "tracks", "addtrack.html"))), - "newtrack": template.Must(template.ParseFiles(filepath.Join("admin", "components", "tracks", "newtrack.html"))), -} +var editTracksTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "components", "tracks", "edittracks.html"), +)) +var addTrackTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "components", "tracks", "addtrack.html"), +)) +var newTrackTemplate = template.Must(template.ParseFiles( + filepath.Join("admin", "components", "tracks", "newtrack.html"), +)) diff --git a/admin/trackhttp.go b/admin/trackhttp.go index 9436671..a92f81a 100644 --- a/admin/trackhttp.go +++ b/admin/trackhttp.go @@ -39,7 +39,7 @@ func serveTrack(app *model.AppState) http.Handler { session := r.Context().Value("session").(*model.Session) - err = pages["track"].Execute(w, TrackResponse{ + err = trackTemplate.Execute(w, TrackResponse{ Session: session, Track: track, Releases: releases, diff --git a/admin/views/totp-confirm.html b/admin/views/totp-confirm.html index ac39e52..b0810e2 100644 --- a/admin/views/totp-confirm.html +++ b/admin/views/totp-confirm.html @@ -3,6 +3,9 @@ \ No newline at end of file diff --git a/public/img/brand/twitch.svg b/public/img/brand/twitch.svg new file mode 100644 index 0000000..3120fea --- /dev/null +++ b/public/img/brand/twitch.svg @@ -0,0 +1,21 @@ + + + + +Asset 2 + + + + + + + + + + + diff --git a/public/img/brand/youtube.svg b/public/img/brand/youtube.svg new file mode 100644 index 0000000..3286071 --- /dev/null +++ b/public/img/brand/youtube.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/script/index.js b/public/script/index.js index 512ed8f..2197bd4 100644 --- a/public/script/index.js +++ b/public/script/index.js @@ -1,3 +1,5 @@ +import { hijackClickEvent } from "./main.js"; + const hexPrimary = document.getElementById("hex-primary"); const hexSecondary = document.getElementById("hex-secondary"); const hexTertiary = document.getElementById("hex-tertiary"); @@ -14,3 +16,8 @@ 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 c1d5101..19eddc2 100644 --- a/public/script/main.js +++ b/public/script/main.js @@ -45,6 +45,23 @@ 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 273ce2b..91f34ab 100644 --- a/public/script/music.js +++ b/public/script/music.js @@ -1,12 +1,6 @@ -import "./main.js"; +import { hijackClickEvent } from "./main.js"; document.querySelectorAll("div.music").forEach(container => { - const link = container.querySelector(".music-title a").href - - container.addEventListener("click", event => { - if (event.target.href) return; - - event.preventDefault(); - location = link; - }); + const link = container.querySelector(".music-title a") + hijackClickEvent(container, link); }); diff --git a/public/style/index.css b/public/style/index.css index f3cb761..6edb362 100644 --- a/public/style/index.css +++ b/public/style/index.css @@ -96,30 +96,36 @@ hr { overflow: visible; } -ul.links { +ul.platform-links { + padding-left: 1em; display: flex; - gap: 1em .5em; + gap: .5em; flex-wrap: wrap; } -ul.links li { +ul.platform-links li { list-style: none; } -ul.links li a { +ul.platform-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; + transition-property: color, border-color, background-color, box-shadow; transition-duration: .2s; animation-delay: 0s; animation: list-item-fadein .2s forwards; opacity: 0; } -ul.links li a:hover { +ul.platform-links li a:hover { color: #eee; border-color: #eee; background-color: var(--links) !important; @@ -127,6 +133,75 @@ ul.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; } @@ -147,4 +222,3 @@ div#web-buttons { transform: translate(-2px, -2px); box-shadow: 1px 1px 0 #eee, 2px 2px 0 #eee; } - diff --git a/views/index.html b/views/index.html index 070d648..f639c55 100644 --- a/views/index.html +++ b/views/index.html @@ -17,7 +17,7 @@ - + {{end}} {{define "content"}} @@ -33,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!

@@ -44,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!! 💕

@@ -84,65 +84,119 @@

where to find me 🛰️

+ +

projects i've worked on 🛠️

-