API login/register/delete-account, automatic db schema init
This commit is contained in:
parent
1846203076
commit
f7edece0af
|
@ -6,6 +6,8 @@
|
||||||
uploads/*
|
uploads/*
|
||||||
test/
|
test/
|
||||||
tmp/
|
tmp/
|
||||||
|
db/
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
|
docker-compose-test.yml
|
||||||
Dockerfile
|
Dockerfile
|
||||||
schema.sql
|
schema.sql
|
||||||
|
|
60
account/controller/account.go
Normal file
60
account/controller/account.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"arimelody-web/account/model"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAccountByEmail(db *sqlx.DB, email string) (*model.Account, error) {
|
||||||
|
var account = model.Account{}
|
||||||
|
|
||||||
|
err := db.Get(&account, "SELECT * FROM account WHERE email=$1", email)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateAccount(db *sqlx.DB, account *model.Account) error {
|
||||||
|
_, err := db.Exec(
|
||||||
|
"INSERT INTO account (username, password, email, avatar_url) " +
|
||||||
|
"VALUES ($1, $2, $3, $4)",
|
||||||
|
account.Username,
|
||||||
|
account.Password,
|
||||||
|
account.Email,
|
||||||
|
account.AvatarURL)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateAccount(db *sqlx.DB, account *model.Account) error {
|
||||||
|
_, err := db.Exec(
|
||||||
|
"UPDATE account " +
|
||||||
|
"SET username=$2, password=$3, email=$4, avatar_url=$5) " +
|
||||||
|
"WHERE id=$1",
|
||||||
|
account.ID,
|
||||||
|
account.Username,
|
||||||
|
account.Password,
|
||||||
|
account.Email,
|
||||||
|
account.AvatarURL)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteAccount(db *sqlx.DB, accountID string) error {
|
||||||
|
_, err := db.Exec("DELETE FROM account WHERE id=$1", accountID)
|
||||||
|
return err
|
||||||
|
}
|
54
account/model/account.go
Normal file
54
account/model/account.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Account struct {
|
||||||
|
ID string `json:"id" db:"id"`
|
||||||
|
Username string `json:"username" db:"username"`
|
||||||
|
Password []byte `json:"password" db:"password"`
|
||||||
|
Email string `json:"email" db:"email"`
|
||||||
|
AvatarURL string `json:"avatar_url" db:"avatar_url"`
|
||||||
|
Privileges []AccountPrivilege `json:"privileges"`
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountPrivilege string
|
||||||
|
|
||||||
|
Invite struct {
|
||||||
|
Code string `db:"code"`
|
||||||
|
CreatedByID string `db:"created_by"`
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
ExpiresAt time.Time `db:"expires_at"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Root AccountPrivilege = "root" // grants all permissions. very dangerous to grant!
|
||||||
|
|
||||||
|
// unused for now
|
||||||
|
CreateInvites AccountPrivilege = "create_invites"
|
||||||
|
ReadAccounts AccountPrivilege = "read_accounts"
|
||||||
|
EditAccounts AccountPrivilege = "edit_accounts"
|
||||||
|
|
||||||
|
ReadReleases AccountPrivilege = "read_releases"
|
||||||
|
EditReleases AccountPrivilege = "edit_releases"
|
||||||
|
|
||||||
|
ReadTracks AccountPrivilege = "read_tracks"
|
||||||
|
EditTracks AccountPrivilege = "edit_tracks"
|
||||||
|
|
||||||
|
ReadArtists AccountPrivilege = "read_artists"
|
||||||
|
EditArtists AccountPrivilege = "edit_artists"
|
||||||
|
)
|
||||||
|
|
||||||
|
var inviteChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||||
|
|
||||||
|
func GenerateInviteCode(length int) []byte {
|
||||||
|
code := []byte{}
|
||||||
|
for i := 0; i < length; i++ {
|
||||||
|
code = append(code, inviteChars[rand.Intn(len(inviteChars) - 1)])
|
||||||
|
}
|
||||||
|
return code
|
||||||
|
}
|
|
@ -31,7 +31,7 @@ var ADMIN_BYPASS = func() bool {
|
||||||
var ADMIN_ID_DISCORD = func() string {
|
var ADMIN_ID_DISCORD = func() string {
|
||||||
id := os.Getenv("DISCORD_ADMIN")
|
id := os.Getenv("DISCORD_ADMIN")
|
||||||
if id == "" {
|
if id == "" {
|
||||||
fmt.Printf("WARN: Discord admin ID (DISCORD_ADMIN) was not provided. Admin login will be unavailable.\n")
|
// fmt.Printf("WARN: Discord admin ID (DISCORD_ADMIN) was not provided.\n")
|
||||||
}
|
}
|
||||||
return id
|
return id
|
||||||
}()
|
}()
|
||||||
|
|
175
api/account.go
Normal file
175
api/account.go
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"arimelody-web/account/controller"
|
||||||
|
"arimelody-web/account/model"
|
||||||
|
"arimelody-web/global"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleLogin() http.HandlerFunc {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginRequest struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
credentials := LoginRequest{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&credentials)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := controller.GetAccount(global.DB, credentials.Username)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
|
http.Error(w, "Invalid username or password", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve account: %s\n", err.Error())
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bcrypt.CompareHashAndPassword(account.Password, []byte(credentials.Password))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid username or password", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: sessions and tokens
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("Logged in successfully. TODO: Session tokens\n"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAccountRegistration() http.HandlerFunc {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegisterRequest struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
credentials := RegisterRequest{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&credentials)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure code exists in DB
|
||||||
|
invite := model.Invite{}
|
||||||
|
err = global.DB.Get(&invite, "SELECT * FROM invite WHERE code=$1", credentials.Code)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
|
http.Error(w, "Invalid invite code", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve invite: %s\n", err.Error())
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().After(invite.ExpiresAt) {
|
||||||
|
http.Error(w, "Invalid invite code", http.StatusBadRequest)
|
||||||
|
_, err = global.DB.Exec("DELETE FROM invite WHERE code=$1", credentials.Code)
|
||||||
|
if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %s\n", err.Error()) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(credentials.Password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "WARN: Failed to generate password hash: %s\n", err.Error())
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
account := model.Account{
|
||||||
|
Username: credentials.Username,
|
||||||
|
Password: hashedPassword,
|
||||||
|
Email: credentials.Email,
|
||||||
|
AvatarURL: "/img/default-avatar.png",
|
||||||
|
}
|
||||||
|
err = controller.CreateAccount(global.DB, &account)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "WARN: Failed to create account: %s\n", err.Error())
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = global.DB.Exec("DELETE FROM invite WHERE code=$1", credentials.Code)
|
||||||
|
if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %s\n", err.Error()) }
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
w.Write([]byte("Account created successfully\n"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDeleteAccount() http.HandlerFunc {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginRequest struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
credentials := LoginRequest{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&credentials)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := controller.GetAccount(global.DB, credentials.Username)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
|
http.Error(w, "Invalid username or password", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve account: %s\n", err.Error())
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bcrypt.CompareHashAndPassword(account.Password, []byte(credentials.Password))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid username or password", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = controller.DeleteAccount(global.DB, account.ID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "WARN: Failed to delete account: %s\n", err.Error())
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("Account deleted successfully\n"))
|
||||||
|
})
|
||||||
|
}
|
|
@ -14,6 +14,12 @@ import (
|
||||||
func Handler() http.Handler {
|
func Handler() http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
// ACCOUNT ENDPOINTS
|
||||||
|
|
||||||
|
mux.Handle("/v1/login", handleLogin())
|
||||||
|
mux.Handle("/v1/register", handleAccountRegistration())
|
||||||
|
mux.Handle("/v1/delete-account", handleDeleteAccount())
|
||||||
|
|
||||||
// ARTIST ENDPOINTS
|
// ARTIST ENDPOINTS
|
||||||
|
|
||||||
mux.Handle("/v1/artist/", http.StripPrefix("/v1/artist", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
mux.Handle("/v1/artist/", http.StripPrefix("/v1/artist", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
@ -18,7 +18,7 @@ var CREDENTIALS_PROVIDED = true
|
||||||
var CLIENT_ID = func() string {
|
var CLIENT_ID = func() string {
|
||||||
id := os.Getenv("DISCORD_CLIENT")
|
id := os.Getenv("DISCORD_CLIENT")
|
||||||
if id == "" {
|
if id == "" {
|
||||||
fmt.Printf("WARN: Discord client ID (DISCORD_CLIENT) was not provided. Admin login will be unavailable.\n")
|
// fmt.Printf("WARN: Discord client ID (DISCORD_CLIENT) was not provided.\n")
|
||||||
CREDENTIALS_PROVIDED = false
|
CREDENTIALS_PROVIDED = false
|
||||||
}
|
}
|
||||||
return id
|
return id
|
||||||
|
@ -26,7 +26,7 @@ var CLIENT_ID = func() string {
|
||||||
var CLIENT_SECRET = func() string {
|
var CLIENT_SECRET = func() string {
|
||||||
secret := os.Getenv("DISCORD_SECRET")
|
secret := os.Getenv("DISCORD_SECRET")
|
||||||
if secret == "" {
|
if secret == "" {
|
||||||
fmt.Printf("WARN: Discord secret (DISCORD_SECRET) was not provided. Admin login will be unavailable.\n")
|
// fmt.Printf("WARN: Discord secret (DISCORD_SECRET) was not provided.\n")
|
||||||
CREDENTIALS_PROVIDED = false
|
CREDENTIALS_PROVIDED = false
|
||||||
}
|
}
|
||||||
return secret
|
return secret
|
||||||
|
|
|
@ -34,7 +34,7 @@ var Args = func() map[string]string {
|
||||||
return args
|
return args
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var HTTP_DOMAIN = func() string {
|
var HTTP_DOMAIN = func() string {
|
||||||
domain := os.Getenv("HTTP_DOMAIN")
|
domain := os.Getenv("HTTP_DOMAIN")
|
||||||
if domain == "" {
|
if domain == "" {
|
||||||
return "https://arimelody.me"
|
return "https://arimelody.me"
|
||||||
|
@ -42,4 +42,13 @@ var HTTP_DOMAIN = func() string {
|
||||||
return domain
|
return domain
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
var APP_SECRET = func() string {
|
||||||
|
secret := os.Getenv("ARIMELODY_SECRET")
|
||||||
|
if secret == "" {
|
||||||
|
fmt.Fprintln(os.Stderr, "FATAL: ARIMELODY_SECRET was not provided. Cannot continue.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return secret
|
||||||
|
}()
|
||||||
|
|
||||||
var DB *sqlx.DB
|
var DB *sqlx.DB
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -6,3 +6,5 @@ require (
|
||||||
github.com/jmoiron/sqlx v1.4.0
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require golang.org/x/crypto v0.27.0 // indirect
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -8,3 +8,5 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
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 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||||
|
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||||
|
|
169
main.go
169
main.go
|
@ -9,6 +9,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"arimelody-web/account/model"
|
||||||
"arimelody-web/admin"
|
"arimelody-web/admin"
|
||||||
"arimelody-web/api"
|
"arimelody-web/api"
|
||||||
"arimelody-web/global"
|
"arimelody-web/global"
|
||||||
|
@ -27,9 +28,9 @@ func main() {
|
||||||
if dbHost == "" { dbHost = "127.0.0.1" }
|
if dbHost == "" { dbHost = "127.0.0.1" }
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
global.DB, err = sqlx.Connect("postgres", "host=" + dbHost + " user=arimelody dbname=arimelody password=fuckingpassword sslmode=disable")
|
global.DB, err = initDB("postgres", "host=" + dbHost + " user=arimelody dbname=arimelody password=fuckingpassword sslmode=disable")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "FATAL: Unable to create database connection pool: %v\n", err)
|
fmt.Fprintf(os.Stderr, "FATAL: Unable to initialise database: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
global.DB.SetConnMaxLifetime(time.Minute * 3)
|
global.DB.SetConnMaxLifetime(time.Minute * 3)
|
||||||
|
@ -37,6 +38,41 @@ func main() {
|
||||||
global.DB.SetMaxIdleConns(10)
|
global.DB.SetMaxIdleConns(10)
|
||||||
defer global.DB.Close()
|
defer global.DB.Close()
|
||||||
|
|
||||||
|
_, err = global.DB.Exec("DELETE FROM invite WHERE expires_at < CURRENT_TIMESTAMP")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to clear expired invite codes: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
accountsCount := 0
|
||||||
|
global.DB.Get(&accountsCount, "SELECT count(*) FROM account")
|
||||||
|
if accountsCount == 0 {
|
||||||
|
code := model.GenerateInviteCode(8)
|
||||||
|
|
||||||
|
tx, err := global.DB.Begin()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to begin transaction: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
_, err = tx.Exec("DELETE FROM invite")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to clear existing invite codes: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
_, err = tx.Exec("INSERT INTO invite (code,expires_at) VALUES ($1, $2)", code, time.Now().Add(60 * time.Minute))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to create invite codes: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to create invite codes: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(os.Stdout, "INFO: No accounts exist! Generated invite code: " + string(code) + " (Use this at /register or /api/v1/register)")
|
||||||
|
}
|
||||||
|
|
||||||
// start the web server!
|
// start the web server!
|
||||||
mux := createServeMux()
|
mux := createServeMux()
|
||||||
port := DEFAULT_PORT
|
port := DEFAULT_PORT
|
||||||
|
@ -44,9 +80,134 @@ func main() {
|
||||||
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), global.HTTPLog(mux)))
|
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), global.HTTPLog(mux)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initDB(driverName string, dataSourceName string) (*sqlx.DB, error) {
|
||||||
|
db, err := sqlx.Connect(driverName, dataSourceName)
|
||||||
|
if err != nil { return nil, err }
|
||||||
|
|
||||||
|
// ensure tables exist
|
||||||
|
// account
|
||||||
|
_, err = db.Exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS account (" +
|
||||||
|
"id uuid PRIMARY KEY DEFAULT gen_random_uuid(), " +
|
||||||
|
"username text NOT NULL UNIQUE, " +
|
||||||
|
"password text NOT NULL, " +
|
||||||
|
"email text, " +
|
||||||
|
"avatar_url text)",
|
||||||
|
)
|
||||||
|
if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create account table: %s", err.Error())) }
|
||||||
|
|
||||||
|
// privilege
|
||||||
|
_, err = db.Exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS privilege (" +
|
||||||
|
"account uuid NOT NULL, " +
|
||||||
|
"privilege text NOT NULL, " +
|
||||||
|
"CONSTRAINT privilege_pk PRIMARY KEY (account, privilege), " +
|
||||||
|
"CONSTRAINT privilege_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE)",
|
||||||
|
)
|
||||||
|
if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create privilege table: %s", err.Error())) }
|
||||||
|
|
||||||
|
// totp
|
||||||
|
_, err = db.Exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS totp (" +
|
||||||
|
"account uuid NOT NULL, " +
|
||||||
|
"name text NOT NULL, " +
|
||||||
|
"created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
|
||||||
|
"CONSTRAINT totp_pk PRIMARY KEY (account, name), " +
|
||||||
|
"CONSTRAINT totp_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE)",
|
||||||
|
)
|
||||||
|
if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create TOTP table: %s", err.Error())) }
|
||||||
|
|
||||||
|
// invites
|
||||||
|
_, err = db.Exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS invite (" +
|
||||||
|
"code text NOT NULL PRIMARY KEY, " +
|
||||||
|
"created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
|
||||||
|
"expires_at TIMESTAMP NOT NULL)",
|
||||||
|
)
|
||||||
|
if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create TOTP table: %s", err.Error())) }
|
||||||
|
|
||||||
|
// artist
|
||||||
|
_, err = db.Exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS artist (" +
|
||||||
|
"id character varying(64) PRIMARY KEY, " +
|
||||||
|
"name text NOT NULL, " +
|
||||||
|
"website text, " +
|
||||||
|
"avatar text)",
|
||||||
|
)
|
||||||
|
if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create artist table: %s", err.Error())) }
|
||||||
|
|
||||||
|
// musicrelease
|
||||||
|
_, err = db.Exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS musicrelease (" +
|
||||||
|
"id character varying(64) PRIMARY KEY, " +
|
||||||
|
"visible bool DEFAULT false, " +
|
||||||
|
"title text NOT NULL, " +
|
||||||
|
"description text, " +
|
||||||
|
"type text, " +
|
||||||
|
"release_date TIMESTAMP NOT NULL, " +
|
||||||
|
"artwork text, " +
|
||||||
|
"buyname text, " +
|
||||||
|
"buylink text, " +
|
||||||
|
"copyright text, " +
|
||||||
|
"copyrightURL text)",
|
||||||
|
)
|
||||||
|
if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create musicrelease table: %s", err.Error())) }
|
||||||
|
|
||||||
|
// musiclink
|
||||||
|
_, err = db.Exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS public.musiclink (" +
|
||||||
|
"release character varying(64) NOT NULL, " +
|
||||||
|
"name text NOT NULL, " +
|
||||||
|
"url text NOT NULL, " +
|
||||||
|
"CONSTRAINT musiclink_pk PRIMARY KEY (release, name), " +
|
||||||
|
"CONSTRAINT musiclink_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE)",
|
||||||
|
)
|
||||||
|
if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create musiclink table: %s", err.Error())) }
|
||||||
|
|
||||||
|
// musiccredit
|
||||||
|
_, err = db.Exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS public.musiccredit (" +
|
||||||
|
"release character varying(64) NOT NULL, " +
|
||||||
|
"artist character varying(64) NOT NULL, " +
|
||||||
|
"role text NOT NULL, " +
|
||||||
|
"is_primary boolean DEFAULT false, " +
|
||||||
|
"CONSTRAINT musiccredit_pk PRIMARY KEY (release, artist), " +
|
||||||
|
"CONSTRAINT musiccredit_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE, " +
|
||||||
|
"CONSTRAINT musiccredit_artist_fk FOREIGN KEY (artist) REFERENCES artist(id) ON DELETE CASCADE)",
|
||||||
|
)
|
||||||
|
if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create musiccredit table: %s", err.Error())) }
|
||||||
|
|
||||||
|
// musictrack
|
||||||
|
_, err = db.Exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS public.musictrack (" +
|
||||||
|
"id uuid DEFAULT gen_random_uuid() PRIMARY KEY, " +
|
||||||
|
"title text NOT NULL, " +
|
||||||
|
"description text, " +
|
||||||
|
"lyrics text, " +
|
||||||
|
"preview_url text)",
|
||||||
|
)
|
||||||
|
if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create musictrack table: %s", err.Error())) }
|
||||||
|
|
||||||
|
// musicreleasetrack
|
||||||
|
_, err = db.Exec(
|
||||||
|
"CREATE TABLE IF NOT EXISTS public.musicreleasetrack (" +
|
||||||
|
"release character varying(64) NOT NULL, " +
|
||||||
|
"track uuid NOT NULL, " +
|
||||||
|
"number integer NOT NULL, " +
|
||||||
|
"CONSTRAINT musicreleasetrack_pk PRIMARY KEY (release, track), " +
|
||||||
|
"CONSTRAINT musicreleasetrack_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE, " +
|
||||||
|
"CONSTRAINT musicreleasetrack_artist_fk FOREIGN KEY (track) REFERENCES track(id) ON DELETE CASCADE)",
|
||||||
|
)
|
||||||
|
if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create musicreleasetrack table: %s", err.Error())) }
|
||||||
|
|
||||||
|
// TODO: automatic database migration
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
func createServeMux() *http.ServeMux {
|
func createServeMux() *http.ServeMux {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler()))
|
mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler()))
|
||||||
mux.Handle("/api/", http.StripPrefix("/api", api.Handler()))
|
mux.Handle("/api/", http.StripPrefix("/api", api.Handler()))
|
||||||
mux.Handle("/music/", http.StripPrefix("/music", musicView.Handler()))
|
mux.Handle("/music/", http.StripPrefix("/music", musicView.Handler()))
|
||||||
|
@ -61,7 +222,7 @@ func createServeMux() *http.ServeMux {
|
||||||
}
|
}
|
||||||
staticHandler("public").ServeHTTP(w, r)
|
staticHandler("public").ServeHTTP(w, r)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue