Compare commits

..

7 commits

55 changed files with 371 additions and 310 deletions

View file

@ -2,10 +2,10 @@ EXEC = arimelody-web
.PHONY: $(EXEC) .PHONY: $(EXEC)
$(EXEC): build:
GOOS=linux GOARCH=amd64 go build -o $(EXEC) GOOS=linux GOARCH=amd64 go build -o $(EXEC)
bundle: $(EXEC) bundle: build
tar czf $(EXEC).tar.gz $(EXEC) admin/components/ admin/views/ admin/static/ views/ public/ schema-migration/ tar czf $(EXEC).tar.gz $(EXEC) admin/components/ admin/views/ admin/static/ views/ public/ schema-migration/
clean: clean:

View file

@ -1,17 +1,18 @@
package admin package admin
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"arimelody-web/controller" "arimelody-web/admin/templates"
"arimelody-web/log" "arimelody-web/controller"
"arimelody-web/model" "arimelody-web/log"
"arimelody-web/model"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
func accountHandler(app *model.AppState) http.Handler { func accountHandler(app *model.AppState) http.Handler {
@ -64,7 +65,7 @@ func accountIndexHandler(app *model.AppState) http.Handler {
session.Message = sessionMessage session.Message = sessionMessage
session.Error = sessionError session.Error = sessionError
err = accountTemplate.Execute(w, accountResponse{ err = templates.AccountTemplate.Execute(w, accountResponse{
Session: session, Session: session,
TOTPs: totps, TOTPs: totps,
}) })
@ -184,7 +185,7 @@ func totpSetupHandler(app *model.AppState) http.Handler {
session := r.Context().Value("session").(*model.Session) session := r.Context().Value("session").(*model.Session)
err := totpSetupTemplate.Execute(w, totpSetupData{ Session: session }) err := templates.TOTPSetupTemplate.Execute(w, totpSetupData{ Session: session })
if err != nil { if err != nil {
fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err) fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -221,7 +222,7 @@ func totpSetupHandler(app *model.AppState) http.Handler {
if err != nil { if err != nil {
fmt.Printf("WARN: Failed to create TOTP method: %s\n", err) fmt.Printf("WARN: Failed to create TOTP method: %s\n", err)
controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
err := totpSetupTemplate.Execute(w, totpConfirmData{ Session: session }) err := templates.TOTPSetupTemplate.Execute(w, totpConfirmData{ Session: session })
if err != nil { if err != nil {
fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err) fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -235,7 +236,7 @@ func totpSetupHandler(app *model.AppState) http.Handler {
fmt.Fprintf(os.Stderr, "WARN: Failed to generate TOTP QR code: %v\n", err) fmt.Fprintf(os.Stderr, "WARN: Failed to generate TOTP QR code: %v\n", err)
} }
err = totpConfirmTemplate.Execute(w, totpConfirmData{ err = templates.TOTPConfirmTemplate.Execute(w, totpConfirmData{
Session: session, Session: session,
TOTP: &totp, TOTP: &totp,
NameEscaped: url.PathEscape(totp.Name), NameEscaped: url.PathEscape(totp.Name),
@ -296,7 +297,7 @@ func totpConfirmHandler(app *model.AppState) http.Handler {
confirmCodeOffset := controller.GenerateTOTP(totp.Secret, 1) confirmCodeOffset := controller.GenerateTOTP(totp.Secret, 1)
if code != confirmCodeOffset { if code != confirmCodeOffset {
session.Error = sql.NullString{ Valid: true, String: "Incorrect TOTP code. Please try again." } session.Error = sql.NullString{ Valid: true, String: "Incorrect TOTP code. Please try again." }
err = totpConfirmTemplate.Execute(w, totpConfirmData{ err = templates.TOTPConfirmTemplate.Execute(w, totpConfirmData{
Session: session, Session: session,
TOTP: totp, TOTP: totp,
NameEscaped: url.PathEscape(totp.Name), NameEscaped: url.PathEscape(totp.Name),

View file

@ -1,12 +1,13 @@
package admin package admin
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"arimelody-web/model" "arimelody-web/admin/templates"
"arimelody-web/controller" "arimelody-web/controller"
"arimelody-web/model"
) )
func serveArtist(app *model.AppState) http.Handler { func serveArtist(app *model.AppState) http.Handler {
@ -39,7 +40,7 @@ func serveArtist(app *model.AppState) http.Handler {
session := r.Context().Value("session").(*model.Session) session := r.Context().Value("session").(*model.Session)
err = artistTemplate.Execute(w, ArtistResponse{ err = templates.EditArtistTemplate.Execute(w, ArtistResponse{
Session: session, Session: session,
Artist: artist, Artist: artist,
Credits: credits, Credits: credits,

View file

@ -1,20 +1,24 @@
package admin package admin
import ( import (
"context" "context"
"database/sql" "database/sql"
"fmt" "embed"
"net/http" "fmt"
"os" "mime"
"path/filepath" "net/http"
"strings" "os"
"time" "path"
"path/filepath"
"strings"
"time"
"arimelody-web/controller" "arimelody-web/admin/templates"
"arimelody-web/log" "arimelody-web/controller"
"arimelody-web/model" "arimelody-web/log"
"arimelody-web/model"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
func Handler(app *model.AppState) http.Handler { func Handler(app *model.AppState) http.Handler {
@ -91,7 +95,7 @@ func AdminIndexHandler(app *model.AppState) http.Handler {
Tracks []*model.Track Tracks []*model.Track
} }
err = indexTemplate.Execute(w, IndexData{ err = templates.IndexTemplate.Execute(w, IndexData{
Session: session, Session: session,
Releases: releases, Releases: releases,
Artists: artists, Artists: artists,
@ -120,7 +124,7 @@ func registerAccountHandler(app *model.AppState) http.Handler {
} }
render := func() { render := func() {
err := registerTemplate.Execute(w, registerData{ Session: session }) err := templates.RegisterTemplate.Execute(w, registerData{ Session: session })
if err != nil { if err != nil {
fmt.Printf("WARN: Error rendering create account page: %s\n", err) fmt.Printf("WARN: Error rendering create account page: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -230,7 +234,7 @@ func loginHandler(app *model.AppState) http.Handler {
} }
render := func() { render := func() {
err := loginTemplate.Execute(w, loginData{ Session: session }) err := templates.LoginTemplate.Execute(w, loginData{ Session: session })
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Error rendering admin login page: %s\n", err) fmt.Fprintf(os.Stderr, "WARN: Error rendering admin login page: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -346,7 +350,7 @@ func loginTOTPHandler(app *model.AppState) http.Handler {
} }
render := func() { render := func() {
err := loginTOTPTemplate.Execute(w, loginTOTPData{ Session: session }) err := templates.LoginTOTPTemplate.Execute(w, loginTOTPData{ Session: session })
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to render login TOTP page: %v\n", err) fmt.Fprintf(os.Stderr, "WARN: Failed to render login TOTP page: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -440,7 +444,7 @@ func logoutHandler(app *model.AppState) http.Handler {
Path: "/", Path: "/",
}) })
err = logoutTemplate.Execute(w, nil) err = templates.LogoutTemplate.Execute(w, nil)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to render logout page: %v\n", err) fmt.Fprintf(os.Stderr, "WARN: Failed to render logout page: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -460,24 +464,21 @@ func requireAccount(next http.Handler) http.HandlerFunc {
}) })
} }
//go:embed "static"
var staticFS embed.FS
func staticHandler() http.Handler { func staticHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
info, err := os.Stat(filepath.Join("admin", "static", filepath.Clean(r.URL.Path))) file, err := staticFS.ReadFile(filepath.Join("static", filepath.Clean(r.URL.Path)))
// does the file exist?
if err != nil { if err != nil {
if os.IsNotExist(err) {
http.NotFound(w, r)
return
}
}
// is thjs a directory? (forbidden)
if info.IsDir() {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
http.FileServer(http.Dir(filepath.Join("admin", "static"))).ServeHTTP(w, r) w.Header().Set("Content-Type", mime.TypeByExtension(path.Ext(r.URL.Path)))
w.WriteHeader(http.StatusOK)
w.Write(file)
}) })
} }

View file

@ -1,12 +1,13 @@
package admin package admin
import ( import (
"arimelody-web/log" "arimelody-web/admin/templates"
"arimelody-web/model" "arimelody-web/log"
"fmt" "arimelody-web/model"
"net/http" "fmt"
"os" "net/http"
"strings" "os"
"strings"
) )
func logsHandler(app *model.AppState) http.Handler { func logsHandler(app *model.AppState) http.Handler {
@ -54,7 +55,7 @@ func logsHandler(app *model.AppState) http.Handler {
Logs []*log.Log Logs []*log.Log
} }
err = logsTemplate.Execute(w, LogsResponse{ err = templates.LogsTemplate.Execute(w, LogsResponse{
Session: session, Session: session,
Logs: logs, Logs: logs,
}) })

View file

@ -1,12 +1,13 @@
package admin package admin
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"arimelody-web/controller" "arimelody-web/admin/templates"
"arimelody-web/model" "arimelody-web/controller"
"arimelody-web/model"
) )
func serveRelease(app *model.AppState) http.Handler { func serveRelease(app *model.AppState) http.Handler {
@ -60,7 +61,7 @@ func serveRelease(app *model.AppState) http.Handler {
Release *model.Release Release *model.Release
} }
err = releaseTemplate.Execute(w, ReleaseResponse{ err = templates.EditReleaseTemplate.Execute(w, ReleaseResponse{
Session: session, Session: session,
Release: release, Release: release,
}) })
@ -74,7 +75,7 @@ func serveRelease(app *model.AppState) http.Handler {
func serveEditCredits(release *model.Release) http.Handler { func serveEditCredits(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) {
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
err := editCreditsTemplate.Execute(w, release) err := templates.EditCreditsTemplate.Execute(w, release)
if err != nil { if err != nil {
fmt.Printf("Error rendering edit credits component for %s: %s\n", release.ID, err) fmt.Printf("Error rendering edit credits component for %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -97,7 +98,7 @@ func serveAddCredit(app *model.AppState, release *model.Release) http.Handler {
} }
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
err = addCreditTemplate.Execute(w, response{ err = templates.AddCreditTemplate.Execute(w, response{
ReleaseID: release.ID, ReleaseID: release.ID,
Artists: artists, Artists: artists,
}) })
@ -123,7 +124,7 @@ func serveNewCredit(app *model.AppState) http.Handler {
} }
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
err = newCreditTemplate.Execute(w, artist) err = templates.NewCreditTemplate.Execute(w, artist)
if err != nil { if err != nil {
fmt.Printf("Error rendering new credit component for %s: %s\n", artist.ID, err) fmt.Printf("Error rendering new credit component for %s: %s\n", artist.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -134,7 +135,7 @@ func serveNewCredit(app *model.AppState) http.Handler {
func serveEditLinks(release *model.Release) http.Handler { func serveEditLinks(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) {
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
err := editLinksTemplate.Execute(w, release) err := templates.EditCreditsTemplate.Execute(w, release)
if err != nil { if err != nil {
fmt.Printf("Error rendering edit links component for %s: %s\n", release.ID, err) fmt.Printf("Error rendering edit links component for %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -151,7 +152,7 @@ func serveEditTracks(release *model.Release) http.Handler {
Add func(a int, b int) int Add func(a int, b int) int
} }
err := editTracksTemplate.Execute(w, editTracksData{ err := templates.EditTracksTemplate.Execute(w, editTracksData{
Release: release, Release: release,
Add: func(a, b int) int { return a + b }, Add: func(a, b int) int { return a + b },
}) })
@ -177,7 +178,7 @@ func serveAddTrack(app *model.AppState, release *model.Release) http.Handler {
} }
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
err = addTrackTemplate.Execute(w, response{ err = templates.AddTrackTemplate.Execute(w, response{
ReleaseID: release.ID, ReleaseID: release.ID,
Tracks: tracks, Tracks: tracks,
}) })
@ -185,7 +186,6 @@ func serveAddTrack(app *model.AppState, release *model.Release) http.Handler {
fmt.Printf("Error rendering add tracks component for %s: %s\n", release.ID, err) fmt.Printf("Error rendering add tracks component for %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
} }
return
}) })
} }
@ -204,11 +204,10 @@ func serveNewTrack(app *model.AppState) http.Handler {
} }
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
err = newTrackTemplate.Execute(w, track) err = templates.NewTrackTemplate.Execute(w, track)
if err != nil { if err != nil {
fmt.Printf("Error rendering new track component for %s: %s\n", track.ID, err) fmt.Printf("Error rendering new track component for %s: %s\n", track.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
} }
return
}) })
} }

View file

@ -1,125 +0,0 @@
package admin
import (
"arimelody-web/log"
"fmt"
"html/template"
"path/filepath"
"strings"
"time"
)
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"),
))
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"),
))
var logsTemplate = template.Must(template.New("layout.html").Funcs(template.FuncMap{
"parseLevel": func(level log.LogLevel) string {
switch level {
case log.LEVEL_INFO:
return "INFO"
case log.LEVEL_WARN:
return "WARN"
}
return fmt.Sprintf("%d?", level)
},
"titleCase": func(logType string) string {
runes := []rune(logType)
for i, r := range runes {
if (i == 0 || runes[i - 1] == ' ') && r >= 'a' && r <= 'z' {
runes[i] = r + ('A' - 'a')
}
}
return string(runes)
},
"lower": func(str string) string { return strings.ToLower(str) },
"prettyTime": func(t time.Time) string {
// return t.Format("2006-01-02 15:04:05")
// return t.Format("15:04:05, 2 Jan 2006")
return t.Format("02 Jan 2006, 15:04:05")
},
}).ParseFiles(
filepath.Join("admin", "views", "layout.html"),
filepath.Join("views", "prideflag.html"),
filepath.Join("admin", "views", "logs.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 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"),
))
var editLinksTemplate = template.Must(template.ParseFiles(
filepath.Join("admin", "components", "links", "editlinks.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"),
))

View file

@ -0,0 +1,128 @@
package templates
import (
"arimelody-web/log"
"fmt"
"html/template"
"strings"
"time"
_ "embed"
)
//go:embed "html/layout.html"
var layoutHTML string
//go:embed "html/prideflag.html"
var prideflagHTML string
//go:embed "html/index.html"
var indexHTML string
//go:embed "html/register.html"
var registerHTML string
//go:embed "html/login.html"
var loginHTML string
//go:embed "html/login-totp.html"
var loginTotpHTML string
//go:embed "html/totp-confirm.html"
var totpConfirmHTML string
//go:embed "html/totp-setup.html"
var totpSetupHTML string
//go:embed "html/logout.html"
var logoutHTML string
//go:embed "html/logs.html"
var logsHTML string
//go:embed "html/edit-account.html"
var editAccountHTML string
//go:embed "html/edit-artist.html"
var editArtistHTML string
//go:embed "html/edit-release.html"
var editReleaseHTML string
//go:embed "html/edit-track.html"
var editTrackHTML string
//go:embed "html/components/credits/newcredit.html"
var componentNewCreditHTML string
//go:embed "html/components/credits/addcredit.html"
var componentAddCreditHTML string
//go:embed "html/components/credits/editcredits.html"
var componentEditCreditsHTML string
//go:embed "html/components/links/editlinks.html"
var componentEditLinksHTML string
//go:embed "html/components/release/release-list-item.html"
var componentReleaseListItemHTML string
//go:embed "html/components/tracks/newtrack.html"
var componentNewTrackHTML string
//go:embed "html/components/tracks/addtrack.html"
var componentAddTrackHTML string
//go:embed "html/components/tracks/edittracks.html"
var componentEditTracksHTML string
var BaseTemplate = template.Must(template.New("base").Parse(
strings.Join([]string{ layoutHTML, prideflagHTML }, "\n"),
))
var IndexTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
strings.Join([]string{
indexHTML,
componentReleaseListItemHTML,
}, "\n"),
))
var LoginTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(loginHTML))
var LoginTOTPTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(loginTotpHTML))
var RegisterTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(registerHTML))
var LogoutTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(logoutHTML))
var AccountTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(editAccountHTML))
var TOTPSetupTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(totpSetupHTML))
var TOTPConfirmTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(totpConfirmHTML))
var LogsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Funcs(template.FuncMap{
"parseLevel": func(level log.LogLevel) string {
switch level {
case log.LEVEL_INFO:
return "INFO"
case log.LEVEL_WARN:
return "WARN"
}
return fmt.Sprintf("%d?", level)
},
"titleCase": func(logType string) string {
runes := []rune(logType)
for i, r := range runes {
if (i == 0 || runes[i - 1] == ' ') && r >= 'a' && r <= 'z' {
runes[i] = r + ('A' - 'a')
}
}
return string(runes)
},
"lower": func(str string) string { return strings.ToLower(str) },
"prettyTime": func(t time.Time) string {
// return t.Format("2006-01-02 15:04:05")
// return t.Format("15:04:05, 2 Jan 2006")
return t.Format("02 Jan 2006, 15:04:05")
},
}).Parse(logsHTML))
var EditReleaseTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(editReleaseHTML))
var EditArtistTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(editArtistHTML))
var EditTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
strings.Join([]string{
editTrackHTML,
componentReleaseListItemHTML,
}, "\n"),
))
var EditCreditsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditCreditsHTML))
var AddCreditTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentAddCreditHTML))
var NewCreditTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentNewCreditHTML))
var EditLinksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditLinksHTML))
var EditTracksTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentEditTracksHTML))
var AddTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentAddTrackHTML))
var NewTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(componentNewTrackHTML))

