- {{if .Session.Account}}
+ {{if .Account}}
diff --git a/admin/views/login-totp.html b/admin/views/login-totp.html
deleted file mode 100644
index 33e8c88..0000000
--- a/admin/views/login-totp.html
+++ /dev/null
@@ -1,47 +0,0 @@
-{{define "head"}}
-
Login - ari melody 💫
-
-
-
-{{end}}
-
-{{define "content"}}
-
- {{if .Session.Message.Valid}}
- {{html .Session.Message.String}}
- {{end}}
- {{if .Session.Error.Valid}}
- {{html .Session.Error.String}}
- {{end}}
-
-
-
-{{end}}
diff --git a/admin/views/login.html b/admin/views/login.html
index fbb7294..7744e91 100644
--- a/admin/views/login.html
+++ b/admin/views/login.html
@@ -3,7 +3,15 @@
{{end}}
{{define "content"}}
- {{if .Session.Message.Valid}}
- {{html .Session.Message.String}}
- {{end}}
- {{if .Session.Error.Valid}}
- {{html .Session.Error.String}}
+ {{if .Message}}
+ {{.Message}}
{{end}}
+ {{if .Token}}
+
+
+
+ Logged in successfully.
+ You should be redirected to /admin soon.
+
+
+ {{else}}
+
+
+ {{end}}
{{end}}
diff --git a/admin/views/logout.html b/admin/views/logout.html
index f127fd6..1999377 100644
--- a/admin/views/logout.html
+++ b/admin/views/logout.html
@@ -12,10 +12,13 @@ p a {
{{define "content"}}
-
+
Logged out successfully.
- You should be redirected to /admin/login shortly.
+ You should be redirected to / in 5 seconds.
+
diff --git a/admin/views/totp-confirm.html b/admin/views/totp-confirm.html
deleted file mode 100644
index 7d305ec..0000000
--- a/admin/views/totp-confirm.html
+++ /dev/null
@@ -1,48 +0,0 @@
-{{define "head"}}
-
TOTP Confirmation - ari melody 💫
-
-
-
-{{end}}
-
-{{define "content"}}
-
- {{if .Session.Error.Valid}}
- {{html .Session.Error.String}}
- {{end}}
-
-
-
-{{end}}
diff --git a/admin/views/totp-setup.html b/admin/views/totp-setup.html
deleted file mode 100644
index e74c970..0000000
--- a/admin/views/totp-setup.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{{define "head"}}
-
TOTP Setup - ari melody 💫
-
-
-{{end}}
-
-{{define "content"}}
-
- {{if .Session.Error.Valid}}
- {{html .Session.Error.String}}
- {{end}}
-
-
-
-{{end}}
diff --git a/api/api.go b/api/api.go
index 50b1c63..9489126 100644
--- a/api/api.go
+++ b/api/api.go
@@ -1,13 +1,11 @@
package api
import (
- "context"
- "errors"
"fmt"
"net/http"
- "os"
"strings"
+ "arimelody-web/admin"
"arimelody-web/controller"
"arimelody-web/model"
)
@@ -38,10 +36,10 @@ func Handler(app *model.AppState) http.Handler {
ServeArtist(app, artist).ServeHTTP(w, r)
case http.MethodPut:
// PUT /api/v1/artist/{id} (admin)
- requireAccount(app, UpdateArtist(app, artist)).ServeHTTP(w, r)
+ admin.RequireAccount(app, UpdateArtist(app, artist)).ServeHTTP(w, r)
case http.MethodDelete:
// DELETE /api/v1/artist/{id} (admin)
- requireAccount(app, DeleteArtist(app, artist)).ServeHTTP(w, r)
+ admin.RequireAccount(app, DeleteArtist(app, artist)).ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
@@ -53,7 +51,7 @@ func Handler(app *model.AppState) http.Handler {
ServeAllArtists(app).ServeHTTP(w, r)
case http.MethodPost:
// POST /api/v1/artist (admin)
- requireAccount(app, CreateArtist(app)).ServeHTTP(w, r)
+ admin.RequireAccount(app, CreateArtist(app)).ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
@@ -80,10 +78,10 @@ func Handler(app *model.AppState) http.Handler {
ServeRelease(app, release).ServeHTTP(w, r)
case http.MethodPut:
// PUT /api/v1/music/{id} (admin)
- requireAccount(app, UpdateRelease(app, release)).ServeHTTP(w, r)
+ admin.RequireAccount(app, UpdateRelease(app, release)).ServeHTTP(w, r)
case http.MethodDelete:
// DELETE /api/v1/music/{id} (admin)
- requireAccount(app, DeleteRelease(app, release)).ServeHTTP(w, r)
+ admin.RequireAccount(app, DeleteRelease(app, release)).ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
@@ -95,7 +93,7 @@ func Handler(app *model.AppState) http.Handler {
ServeCatalog(app).ServeHTTP(w, r)
case http.MethodPost:
// POST /api/v1/music (admin)
- requireAccount(app, CreateRelease(app)).ServeHTTP(w, r)
+ admin.RequireAccount(app, CreateRelease(app)).ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
@@ -119,13 +117,13 @@ func Handler(app *model.AppState) http.Handler {
switch r.Method {
case http.MethodGet:
// GET /api/v1/track/{id} (admin)
- requireAccount(app, ServeTrack(app, track)).ServeHTTP(w, r)
+ admin.RequireAccount(app, ServeTrack(app, track)).ServeHTTP(w, r)
case http.MethodPut:
// PUT /api/v1/track/{id} (admin)
- requireAccount(app, UpdateTrack(app, track)).ServeHTTP(w, r)
+ admin.RequireAccount(app, UpdateTrack(app, track)).ServeHTTP(w, r)
case http.MethodDelete:
// DELETE /api/v1/track/{id} (admin)
- requireAccount(app, DeleteTrack(app, track)).ServeHTTP(w, r)
+ admin.RequireAccount(app, DeleteTrack(app, track)).ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
@@ -134,10 +132,10 @@ func Handler(app *model.AppState) http.Handler {
switch r.Method {
case http.MethodGet:
// GET /api/v1/track (admin)
- requireAccount(app, ServeAllTracks(app)).ServeHTTP(w, r)
+ admin.RequireAccount(app, ServeAllTracks(app)).ServeHTTP(w, r)
case http.MethodPost:
// POST /api/v1/track (admin)
- requireAccount(app, CreateTrack(app)).ServeHTTP(w, r)
+ admin.RequireAccount(app, CreateTrack(app)).ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
@@ -145,51 +143,3 @@ func Handler(app *model.AppState) http.Handler {
return mux
}
-
-func requireAccount(app *model.AppState, next http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- session, err := getSession(app, r)
- if err != nil {
- fmt.Fprintf(os.Stderr, "WARN: Failed to get session: %v\n", err)
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
- if session.Account == nil {
- http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- return
- }
- ctx := context.WithValue(r.Context(), "session", session)
- next.ServeHTTP(w, r.WithContext(ctx))
- })
-}
-
-func getSession(app *model.AppState, r *http.Request) (*model.Session, error) {
- var token string
-
- // check cookies first
- 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\n", err))
- }
- if sessionCookie != nil {
- token = sessionCookie.Value
- } else {
- // check Authorization header
- token = strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
- }
-
- if token == "" { return nil, nil }
-
- // fetch existing session
- session, err := controller.GetSession(app.DB, token)
-
- if err != nil && !strings.Contains(err.Error(), "no rows") {
- return nil, errors.New(fmt.Sprintf("Failed to retrieve session: %v\n", err))
- }
-
- if session != nil {
- // TODO: consider running security checks here (i.e. user agent mismatches)
- }
-
- return session, nil
-}
diff --git a/api/artist.go b/api/artist.go
index 51c9d62..a9676b1 100644
--- a/api/artist.go
+++ b/api/artist.go
@@ -51,8 +51,13 @@ func ServeArtist(app *model.AppState, artist *model.Artist) http.Handler {
}
)
- session := r.Context().Value("session").(*model.Session)
- show_hidden_releases := session != nil && session.Account != nil
+ account, err := controller.GetAccountByRequest(app.DB, r)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %v\n", err)
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ return
+ }
+ show_hidden_releases := account != nil
dbCredits, err := controller.GetArtistCredits(app.DB, artist.ID, show_hidden_releases)
if err != nil {
diff --git a/api/release.go b/api/release.go
index b89cec8..c71043e 100644
--- a/api/release.go
+++ b/api/release.go
@@ -19,14 +19,13 @@ func ServeRelease(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.DB, r)
+ account, err := controller.GetAccountByRequest(app.DB, r)
if err != nil {
- fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve session: %v\n", err)
+ fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
-
- if session != nil && session.Account != nil {
+ if account != nil {
// TODO: check privilege on release
privileged = true
}
@@ -146,11 +145,16 @@ func ServeCatalog(app *model.AppState) http.Handler {
}
catalog := []Release{}
- session := r.Context().Value("session").(*model.Session)
+ account, err := controller.GetAccountByRequest(app.DB, r)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %v\n", err)
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ return
+ }
for _, release := range releases {
if !release.Visible {
privileged := false
- if session != nil && session.Account != nil {
+ if account != nil {
// TODO: check privilege on release
privileged = true
}
diff --git a/bundle.sh b/bundle.sh
index dc7c023..4e7068b 100755
--- a/bundle.sh
+++ b/bundle.sh
@@ -6,4 +6,4 @@ if [ ! -f arimelody-web ]; then
exit 1
fi
-tar czvf arimelody-web.tar.gz arimelody-web admin/components/ admin/views/ admin/static/ views/ public/ schema-migration/
+tar czvf arimelody-web.tar.gz arimelody-web admin/components/ admin/views/ admin/static/ views/ public/
diff --git a/controller/account.go b/controller/account.go
index 9c7c1e1..044faec 100644
--- a/controller/account.go
+++ b/controller/account.go
@@ -2,6 +2,9 @@ package controller
import (
"arimelody-web/model"
+ "errors"
+ "fmt"
+ "net/http"
"strings"
"github.com/jmoiron/sqlx"
@@ -18,21 +21,7 @@ func GetAllAccounts(db *sqlx.DB) ([]model.Account, error) {
return accounts, nil
}
-func GetAccountByID(db *sqlx.DB, id string) (*model.Account, error) {
- var account = model.Account{}
-
- err := db.Get(&account, "SELECT * FROM account WHERE id=$1", id)
- if err != nil {
- if strings.Contains(err.Error(), "no rows") {
- return nil, nil
- }
- return nil, err
- }
-
- return &account, nil
-}
-
-func GetAccountByUsername(db *sqlx.DB, username string) (*model.Account, error) {
+func GetAccount(db *sqlx.DB, username string) (*model.Account, error) {
var account = model.Account{}
err := db.Get(&account, "SELECT * FROM account WHERE username=$1", username)
@@ -60,12 +49,12 @@ func GetAccountByEmail(db *sqlx.DB, email string) (*model.Account, error) {
return &account, nil
}
-func GetAccountBySession(db *sqlx.DB, sessionToken string) (*model.Account, error) {
- if sessionToken == "" { return nil, nil }
+func GetAccountByToken(db *sqlx.DB, token string) (*model.Account, error) {
+ if token == "" { return nil, nil }
account := model.Account{}
- err := db.Get(&account, "SELECT account.* FROM account JOIN token ON id=account WHERE token=$1", sessionToken)
+ err := db.Get(&account, "SELECT account.* FROM account JOIN token ON id=account WHERE token=$1", token)
if err != nil {
if strings.Contains(err.Error(), "no rows") {
return nil, nil
@@ -76,6 +65,42 @@ func GetAccountBySession(db *sqlx.DB, sessionToken string) (*model.Account, erro
return &account, nil
}
+func GetTokenFromRequest(db *sqlx.DB, r *http.Request) string {
+ tokenStr := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
+ if len(tokenStr) > 0 {
+ return tokenStr
+ }
+
+ cookie, err := r.Cookie(model.COOKIE_TOKEN)
+ if err != nil {
+ return ""
+ }
+ return cookie.Value
+}
+
+func GetAccountByRequest(db *sqlx.DB, r *http.Request) (*model.Account, error) {
+ tokenStr := GetTokenFromRequest(db, r)
+
+ token, err := GetToken(db, tokenStr)
+ if err != nil {
+ if strings.Contains(err.Error(), "no rows") {
+ return nil, nil
+ }
+ return nil, errors.New("GetToken: " + err.Error())
+ }
+
+ // does user-agent match the token?
+ if r.UserAgent() != token.UserAgent {
+ // invalidate the token
+ DeleteToken(db, tokenStr)
+ fmt.Printf("WARN: Attempted use of token by unauthorised User-Agent (Expected `%s`, got `%s`)\n", token.UserAgent, r.UserAgent())
+ // TODO: log unauthorised activity to the user
+ return nil, errors.New("User agent mismatch")
+ }
+
+ return GetAccountByToken(db, tokenStr)
+}
+
func CreateAccount(db *sqlx.DB, account *model.Account) error {
err := db.Get(
&account.ID,
@@ -94,7 +119,7 @@ func CreateAccount(db *sqlx.DB, account *model.Account) error {
func UpdateAccount(db *sqlx.DB, account *model.Account) error {
_, err := db.Exec(
"UPDATE account " +
- "SET username=$2,password=$3,email=$4,avatar_url=$5 " +
+ "SET username=$2, password=$3, email=$4, avatar_url=$5) " +
"WHERE id=$1",
account.ID,
account.Username,
@@ -106,7 +131,7 @@ func UpdateAccount(db *sqlx.DB, account *model.Account) error {
return err
}
-func DeleteAccount(db *sqlx.DB, accountID string) error {
- _, err := db.Exec("DELETE FROM account WHERE id=$1", accountID)
+func DeleteAccount(db *sqlx.DB, username string) error {
+ _, err := db.Exec("DELETE FROM account WHERE username=$1", username)
return err
}
diff --git a/controller/config.go b/controller/config.go
index 8772b6b..28d4be4 100644
--- a/controller/config.go
+++ b/controller/config.go
@@ -19,7 +19,6 @@ func GetConfig() model.Config {
config := model.Config{
BaseUrl: "https://arimelody.me",
- Host: "0.0.0.0",
Port: 8080,
DB: model.DBConfig{
Host: "127.0.0.1",
@@ -56,7 +55,6 @@ func handleConfigOverrides(config *model.Config) error {
var err error
if env, has := os.LookupEnv("ARIMELODY_BASE_URL"); has { config.BaseUrl = env }
- if env, has := os.LookupEnv("ARIMELODY_HOST"); has { config.Host = env }
if env, has := os.LookupEnv("ARIMELODY_PORT"); has {
config.Port, err = strconv.ParseInt(env, 10, 0)
if err != nil { return errors.New("ARIMELODY_PORT: " + err.Error()) }
diff --git a/controller/migrator.go b/controller/migrator.go
index d0011ee..3624255 100644
--- a/controller/migrator.go
+++ b/controller/migrator.go
@@ -50,7 +50,7 @@ func CheckDBVersionAndMigrate(db *sqlx.DB) {
func ApplyMigration(db *sqlx.DB, scriptFile string) {
fmt.Printf("Applying schema migration %s...\n", scriptFile)
- bytes, err := os.ReadFile("schema-migration/" + scriptFile + ".sql")
+ bytes, err := os.ReadFile("schema_migration/" + scriptFile + ".sql")
if err != nil {
fmt.Fprintf(os.Stderr, "FATAL: Failed to open schema file \"%s\": %v\n", scriptFile, err)
os.Exit(1)
diff --git a/controller/qr.go b/controller/qr.go
deleted file mode 100644
index 7ada0f8..0000000
--- a/controller/qr.go
+++ /dev/null
@@ -1,120 +0,0 @@
-package controller
-
-import (
- "bytes"
- "encoding/base64"
- "errors"
- "fmt"
- "image"
- "image/color"
- "image/png"
-
- "github.com/skip2/go-qrcode"
-)
-
-func GenerateQRCode(data string) (string, error) {
- imgBytes, err := qrcode.Encode(data, qrcode.Medium, 256)
- if err != nil {
- return "", err
- }
- base64Img := base64.StdEncoding.EncodeToString(imgBytes)
- return base64Img, nil
-}
-
-// vvv DEPRECATED vvv
-
-const margin = 4
-
-type QRCodeECCLevel int64
-const (
- LOW QRCodeECCLevel = iota
- MEDIUM
- QUARTILE
- 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 {
- 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/session.go b/controller/session.go
deleted file mode 100644
index cf423fe..0000000
--- a/controller/session.go
+++ /dev/null
@@ -1,177 +0,0 @@
-package controller
-
-import (
- "database/sql"
- "errors"
- "fmt"
- "net/http"
- "strings"
- "time"
-
- "arimelody-web/model"
-
- "github.com/jmoiron/sqlx"
-)
-
-const TOKEN_LEN = 64
-
-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))
- }
-
- var session *model.Session
-
- if sessionCookie != nil {
- // fetch existing session
- 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 {
- // TODO: consider running security checks here (i.e. user agent mismatches)
- }
- }
-
- return session, nil
-}
-
-func CreateSession(db *sqlx.DB, userAgent string) (*model.Session, error) {
- tokenString := GenerateAlnumString(TOKEN_LEN)
-
- session := model.Session{
- Token: string(tokenString),
- UserAgent: userAgent,
- CreatedAt: time.Now(),
- ExpiresAt: time.Now().Add(time.Hour * 24),
- }
-
- _, err := db.Exec("INSERT INTO session " +
- "(token, user_agent, created_at, expires_at) VALUES " +
- "($1, $2, $3, $4)",
- session.Token,
- session.UserAgent,
- session.CreatedAt,
- session.ExpiresAt,
- )
- if err != nil {
- return nil, err
- }
-
- return &session, nil
-}
-
-// func WriteSession(db *sqlx.DB, session *model.Session) error {
-// _, err := db.Exec(
-// "UPDATE session " +
-// "SET account=$2,message=$3,error=$4 " +
-// "WHERE token=$1",
-// session.Token,
-// session.Account.ID,
-// session.Message,
-// session.Error,
-// )
-// return err
-// }
-
-func SetSessionAttemptAccount(db *sqlx.DB, session *model.Session, account *model.Account) error {
- var err error
- session.AttemptAccount = account
- if account == nil {
- _, err = db.Exec("UPDATE session SET attempt_account=NULL WHERE token=$1", session.Token)
- } else {
- _, err = db.Exec("UPDATE session SET attempt_account=$2 WHERE token=$1", session.Token, account.ID)
- }
- return err
-}
-
-func SetSessionAccount(db *sqlx.DB, session *model.Session, account *model.Account) error {
- var err error
- session.Account = account
- if account == nil {
- _, err = db.Exec("UPDATE session SET account=NULL WHERE token=$1", session.Token)
- } else {
- _, err = db.Exec("UPDATE session SET account=$2 WHERE token=$1", session.Token, account.ID)
- }
- return err
-}
-
-func SetSessionMessage(db *sqlx.DB, session *model.Session, message string) error {
- var err error
- if message == "" {
- if !session.Message.Valid { return nil }
- session.Message = sql.NullString{ }
- _, err = db.Exec("UPDATE session SET message=NULL WHERE token=$1", session.Token)
- } else {
- session.Message = sql.NullString{ String: message, Valid: true }
- _, err = db.Exec("UPDATE session SET message=$2 WHERE token=$1", session.Token, message)
- }
- return err
-}
-
-func SetSessionError(db *sqlx.DB, session *model.Session, message string) error {
- var err error
- if message == "" {
- if !session.Error.Valid { return nil }
- session.Error = sql.NullString{ }
- _, err = db.Exec("UPDATE session SET error=NULL WHERE token=$1", session.Token)
- } else {
- session.Error = sql.NullString{ String: message, Valid: true }
- _, err = db.Exec("UPDATE session SET error=$2 WHERE token=$1", session.Token, message)
- }
- return err
-}
-
-func GetSession(db *sqlx.DB, token string) (*model.Session, error) {
- type dbSession struct {
- model.Session
- AttemptAccountID sql.NullString `db:"attempt_account"`
- AccountID sql.NullString `db:"account"`
- }
-
- session := dbSession{}
- err := db.Get(
- &session,
- "SELECT * FROM session WHERE token=$1",
- token,
- )
- if err != nil {
- return nil, err
- }
-
- if session.AccountID.Valid {
- session.Account, err = GetAccountByID(db, session.AccountID.String)
- if err != nil {
- return nil, err
- }
- }
-
- if session.AttemptAccountID.Valid {
- session.AttemptAccount, err = GetAccountByID(db, session.AttemptAccountID.String)
- if err != nil {
- return nil, err
- }
- }
-
- return &session.Session, err
-}
-
-// func GetAllSessionsForAccount(db *sqlx.DB, accountID string) ([]model.Session, error) {
-// sessions := []model.Session{}
-// err := db.Select(&sessions, "SELECT * FROM session WHERE account=$1 AND expires_at>current_timestamp", accountID)
-// return sessions, err
-// }
-
-func DeleteAllSessionsForAccount(db *sqlx.DB, accountID string) error {
- _, err := db.Exec("DELETE FROM session WHERE account=$1", accountID)
- return err
-}
-
-func DeleteSession(db *sqlx.DB, token string) error {
- _, err := db.Exec("DELETE FROM session WHERE token=$1", token)
- return err
-}
-
diff --git a/controller/token.go b/controller/token.go
new file mode 100644
index 0000000..12982bd
--- /dev/null
+++ b/controller/token.go
@@ -0,0 +1,61 @@
+package controller
+
+import (
+ "time"
+
+ "arimelody-web/model"
+
+ "github.com/jmoiron/sqlx"
+)
+
+const TOKEN_LEN = 32
+
+func CreateToken(db *sqlx.DB, accountID string, userAgent string) (*model.Token, error) {
+ tokenString := GenerateAlnumString(TOKEN_LEN)
+
+ token := model.Token{
+ Token: string(tokenString),
+ AccountID: accountID,
+ UserAgent: userAgent,
+ CreatedAt: time.Now(),
+ ExpiresAt: time.Now().Add(time.Hour * 24),
+ }
+
+ _, err := db.Exec("INSERT INTO token " +
+ "(token, account, user_agent, created_at, expires_at) VALUES " +
+ "($1, $2, $3, $4, $5)",
+ token.Token,
+ token.AccountID,
+ token.UserAgent,
+ token.CreatedAt,
+ token.ExpiresAt,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ return &token, nil
+}
+
+func GetToken(db *sqlx.DB, token_str string) (*model.Token, error) {
+ token := model.Token{}
+ err := db.Get(&token, "SELECT * FROM token WHERE token=$1", token_str)
+ return &token, err
+}
+
+func GetAllTokensForAccount(db *sqlx.DB, accountID string) ([]model.Token, error) {
+ tokens := []model.Token{}
+ err := db.Select(&tokens, "SELECT * FROM token WHERE account=$1 AND expires_at>current_timestamp", accountID)
+ return tokens, err
+}
+
+func DeleteAllTokensForAccount(db *sqlx.DB, accountID string) error {
+ _, err := db.Exec("DELETE FROM token WHERE account=$1", accountID)
+ return err
+}
+
+func DeleteToken(db *sqlx.DB, token string) error {
+ _, err := db.Exec("DELETE FROM token WHERE token=$1", token)
+ return err
+}
+
diff --git a/controller/totp.go b/controller/totp.go
index 88f6bc3..18616da 100644
--- a/controller/totp.go
+++ b/controller/totp.go
@@ -18,8 +18,8 @@ import (
)
const TOTP_SECRET_LENGTH = 32
-const TOTP_TIME_STEP int64 = 30
-const TOTP_CODE_LENGTH = 6
+const TIME_STEP int64 = 30
+const CODE_LENGTH = 6
func GenerateTOTP(secret string, timeStepOffset int) string {
decodedSecret, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(secret)
@@ -27,7 +27,7 @@ func GenerateTOTP(secret string, timeStepOffset int) string {
fmt.Fprintf(os.Stderr, "WARN: Invalid Base32 secret\n")
}
- counter := time.Now().Unix() / TOTP_TIME_STEP - int64(timeStepOffset)
+ counter := time.Now().Unix() / TIME_STEP - int64(timeStepOffset)
counterBytes := make([]byte, 8)
binary.BigEndian.PutUint64(counterBytes, uint64(counter))
@@ -37,9 +37,9 @@ func GenerateTOTP(secret string, timeStepOffset int) string {
offset := hash[len(hash) - 1] & 0x0f
binaryCode := int32(binary.BigEndian.Uint32(hash[offset : offset + 4]) & 0x7FFFFFFF)
- code := binaryCode % int32(math.Pow10(TOTP_CODE_LENGTH))
+ code := binaryCode % int32(math.Pow10(CODE_LENGTH))
- return fmt.Sprintf(fmt.Sprintf("%%0%dd", TOTP_CODE_LENGTH), code)
+ return fmt.Sprintf(fmt.Sprintf("%%0%dd", CODE_LENGTH), code)
}
func GenerateTOTPSecret(length int) string {
@@ -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", CODE_LENGTH))
+ query.Set("period", fmt.Sprintf("%d", TIME_STEP))
url.RawQuery = query.Encode()
return url.String()
@@ -78,7 +78,7 @@ func GetTOTPsForAccount(db *sqlx.DB, accountID string) ([]model.TOTP, error) {
err := db.Select(
&totps,
"SELECT * FROM totp " +
- "WHERE account=$1 AND confirmed=true " +
+ "WHERE account=$1 " +
"ORDER BY created_at ASC",
accountID,
)
@@ -89,36 +89,14 @@ func GetTOTPsForAccount(db *sqlx.DB, accountID string) ([]model.TOTP, error) {
return totps, nil
}
-func CheckTOTPForAccount(db *sqlx.DB, accountID string, totp string) (*model.TOTP, error) {
- totps, err := GetTOTPsForAccount(db, accountID)
- if err != nil {
- return nil, err
- }
- for _, method := range totps {
- check := GenerateTOTP(method.Secret, 0)
- if check == totp {
- return &method, nil
- }
- // try again with offset- maybe user input the code late?
- check = GenerateTOTP(method.Secret, 1)
- if check == totp {
- return &method, nil
- }
- }
- // user failed all TOTP checks
- // note: this state will still occur even if the account has no TOTP methods.
- return nil, nil
-}
-
func GetTOTP(db *sqlx.DB, accountID string, name string) (*model.TOTP, error) {
totp := model.TOTP{}
err := db.Get(
&totp,
"SELECT * FROM totp " +
- "WHERE account=$1 AND name=$2",
+ "WHERE account=$1",
accountID,
- name,
)
if err != nil {
if strings.Contains(err.Error(), "no rows") {
@@ -130,15 +108,6 @@ func GetTOTP(db *sqlx.DB, accountID string, name string) (*model.TOTP, error) {
return &totp, nil
}
-func ConfirmTOTP(db *sqlx.DB, accountID string, name string) error {
- _, err := db.Exec(
- "UPDATE totp SET confirmed=true WHERE account=$1 AND name=$2",
- accountID,
- name,
- )
- return err
-}
-
func CreateTOTP(db *sqlx.DB, totp *model.TOTP) error {
_, err := db.Exec(
"INSERT INTO totp (account, name, secret) " +
@@ -158,8 +127,3 @@ func DeleteTOTP(db *sqlx.DB, accountID string, name string) error {
)
return err
}
-
-func DeleteUnconfirmedTOTPs(db *sqlx.DB) error {
- _, err := db.Exec("DELETE FROM totp WHERE confirmed=false")
- return err
-}
diff --git a/go.mod b/go.mod
index f8717a2..73a1cf1 100644
--- a/go.mod
+++ b/go.mod
@@ -8,8 +8,4 @@ require (
)
require golang.org/x/crypto v0.27.0 // indirect
-
-require (
- github.com/pelletier/go-toml/v2 v2.2.3 // indirect
- github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
-)
+require github.com/pelletier/go-toml/v2 v2.2.3 // indirect
diff --git a/go.sum b/go.sum
index 15259a1..35e1b20 100644
--- a/go.sum
+++ b/go.sum
@@ -8,9 +8,7 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
-github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
-github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
-github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
-github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
+github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
+github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
diff --git a/main.go b/main.go
index 7e8e06f..23fc997 100644
--- a/main.go
+++ b/main.go
@@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"log"
- "math"
"math/rand"
"net/http"
"os"
@@ -23,7 +22,6 @@ import (
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
- "golang.org/x/crypto/bcrypt"
)
// used for database migrations
@@ -89,19 +87,19 @@ func main() {
}
username := os.Args[2]
totpName := os.Args[3]
+ secret := controller.GenerateTOTPSecret(controller.TOTP_SECRET_LENGTH)
- account, err := controller.GetAccountByUsername(app.DB, username)
+ account, err := controller.GetAccount(app.DB, username)
if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err)
+ fmt.Fprintf(os.Stderr, "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)
+ fmt.Fprintf(os.Stderr, "Account \"%s\" does not exist.\n", username)
os.Exit(1)
}
- secret := controller.GenerateTOTPSecret(controller.TOTP_SECRET_LENGTH)
totp := model.TOTP {
AccountID: account.ID,
Name: totpName,
@@ -110,11 +108,7 @@ func main() {
err = controller.CreateTOTP(app.DB, &totp)
if err != nil {
- if strings.HasPrefix(err.Error(), "pq: duplicate key") {
- fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" already has a TOTP method named \"%s\"!\n", account.Username, totp.Name)
- os.Exit(1)
- }
- fmt.Fprintf(os.Stderr, "FATAL: Failed to create TOTP method: %v\n", err)
+ fmt.Fprintf(os.Stderr, "Failed to create TOTP method: %v\n", err)
os.Exit(1)
}
@@ -130,20 +124,20 @@ func main() {
username := os.Args[2]
totpName := os.Args[3]
- account, err := controller.GetAccountByUsername(app.DB, username)
+ account, err := controller.GetAccount(app.DB, username)
if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err)
+ fmt.Fprintf(os.Stderr, "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)
+ fmt.Fprintf(os.Stderr, "Account \"%s\" does not exist.\n", username)
os.Exit(1)
}
err = controller.DeleteTOTP(app.DB, account.ID, totpName)
if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: Failed to create TOTP method: %v\n", err)
+ fmt.Fprintf(os.Stderr, "Failed to create TOTP method: %v\n", err)
os.Exit(1)
}
@@ -157,20 +151,20 @@ func main() {
}
username := os.Args[2]
- account, err := controller.GetAccountByUsername(app.DB, username)
+ account, err := controller.GetAccount(app.DB, username)
if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err)
+ fmt.Fprintf(os.Stderr, "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)
+ fmt.Fprintf(os.Stderr, "Account \"%s\" does not exist.\n", username)
os.Exit(1)
}
totps, err := controller.GetTOTPsForAccount(app.DB, account.ID)
if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: Failed to create TOTP methods: %v\n", err)
+ fmt.Fprintf(os.Stderr, "Failed to create TOTP methods: %v\n", err)
os.Exit(1)
}
@@ -190,61 +184,48 @@ func main() {
username := os.Args[2]
totpName := os.Args[3]
- account, err := controller.GetAccountByUsername(app.DB, username)
+ account, err := controller.GetAccount(app.DB, username)
if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err)
+ fmt.Fprintf(os.Stderr, "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)
+ fmt.Fprintf(os.Stderr, "Account \"%s\" does not exist.\n", username)
os.Exit(1)
}
totp, err := controller.GetTOTP(app.DB, account.ID, totpName)
if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch TOTP method \"%s\": %v\n", totpName, err)
+ fmt.Fprintf(os.Stderr, "Failed to fetch TOTP method \"%s\": %v\n", totpName, err)
os.Exit(1)
}
if totp == nil {
- fmt.Fprintf(os.Stderr, "FATAL: TOTP method \"%s\" does not exist for account \"%s\"\n", totpName, username)
+ fmt.Fprintf(os.Stderr, "TOTP method \"%s\" does not exist for account \"%s\"\n", totpName, username)
os.Exit(1)
}
code := controller.GenerateTOTP(totp.Secret, 0)
fmt.Printf("%s\n", code)
return
-
- case "cleanTOTP":
- err := controller.DeleteUnconfirmedTOTPs(app.DB)
- if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: Failed to clean up TOTP methods: %v\n", err)
- os.Exit(1)
- }
- fmt.Printf("Cleaned up dangling TOTP methods successfully.\n")
- return
case "createInvite":
fmt.Printf("Creating invite...\n")
invite, err := controller.CreateInvite(app.DB, 16, time.Hour * 24)
if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: Failed to create invite code: %v\n", err)
+ fmt.Fprintf(os.Stderr, "Failed to create invite code: %v\n", err)
os.Exit(1)
}
- fmt.Printf(
- "Here you go! This code expires in %d hours: %s\n",
- int(math.Ceil(invite.ExpiresAt.Sub(invite.CreatedAt).Hours())),
- invite.Code,
- )
+ fmt.Printf("Here you go! This code expires in 24 hours: %s\n", invite.Code)
return
case "purgeInvites":
fmt.Printf("Deleting all invites...\n")
err := controller.DeleteAllInvites(app.DB)
if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: Failed to delete invites: %v\n", err)
+ fmt.Fprintf(os.Stderr, "Failed to delete invites: %v\n", err)
os.Exit(1)
}
@@ -254,13 +235,11 @@ func main() {
case "listAccounts":
accounts, err := controller.GetAllAccounts(app.DB)
if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch accounts: %v\n", err)
+ fmt.Fprintf(os.Stderr, "Failed to fetch accounts: %v\n", err)
os.Exit(1)
}
for _, account := range accounts {
- email := "
"
- if account.Email.Valid { email = account.Email.String }
fmt.Printf(
"User: %s\n" +
"\tID: %s\n" +
@@ -268,45 +247,12 @@ func main() {
"\tCreated: %s\n",
account.Username,
account.ID,
- email,
+ account.Email,
account.CreatedAt,
)
}
return
- case "changePassword":
- if len(os.Args) < 4 {
- fmt.Fprintf(os.Stderr, "FATAL: `username` and `password` must be specified for changePassword\n")
- os.Exit(1)
- }
-
- username := os.Args[2]
- password := os.Args[3]
- 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)
- }
-
- hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
- if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: Failed to update password: %v\n", err)
- os.Exit(1)
- }
- account.Password = string(hashedPassword)
- err = controller.UpdateAccount(app.DB, account)
- if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: Failed to delete account: %v\n", err)
- os.Exit(1)
- }
-
- fmt.Printf("Account \"%s\" deleted successfully.\n", account.Username)
- return
-
case "deleteAccount":
if len(os.Args) < 3 {
fmt.Fprintf(os.Stderr, "FATAL: `username` must be specified for deleteAccount\n")
@@ -315,14 +261,14 @@ func main() {
username := os.Args[2]
fmt.Printf("Deleting account \"%s\"...\n", username)
- account, err := controller.GetAccountByUsername(app.DB, username)
+ account, err := controller.GetAccount(app.DB, username)
if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err)
+ fmt.Fprintf(os.Stderr, "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)
+ fmt.Fprintf(os.Stderr, "Account \"%s\" does not exist.\n", username)
os.Exit(1)
}
@@ -333,9 +279,9 @@ func main() {
return
}
- err = controller.DeleteAccount(app.DB, account.ID)
+ err = controller.DeleteAccount(app.DB, username)
if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: Failed to delete account: %v\n", err)
+ fmt.Fprintf(os.Stderr, "Failed to delete account: %v\n", err)
os.Exit(1)
}
@@ -351,7 +297,6 @@ func main() {
"listTOTP :\n\tLists an account's TOTP methods.\n" +
"deleteTOTP :\n\tDeletes an account's TOTP method.\n" +
"testTOTP :\n\tGenerates the code for an account's TOTP method.\n" +
- "cleanTOTP:\n\tCleans up unconfirmed (dangling) TOTP methods.\n" +
"\n" +
"createInvite:\n\tCreates an invite code to register new accounts.\n" +
"purgeInvites:\n\tDeletes all available invite codes.\n" +
@@ -391,18 +336,11 @@ func main() {
os.Exit(1)
}
- // clean up unconfirmed TOTP methods
- err = controller.DeleteUnconfirmedTOTPs(app.DB)
- if err != nil {
- fmt.Fprintf(os.Stderr, "FATAL: Failed to clean up unconfirmed TOTP methods: %v\n", err)
- os.Exit(1)
- }
-
// start the web server!
mux := createServeMux(&app)
- fmt.Printf("Now serving at http://%s:%d\n", app.Config.Host, app.Config.Port)
+ fmt.Printf("Now serving at %s:%d\n", app.Config.BaseUrl, app.Config.Port)
log.Fatal(
- http.ListenAndServe(fmt.Sprintf("%s:%d", app.Config.Host, app.Config.Port),
+ http.ListenAndServe(fmt.Sprintf(":%d", app.Config.Port),
HTTPLog(DefaultHeaders(mux)),
))
}
@@ -421,7 +359,7 @@ func createServeMux(app *model.AppState) *http.ServeMux {
}
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
- err := templates.IndexTemplate.Execute(w, nil)
+ err := templates.Pages["index"].Execute(w, nil)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
diff --git a/model/account.go b/model/account.go
index 166e880..72720a0 100644
--- a/model/account.go
+++ b/model/account.go
@@ -1,20 +1,17 @@
package model
-import (
- "database/sql"
- "time"
-)
+import "time"
-const COOKIE_TOKEN string = "AM_SESSION"
+const COOKIE_TOKEN string = "AM_TOKEN"
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"`
+ ID string `json:"id" db:"id"`
+ Username string `json:"username" db:"username"`
+ Password string `json:"password" db:"password"`
+ Email string `json:"email" db:"email"`
+ AvatarURL string `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 6a965d5..08016b7 100644
--- a/model/appstate.go
+++ b/model/appstate.go
@@ -19,7 +19,6 @@ type (
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"`
DB DBConfig `toml:"db"`
diff --git a/model/session.go b/model/session.go
deleted file mode 100644
index de016e1..0000000
--- a/model/session.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package model
-
-import (
- "database/sql"
- "time"
-)
-
-type Session struct {
- Token string `json:"-" db:"token"`
- UserAgent string `json:"user_agent" db:"user_agent"`
- CreatedAt time.Time `json:"created_at" db:"created_at"`
- ExpiresAt time.Time `json:"-" db:"expires_at"`
-
- Account *Account `json:"-" db:"-"`
- AttemptAccount *Account `json:"-" db:"-"`
- Message sql.NullString `json:"-" db:"message"`
- Error sql.NullString `json:"-" db:"error"`
-}
diff --git a/model/token.go b/model/token.go
new file mode 100644
index 0000000..31beb72
--- /dev/null
+++ b/model/token.go
@@ -0,0 +1,11 @@
+package model
+
+import "time"
+
+type Token struct {
+ Token string `json:"token" db:"token"`
+ AccountID string `json:"-" db:"account"`
+ UserAgent string `json:"user_agent" db:"user_agent"`
+ CreatedAt time.Time `json:"created_at" db:"created_at"`
+ ExpiresAt time.Time `json:"expires_at" db:"expires_at"`
+}
diff --git a/model/totp.go b/model/totp.go
index cfad10a..8d8422f 100644
--- a/model/totp.go
+++ b/model/totp.go
@@ -9,5 +9,4 @@ type TOTP struct {
AccountID string `json:"accountID" db:"account"`
Secret string `json:"-" db:"secret"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
- Confirmed bool `json:"-" db:"confirmed"`
}
diff --git a/model/track.go b/model/track.go
index ca54ddd..d44c224 100644
--- a/model/track.go
+++ b/model/track.go
@@ -12,8 +12,6 @@ type (
Description string `json:"description"`
Lyrics string `json:"lyrics" db:"lyrics"`
PreviewURL string `json:"previewURL" db:"preview_url"`
-
- Number int
}
)
diff --git a/public/style/footer.css b/public/style/footer.css
index d9682b1..72cdebb 100644
--- a/public/style/footer.css
+++ b/public/style/footer.css
@@ -1,11 +1,11 @@
footer {
- border-top: 1px solid #8888;
+ border-top: 1px solid #888;
}
#footer {
- width: min(calc(100% - 4rem), 720px);
- margin: auto;
- padding: 2rem 0;
- color: #aaa;
+ width: min(calc(100% - 4rem), 720px);
+ margin: auto;
+ padding: 2rem 0;
+ color: #aaa;
}
diff --git a/public/style/index.css b/public/style/index.css
index f3cb761..363c381 100644
--- a/public/style/index.css
+++ b/public/style/index.css
@@ -91,7 +91,7 @@ hr {
text-align: center;
line-height: 0px;
border-width: 1px 0 0 0;
- border-color: #888;
+ border-color: #888f;
margin: 1.5em 0;
overflow: visible;
}
diff --git a/schema-migration/000-init.sql b/schema_migration/000-init.sql
similarity index 80%
rename from schema-migration/000-init.sql
rename to schema_migration/000-init.sql
index 42b982a..3f4c723 100644
--- a/schema-migration/000-init.sql
+++ b/schema_migration/000-init.sql
@@ -1,53 +1,51 @@
+CREATE SCHEMA IF NOT EXISTS arimelody;
+
--
-- Tables
--
-- Accounts
CREATE TABLE arimelody.account (
- id UUID DEFAULT gen_random_uuid(),
- username TEXT NOT NULL UNIQUE,
- password TEXT NOT NULL,
- email TEXT,
- avatar_url TEXT,
+ id uuid DEFAULT gen_random_uuid(),
+ username text NOT NULL UNIQUE,
+ password text NOT NULL,
+ email text,
+ avatar_url text,
created_at TIMESTAMP DEFAULT current_timestamp
);
ALTER TABLE arimelody.account ADD CONSTRAINT account_pk PRIMARY KEY (id);
-- Privilege
CREATE TABLE arimelody.privilege (
- account UUID NOT NULL,
- privilege TEXT NOT NULL
+ account uuid NOT NULL,
+ privilege text NOT NULL
);
ALTER TABLE arimelody.privilege ADD CONSTRAINT privilege_pk PRIMARY KEY (account, privilege);
-- Invites
CREATE TABLE arimelody.invite (
code text NOT NULL,
- created_at TIMESTAMP NOT NULL DEFAULT current_timestamp,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL
);
ALTER TABLE arimelody.invite ADD CONSTRAINT invite_pk PRIMARY KEY (code);
--- Sessions
-CREATE TABLE arimelody.session (
+-- Tokens
+CREATE TABLE arimelody.token (
token TEXT,
+ account UUID NOT NULL,
user_agent TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT current_timestamp,
- expires_at TIMESTAMP DEFAULT NULL,
- account UUID,
- attempt_account UUID,
- message TEXT,
- error TEXT
+ expires_at TIMESTAMP DEFAULT NULL
);
-ALTER TABLE arimelody.session ADD CONSTRAINT session_pk PRIMARY KEY (token);
+ALTER TABLE arimelody.token ADD CONSTRAINT token_pk PRIMARY KEY (token);
--- TOTP methods
+-- TOTPs
CREATE TABLE arimelody.totp (
name TEXT NOT NULL,
account UUID NOT NULL,
secret TEXT,
- created_at TIMESTAMP NOT NULL DEFAULT current_timestamp,
- confirmed BOOLEAN DEFAULT false
+ created_at TIMESTAMP NOT NULL DEFAULT current_timestamp
);
ALTER TABLE arimelody.totp ADD CONSTRAINT totp_pk PRIMARY KEY (account, name);
@@ -120,8 +118,7 @@ ALTER TABLE arimelody.musicreleasetrack ADD CONSTRAINT musicreleasetrack_pk PRIM
--
ALTER TABLE arimelody.privilege ADD CONSTRAINT privilege_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE;
-ALTER TABLE arimelody.session ADD CONSTRAINT session_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE;
-ALTER TABLE arimelody.session ADD CONSTRAINT session_attempt_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE;
+ALTER TABLE arimelody.token ADD CONSTRAINT token_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE;
ALTER TABLE arimelody.totp ADD CONSTRAINT totp_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE;
ALTER TABLE arimelody.musiccredit ADD CONSTRAINT musiccredit_artist_fk FOREIGN KEY (artist) REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/schema-migration/001-pre-versioning.sql b/schema_migration/001-pre-versioning.sql
similarity index 51%
rename from schema-migration/001-pre-versioning.sql
rename to schema_migration/001-pre-versioning.sql
index 1447bae..62bc15b 100644
--- a/schema-migration/001-pre-versioning.sql
+++ b/schema_migration/001-pre-versioning.sql
@@ -1,58 +1,67 @@
+--
+-- Migration
+--
+
+-- Move existing tables to new schema
+ALTER TABLE public.artist SET SCHEMA arimelody;
+ALTER TABLE public.musicrelease SET SCHEMA arimelody;
+ALTER TABLE public.musiclink SET SCHEMA arimelody;
+ALTER TABLE public.musiccredit SET SCHEMA arimelody;
+ALTER TABLE public.musictrack SET SCHEMA arimelody;
+ALTER TABLE public.musicreleasetrack SET SCHEMA arimelody;
+
+
+
--
-- New items
--
--- Accounts
+-- Acounts
CREATE TABLE arimelody.account (
- id UUID DEFAULT gen_random_uuid(),
- username TEXT NOT NULL UNIQUE,
- password TEXT NOT NULL,
- email TEXT,
- avatar_url TEXT,
+ id uuid DEFAULT gen_random_uuid(),
+ username text NOT NULL UNIQUE,
+ password text NOT NULL,
+ email text,
+ avatar_url text,
created_at TIMESTAMP DEFAULT current_timestamp
);
ALTER TABLE arimelody.account ADD CONSTRAINT account_pk PRIMARY KEY (id);
-- Privilege
CREATE TABLE arimelody.privilege (
- account UUID NOT NULL,
- privilege TEXT NOT NULL
+ account uuid NOT NULL,
+ privilege text NOT NULL
);
ALTER TABLE arimelody.privilege ADD CONSTRAINT privilege_pk PRIMARY KEY (account, privilege);
-- Invites
CREATE TABLE arimelody.invite (
code text NOT NULL,
- created_at TIMESTAMP NOT NULL DEFAULT current_timestamp,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL
);
ALTER TABLE arimelody.invite ADD CONSTRAINT invite_pk PRIMARY KEY (code);
--- Sessions
-CREATE TABLE arimelody.session (
+-- Tokens
+CREATE TABLE arimelody.token (
token TEXT,
+ account UUID NOT NULL,
user_agent TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT current_timestamp,
- expires_at TIMESTAMP DEFAULT NULL,
- account UUID,
- attempt_account UUID,
- message TEXT,
- error TEXT
+ expires_at TIMESTAMP DEFAULT NULL
);
-ALTER TABLE arimelody.session ADD CONSTRAINT session_pk PRIMARY KEY (token);
+ALTER TABLE arimelody.token ADD CONSTRAINT token_pk PRIMARY KEY (token);
--- TOTP methods
+-- TOTPs
CREATE TABLE arimelody.totp (
name TEXT NOT NULL,
account UUID NOT NULL,
secret TEXT,
- created_at TIMESTAMP NOT NULL DEFAULT current_timestamp,
- confirmed BOOLEAN DEFAULT false
+ created_at TIMESTAMP NOT NULL DEFAULT current_timestamp
);
ALTER TABLE arimelody.totp ADD CONSTRAINT totp_pk PRIMARY KEY (account, name);
-- Foreign keys
ALTER TABLE arimelody.privilege ADD CONSTRAINT privilege_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE;
-ALTER TABLE arimelody.session ADD CONSTRAINT session_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE;
-ALTER TABLE arimelody.session ADD CONSTRAINT session_attempt_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE;
+ALTER TABLE arimelody.token ADD CONSTRAINT token_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE;
ALTER TABLE arimelody.totp ADD CONSTRAINT totp_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE;
diff --git a/templates/templates.go b/templates/templates.go
index 8d1a5ca..094e61d 100644
--- a/templates/templates.go
+++ b/templates/templates.go
@@ -5,24 +5,29 @@ import (
"path/filepath"
)
-var IndexTemplate = template.Must(template.ParseFiles(
- filepath.Join("views", "layout.html"),
- filepath.Join("views", "header.html"),
- filepath.Join("views", "footer.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("views", "index.html"),
-))
-var MusicTemplate = template.Must(template.ParseFiles(
- filepath.Join("views", "layout.html"),
- filepath.Join("views", "header.html"),
- filepath.Join("views", "footer.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("views", "music.html"),
-))
-var MusicGatewayTemplate = template.Must(template.ParseFiles(
- filepath.Join("views", "layout.html"),
- filepath.Join("views", "header.html"),
- filepath.Join("views", "footer.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("views", "music-gateway.html"),
-))
+var Pages = map[string]*template.Template{
+ "index": template.Must(template.ParseFiles(
+ filepath.Join("views", "layout.html"),
+ filepath.Join("views", "header.html"),
+ filepath.Join("views", "footer.html"),
+ filepath.Join("views", "prideflag.html"),
+ filepath.Join("views", "index.html"),
+ )),
+ "music": template.Must(template.ParseFiles(
+ filepath.Join("views", "layout.html"),
+ filepath.Join("views", "header.html"),
+ filepath.Join("views", "footer.html"),
+ filepath.Join("views", "prideflag.html"),
+ filepath.Join("views", "music.html"),
+ )),
+ "music-gateway": template.Must(template.ParseFiles(
+ filepath.Join("views", "layout.html"),
+ filepath.Join("views", "header.html"),
+ filepath.Join("views", "footer.html"),
+ filepath.Join("views", "prideflag.html"),
+ filepath.Join("views", "music-gateway.html"),
+ )),
+}
+
+var Components = map[string]*template.Template{
+}
diff --git a/view/music.go b/view/music.go
index 89e428c..cae325f 100644
--- a/view/music.go
+++ b/view/music.go
@@ -48,7 +48,7 @@ func ServeCatalog(app *model.AppState) http.Handler {
}
}
- err = templates.MusicTemplate.Execute(w, releases)
+ err = templates.Pages["music"].Execute(w, releases)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
@@ -60,14 +60,13 @@ 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.DB, r)
+ account, err := controller.GetAccountByRequest(app.DB, r)
if err != nil {
- fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve session: %v\n", err)
+ fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
-
- if session != nil && session.Account != nil {
+ if account != nil {
// TODO: check privilege on release
privileged = true
}
@@ -86,7 +85,7 @@ func ServeGateway(app *model.AppState, release *model.Release) http.Handler {
response.Links = release.Links
}
- err := templates.MusicGatewayTemplate.Execute(w, response)
+ err := templates.Pages["music-gateway"].Execute(w, response)
if err != nil {
fmt.Printf("Error rendering music gateway for %s: %s\n", release.ID, err)