View file

@ -1,12 +1,13 @@
package admin package admin
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"arimelody-web/model" "arimelody-web/admin/templates"
"arimelody-web/controller" "arimelody-web/controller"
"arimelody-web/model"
) )
func serveTrack(app *model.AppState) http.Handler { func serveTrack(app *model.AppState) http.Handler {
@ -39,7 +40,7 @@ func serveTrack(app *model.AppState) http.Handler {
session := r.Context().Value("session").(*model.Session) session := r.Context().Value("session").(*model.Session)
err = trackTemplate.Execute(w, TrackResponse{ err = templates.EditTrackTemplate.Execute(w, TrackResponse{
Session: session, Session: session,
Track: track, Track: track,
Releases: releases, Releases: releases,

View file

@ -2,7 +2,6 @@ package api
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
@ -173,7 +172,7 @@ func getSession(app *model.AppState, r *http.Request) (*model.Session, error) {
// check cookies first // check cookies first
sessionCookie, err := r.Cookie(model.COOKIE_TOKEN) sessionCookie, err := r.Cookie(model.COOKIE_TOKEN)
if err != nil && err != http.ErrNoCookie { if err != nil && err != http.ErrNoCookie {
return nil, errors.New(fmt.Sprintf("Failed to retrieve session cookie: %v\n", err)) return nil, fmt.Errorf("Failed to retrieve session cookie: %v\n", err)
} }
if sessionCookie != nil { if sessionCookie != nil {
token = sessionCookie.Value token = sessionCookie.Value
@ -188,7 +187,7 @@ func getSession(app *model.AppState, r *http.Request) (*model.Session, error) {
session, err := controller.GetSession(app.DB, token) session, err := controller.GetSession(app.DB, token)
if err != nil && !strings.Contains(err.Error(), "no rows") { if err != nil && !strings.Contains(err.Error(), "no rows") {
return nil, errors.New(fmt.Sprintf("Failed to retrieve session: %v\n", err)) return nil, fmt.Errorf("Failed to retrieve session: %v\n", err)
} }
if session != nil { if session != nil {

View file

@ -50,7 +50,7 @@ func HandleImageUpload(app *model.AppState, data *string, directory string, file
return "", nil return "", nil
} }
app.Log.Info(log.TYPE_FILES, "\"%s/%s.%s\" created.", directory, filename, ext) app.Log.Info(log.TYPE_FILES, "\"%s\" created.", imagePath)
return filename, nil return filename, nil
} }

View file

@ -5,7 +5,7 @@ import "math/rand"
func GenerateAlnumString(length int) []byte { func GenerateAlnumString(length int) []byte {
const CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" const CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
res := []byte{} res := []byte{}
for i := 0; i < length; i++ { for range length {
res = append(res, CHARS[rand.Intn(len(CHARS))]) res = append(res, CHARS[rand.Intn(len(CHARS))])
} }
return res return res

View file

@ -1,11 +1,12 @@
package controller package controller
import ( import (
"fmt" "embed"
"os" "fmt"
"time" "os"
"time"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
) )
const DB_VERSION int = 4 const DB_VERSION int = 4
@ -55,10 +56,13 @@ func CheckDBVersionAndMigrate(db *sqlx.DB) {
fmt.Printf("Database schema up to date.\n") fmt.Printf("Database schema up to date.\n")
} }
//go:embed "schema-migration"
var schemaFS embed.FS
func ApplyMigration(db *sqlx.DB, scriptFile string) { func ApplyMigration(db *sqlx.DB, scriptFile string) {
fmt.Printf("Applying schema migration %s...\n", scriptFile) fmt.Printf("Applying schema migration %s...\n", scriptFile)
bytes, err := os.ReadFile("schema-migration/" + scriptFile + ".sql") bytes, err := schemaFS.ReadFile("schema-migration/" + scriptFile + ".sql")
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "FATAL: Failed to open schema file \"%s\": %v\n", scriptFile, err) fmt.Fprintf(os.Stderr, "FATAL: Failed to open schema file \"%s\": %v\n", scriptFile, err)
os.Exit(1) os.Exit(1)

View file

@ -2,8 +2,8 @@ package controller
import ( import (
"encoding/base64" "encoding/base64"
"image" // "image"
"image/color" // "image/color"
"github.com/skip2/go-qrcode" "github.com/skip2/go-qrcode"
) )
@ -18,36 +18,35 @@ func GenerateQRCode(data string) (string, error) {
} }
// vvv DEPRECATED vvv // vvv DEPRECATED vvv
// const margin = 4
const margin = 4 //
// type QRCodeECCLevel int64
type QRCodeECCLevel int64 // const (
const ( // LOW QRCodeECCLevel = iota
LOW QRCodeECCLevel = iota // MEDIUM
MEDIUM // QUARTILE
QUARTILE // HIGH
HIGH // )
) //
// func drawLargeAlignmentSquare(x int, y int, img *image.Gray) {
func drawLargeAlignmentSquare(x int, y int, img *image.Gray) { // for yi := range 7 {
for yi := range 7 { // for xi := range 7 {
for xi := range 7 { // if (xi == 0 || xi == 6) || (yi == 0 || yi == 6) {
if (xi == 0 || xi == 6) || (yi == 0 || yi == 6) { // img.Set(x + xi, y + yi, color.Black)
img.Set(x + xi, y + yi, color.Black) // } else if (xi > 1 && xi < 5) && (yi > 1 && yi < 5) {
} else if (xi > 1 && xi < 5) && (yi > 1 && yi < 5) { // img.Set(x + xi, y + yi, color.Black)
img.Set(x + xi, y + yi, color.Black) // }
} // }
} // }
} // }
} //
// func drawSmallAlignmentSquare(x int, y int, img *image.Gray) {
func drawSmallAlignmentSquare(x int, y int, img *image.Gray) { // for yi := range 5 {
for yi := range 5 { // for xi := range 5 {
for xi := range 5 { // if (xi == 0 || xi == 4) || (yi == 0 || yi == 4) {
if (xi == 0 || xi == 4) || (yi == 0 || yi == 4) { // img.Set(x + xi, y + yi, color.Black)
img.Set(x + xi, y + yi, color.Black) // }
} // }
} // }
} // img.Set(x + 2, y + 2, color.Black)
img.Set(x + 2, y + 2, color.Black) // }
}

View file

@ -1,7 +1,6 @@
package controller package controller
import ( import (
"errors"
"fmt" "fmt"
"arimelody-web/model" "arimelody-web/model"
@ -21,7 +20,7 @@ func GetRelease(db *sqlx.DB, id string, full bool) (*model.Release, error) {
// get credits // get credits
credits, err := GetReleaseCredits(db, id) credits, err := GetReleaseCredits(db, id)
if err != nil { if err != nil {
return nil, errors.New(fmt.Sprintf("Credits: %s", err)) return nil, fmt.Errorf("Credits: %s", err)
} }
for _, credit := range credits { for _, credit := range credits {
release.Credits = append(release.Credits, credit) release.Credits = append(release.Credits, credit)
@ -30,7 +29,7 @@ func GetRelease(db *sqlx.DB, id string, full bool) (*model.Release, error) {
// get tracks // get tracks
tracks, err := GetReleaseTracks(db, id) tracks, err := GetReleaseTracks(db, id)
if err != nil { if err != nil {
return nil, errors.New(fmt.Sprintf("Tracks: %s", err)) return nil, fmt.Errorf("Tracks: %s", err)
} }
for _, track := range tracks { for _, track := range tracks {
release.Tracks = append(release.Tracks, track) release.Tracks = append(release.Tracks, track)
@ -39,7 +38,7 @@ func GetRelease(db *sqlx.DB, id string, full bool) (*model.Release, error) {
// get links // get links
links, err := GetReleaseLinks(db, id) links, err := GetReleaseLinks(db, id)
if err != nil { if err != nil {
return nil, errors.New(fmt.Sprintf("Links: %s", err)) return nil, fmt.Errorf("Links: %s", err)
} }
for _, link := range links { for _, link := range links {
release.Links = append(release.Links, link) release.Links = append(release.Links, link)
@ -71,7 +70,7 @@ func GetAllReleases(db *sqlx.DB, onlyVisible bool, limit int, full bool) ([]*mod
// get credits // get credits
credits, err := GetReleaseCredits(db, release.ID) credits, err := GetReleaseCredits(db, release.ID)
if err != nil { if err != nil {
return nil, errors.New(fmt.Sprintf("Credits: %s", err)) return nil, fmt.Errorf("Credits: %s", err)
} }
for _, credit := range credits { for _, credit := range credits {
release.Credits = append(release.Credits, credit) release.Credits = append(release.Credits, credit)
@ -81,7 +80,7 @@ func GetAllReleases(db *sqlx.DB, onlyVisible bool, limit int, full bool) ([]*mod
// get tracks // get tracks
tracks, err := GetReleaseTracks(db, release.ID) tracks, err := GetReleaseTracks(db, release.ID)
if err != nil { if err != nil {
return nil, errors.New(fmt.Sprintf("Tracks: %s", err)) return nil, fmt.Errorf("Tracks: %s", err)
} }
for _, track := range tracks { for _, track := range tracks {
release.Tracks = append(release.Tracks, track) release.Tracks = append(release.Tracks, track)
@ -90,7 +89,7 @@ func GetAllReleases(db *sqlx.DB, onlyVisible bool, limit int, full bool) ([]*mod
// get links // get links
links, err := GetReleaseLinks(db, release.ID) links, err := GetReleaseLinks(db, release.ID)
if err != nil { if err != nil {
return nil, errors.New(fmt.Sprintf("Links: %s", err)) return nil, fmt.Errorf("Links: %s", err)
} }
for _, link := range links { for _, link := range links {
release.Links = append(release.Links, link) release.Links = append(release.Links, link)

View file

@ -2,7 +2,6 @@ package controller
import ( import (
"database/sql" "database/sql"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
@ -19,7 +18,7 @@ const TOKEN_LEN = 64
func GetSessionFromRequest(app *model.AppState, r *http.Request) (*model.Session, error) { func GetSessionFromRequest(app *model.AppState, r *http.Request) (*model.Session, error) {
sessionCookie, err := r.Cookie(model.COOKIE_TOKEN) sessionCookie, err := r.Cookie(model.COOKIE_TOKEN)
if err != nil && err != http.ErrNoCookie { if err != nil && err != http.ErrNoCookie {
return nil, errors.New(fmt.Sprintf("Failed to retrieve session cookie: %v", err)) return nil, fmt.Errorf("Failed to retrieve session cookie: %v", err)
} }
var session *model.Session var session *model.Session
@ -29,7 +28,7 @@ func GetSessionFromRequest(app *model.AppState, r *http.Request) (*model.Session
session, err = GetSession(app.DB, sessionCookie.Value) session, err = GetSession(app.DB, sessionCookie.Value)
if err != nil && !strings.Contains(err.Error(), "no rows") { if err != nil && !strings.Contains(err.Error(), "no rows") {
return nil, errors.New(fmt.Sprintf("Failed to retrieve session: %v", err)) return nil, fmt.Errorf("Failed to retrieve session: %v", err)
} }
if session != nil { if session != nil {

55
main.go
View file

@ -1,32 +1,33 @@
package main package main
import ( import (
"bufio" "bufio"
"errors" "embed"
"fmt" "errors"
stdLog "log" "fmt"
"math" stdLog "log"
"math/rand" "math"
"net" "math/rand"
"net/http" "net"
"os" "net/http"
"path/filepath" "os"
"strconv" "path/filepath"
"strings" "strconv"
"time" "strings"
"time"
"arimelody-web/admin" "arimelody-web/admin"
"arimelody-web/api" "arimelody-web/api"
"arimelody-web/colour" "arimelody-web/colour"
"arimelody-web/controller" "arimelody-web/controller"
"arimelody-web/cursor" "arimelody-web/cursor"
"arimelody-web/log" "arimelody-web/log"
"arimelody-web/model" "arimelody-web/model"
"arimelody-web/view" "arimelody-web/view"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
_ "github.com/lib/pq" _ "github.com/lib/pq"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
// used for database migrations // used for database migrations
@ -35,12 +36,16 @@ const DB_VERSION = 1
const DEFAULT_PORT int64 = 8080 const DEFAULT_PORT int64 = 8080
const HRT_DATE int64 = 1756478697 const HRT_DATE int64 = 1756478697
//go:embed "public"
var publicFS embed.FS
func main() { func main() {
fmt.Printf("made with <3 by ari melody\n\n") fmt.Printf("made with <3 by ari melody\n\n")
app := model.AppState{ app := model.AppState{
Config: controller.GetConfig(), Config: controller.GetConfig(),
Twitch: nil, Twitch: nil,
PublicFS: publicFS,
} }
// initialise database connection // initialise database connection
@ -526,7 +531,7 @@ func createServeMux(app *model.AppState) *http.ServeMux {
mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler(app))) mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler(app)))
mux.Handle("/api/", http.StripPrefix("/api", api.Handler(app))) mux.Handle("/api/", http.StripPrefix("/api", api.Handler(app)))
mux.Handle("/music/", http.StripPrefix("/music", view.MusicHandler(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("/uploads/", http.StripPrefix("/uploads", view.ServeFiles(filepath.Join(app.Config.DataDirectory, "uploads"))))
mux.Handle("/cursor-ws", cursor.Handler(app)) mux.Handle("/cursor-ws", cursor.Handler(app))
mux.Handle("/", view.IndexHandler(app)) mux.Handle("/", view.IndexHandler(app))

View file

@ -1,9 +1,11 @@
package model package model
import ( import (
"github.com/jmoiron/sqlx" "embed"
"arimelody-web/log" "github.com/jmoiron/sqlx"
"arimelody-web/log"
) )
type ( type (
@ -43,5 +45,6 @@ type (
Config Config Config Config
Log log.Logger Log log.Logger
Twitch *TwitchState Twitch *TwitchState
PublicFS embed.FS
} }
) )

View file

@ -0,0 +1,21 @@
{{define "prideflag"}}
<a href="https://github.com/arimelody/prideflag" target="_blank" id="prideflag">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120" hx-preserve="true">
<path id="red" d="M120,80 L100,100 L120,120 Z" style="fill:#d20605"/>
<path id="orange" d="M120,80 V40 L80,80 L100,100 Z" style="fill:#ef9c00"/>
<path id="yellow" d="M120,40 V0 L60,60 L80,80 Z" style="fill:#e5fe02"/>
<path id="green" d="M120,0 H80 L40,40 L60,60 Z" style="fill:#09be01"/>
<path id="blue" d="M80,0 H40 L20,20 L40,40 Z" style="fill:#081a9a"/>
<path id="purple" d="M40,0 H0 L20,20 Z" style="fill:#76008a"/>
<rect id="black" x="60" width="60" height="60" style="fill:#010101"/>
<rect id="brown" x="70" width="50" height="50" style="fill:#603814"/>
<rect id="lightblue" x="80" width="40" height="40" style="fill:#73d6ed"/>
<rect id="pink" x="90" width="30" height="30" style="fill:#ffafc8"/>
<rect id="white" x="100" width="20" height="20" style="fill:#fff"/>
<rect id="intyellow" x="110" width="10" height="10" style="fill:#fed800"/>
<circle id="intpurple" cx="120" cy="0" r="5" stroke="#7601ad" stroke-width="2" fill="none"/>
</svg>
</a>
{{end}}

View file

@ -1,28 +1,36 @@
package templates package templates
import ( import (
"html/template" _ "embed"
"path/filepath" "html/template"
"strings"
) )
var IndexTemplate = template.Must(template.ParseFiles( //go:embed "html/layout.html"
filepath.Join("views", "layout.html"), var layoutHTML string
filepath.Join("views", "header.html"), //go:embed "html/header.html"
filepath.Join("views", "footer.html"), var headerHTML string
filepath.Join("views", "prideflag.html"), //go:embed "html/footer.html"
filepath.Join("views", "index.html"), var footerHTML string
)) //go:embed "html/prideflag.html"
var MusicTemplate = template.Must(template.ParseFiles( var prideflagHTML string
filepath.Join("views", "layout.html"), //go:embed "html/index.html"
filepath.Join("views", "header.html"), var indexHTML string
filepath.Join("views", "footer.html"), //go:embed "html/music.html"
filepath.Join("views", "prideflag.html"), var musicHTML string
filepath.Join("views", "music.html"), //go:embed "html/music-gateway.html"
)) var musicGatewayHTML string
var MusicGatewayTemplate = template.Must(template.ParseFiles( // //go:embed "html/404.html"
filepath.Join("views", "layout.html"), // var error404HTML string
filepath.Join("views", "header.html"),
filepath.Join("views", "footer.html"), var BaseTemplate = template.Must(template.New("base").Parse(
filepath.Join("views", "prideflag.html"), strings.Join([]string{
filepath.Join("views", "music-gateway.html"), layoutHTML,
headerHTML,
footerHTML,
prideflagHTML,
}, "\n"),
)) ))
var IndexTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(indexHTML))
var MusicTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(musicHTML))
var MusicGatewayTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(musicGatewayHTML))

View file

@ -1,13 +1,31 @@
package view package view
import ( import (
"embed"
"errors" "errors"
"mime"
"net/http" "net/http"
"os" "os"
"path"
"path/filepath" "path/filepath"
) )
func StaticHandler(directory string) http.Handler { func ServeEmbedFS(fs embed.FS, dir string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
file, err := fs.ReadFile(filepath.Join(dir, filepath.Clean(r.URL.Path)))
if err != nil {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", mime.TypeByExtension(path.Ext(r.URL.Path)))
w.WriteHeader(http.StatusOK)
w.Write(file)
})
}
func ServeFiles(directory string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
info, err := os.Stat(filepath.Join(directory, filepath.Clean(r.URL.Path))) info, err := os.Stat(filepath.Join(directory, filepath.Clean(r.URL.Path)))
@ -28,4 +46,3 @@ func StaticHandler(directory string) http.Handler {
http.FileServer(http.Dir(directory)).ServeHTTP(w, r) http.FileServer(http.Dir(directory)).ServeHTTP(w, r)
}) })
} }

View file

@ -40,6 +40,6 @@ func IndexHandler(app *model.AppState) http.Handler {
return return
} }
StaticHandler("public").ServeHTTP(w, r) ServeEmbedFS(app.PublicFS, "public").ServeHTTP(w, r)
}) })
} }