diff --git a/README.md b/README.md index 81bc52f..0873ff6 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,6 @@ need to be up for this, making this ideal for some offline maintenance. - `createInvite`: Creates an invite code to register new accounts. - `purgeInvites`: Deletes all available invite codes. -- `listAccounts`: Lists all active accounts. - `deleteAccount `: Deletes an account with a given `username`. ## database diff --git a/admin/views/edit-account.html b/admin/views/edit-account.html deleted file mode 100644 index 4096d60..0000000 --- a/admin/views/edit-account.html +++ /dev/null @@ -1,63 +0,0 @@ -{{define "head"}} -Account Settings - ari melody 💫 - - -{{end}} - -{{define "content"}} -
-

Account Settings ({{.Account.Username}})

- -
-

Change Password

- -
-
- - - - - - - - -
- - -
-
- -
-

MFA Devices

-
-
- {{if .TOTPs}} - {{range .TOTPs}} -
-

{{.Name}}

-

{{.CreatedAt}}

-
- {{end}} - {{else}} -

You have no MFA devices.

- {{end}} - - Add MFA Device -
- -
-

Danger Zone

-
-
-

- Clicking the button below will delete your account. - This action is irreversible. - You will be prompted to confirm this decision. -

- -
- -
- - -{{end}} diff --git a/admin/views/login.html b/admin/views/login.html index f7edc9b..16c0fcc 100644 --- a/admin/views/login.html +++ b/admin/views/login.html @@ -87,13 +87,13 @@ button:active {
- + - + - +
diff --git a/api/api.go b/api/api.go index 30ef73e..af6c6a7 100644 --- a/api/api.go +++ b/api/api.go @@ -15,17 +15,9 @@ func Handler() http.Handler { // ACCOUNT ENDPOINTS - /* - // temporarily disabling these - // accounts should really be handled via the frontend rn, and juggling - // two different token bearer methods kinda sucks!! - // i'll look into generating API tokens on the frontend in the future - // TODO: generate API keys on the frontend - mux.Handle("/v1/login", handleLogin()) mux.Handle("/v1/register", handleAccountRegistration()) mux.Handle("/v1/delete-account", handleDeleteAccount()) - */ // ARTIST ENDPOINTS diff --git a/controller/account.go b/controller/account.go index 3547d35..362e297 100644 --- a/controller/account.go +++ b/controller/account.go @@ -11,17 +11,6 @@ import ( "github.com/jmoiron/sqlx" ) -func GetAllAccounts(db *sqlx.DB) ([]model.Account, error) { - var accounts = []model.Account{} - - err := db.Select(&accounts, "SELECT * FROM account ORDER BY created_at ASC") - if err != nil { - return nil, err - } - - return accounts, nil -} - func GetAccount(db *sqlx.DB, username string) (*model.Account, error) { var account = model.Account{} diff --git a/controller/totp.go b/controller/totp.go deleted file mode 100644 index 8dc9d69..0000000 --- a/controller/totp.go +++ /dev/null @@ -1,104 +0,0 @@ -package controller - -import ( - "arimelody-web/model" - "crypto/hmac" - "crypto/sha1" - "encoding/binary" - "fmt" - "math" - "net/url" - "time" - - "github.com/jmoiron/sqlx" -) - -const TIME_STEP int64 = 30 -const CODE_LENGTH = 6 - -func GenerateTOTP(secret string, timeStepOffset int) string { - counter := time.Now().Unix() / TIME_STEP - int64(timeStepOffset) - counterBytes := make([]byte, 8) - binary.BigEndian.PutUint64(counterBytes, uint64(counter)) - - mac := hmac.New(sha1.New, []byte(secret)) - mac.Write(counterBytes) - hash := mac.Sum(nil) - - offset := hash[len(hash) - 1] & 0x0f - binaryCode := int32(binary.BigEndian.Uint32(hash[offset : offset + 4]) & 0x7FFFFFFF) - code := binaryCode % int32(math.Pow10(CODE_LENGTH)) - - return fmt.Sprintf(fmt.Sprintf("%%0%dd", CODE_LENGTH), code) -} - -func GenerateTOTPURI(username string, secret string) string { - url := url.URL{ - Scheme: "otpauth", - Host: "totp", - Path: url.QueryEscape("arimelody.me") + ":" + url.QueryEscape(username), - } - - query := url.Query() - query.Set("secret", secret) - query.Set("issuer", "arimelody.me") - 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() -} - -func GetTOTPsForAccount(db *sqlx.DB, accountID string) ([]model.TOTP, error) { - totps := []model.TOTP{} - - err := db.Select( - &totps, - "SELECT * FROM totp " + - "WHERE account=$1 " + - "ORDER BY created_at ASC", - accountID, - ) - if err != nil { - return nil, err - } - - return totps, 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", - accountID, - ) - if err != nil { - return nil, err - } - - return &totp, nil -} - -func CreateTOTP(db *sqlx.DB, totp *model.TOTP) error { - _, err := db.Exec( - "INSERT INTO totp (account, name, secret) " + - "VALUES ($1,$2,$3)", - totp.AccountID, - totp.Name, - totp.Secret, - ) - return err -} - -func DeleteTOTP(db *sqlx.DB, accountID string, name string) error { - _, err := db.Exec( - "DELETE FROM totp WHERE account=$1 AND name=$2", - accountID, - name, - ) - return err -} diff --git a/main.go b/main.go index aa3cff6..2f0cb43 100644 --- a/main.go +++ b/main.go @@ -14,7 +14,6 @@ import ( "arimelody-web/api" "arimelody-web/controller" "arimelody-web/global" - "arimelody-web/model" "arimelody-web/templates" "arimelody-web/view" @@ -31,6 +30,10 @@ func main() { fmt.Printf("made with <3 by ari melody\n\n") // initialise database connection + if env := os.Getenv("ARIMELODY_DB_HOST"); env != "" { global.Config.DB.Host = env } + if env := os.Getenv("ARIMELODY_DB_NAME"); env != "" { global.Config.DB.Name = env } + if env := os.Getenv("ARIMELODY_DB_USER"); env != "" { global.Config.DB.User = env } + if env := os.Getenv("ARIMELODY_DB_PASS"); env != "" { global.Config.DB.Pass = env } if global.Config.DB.Host == "" { fmt.Fprintf(os.Stderr, "FATAL: db.host not provided! Exiting...\n") os.Exit(1) @@ -73,136 +76,6 @@ func main() { arg := os.Args[1] switch arg { - case "createTOTP": - if len(os.Args) < 4 { - fmt.Fprintf(os.Stderr, "FATAL: `username` and `name` must be specified for createTOTP.\n") - os.Exit(1) - } - username := os.Args[2] - totpName := os.Args[3] - secret := controller.GenerateAlnumString(32) - - account, err := controller.GetAccount(global.DB, username) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to fetch account \"%s\": %v\n", username, err) - os.Exit(1) - } - - if account == nil { - fmt.Fprintf(os.Stderr, "Account \"%s\" does not exist.\n", username) - os.Exit(1) - } - - totp := model.TOTP { - AccountID: account.ID, - Name: totpName, - Secret: string(secret), - } - - err = controller.CreateTOTP(global.DB, &totp) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to create TOTP method: %v\n", err) - os.Exit(1) - } - - url := controller.GenerateTOTPURI(account.Username, totp.Secret) - fmt.Printf("%s\n", url) - return - - case "deleteTOTP": - if len(os.Args) < 4 { - fmt.Fprintf(os.Stderr, "FATAL: `username` and `name` must be specified for deleteTOTP.\n") - os.Exit(1) - } - username := os.Args[2] - totpName := os.Args[3] - - account, err := controller.GetAccount(global.DB, username) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to fetch account \"%s\": %v\n", username, err) - os.Exit(1) - } - - if account == nil { - fmt.Fprintf(os.Stderr, "Account \"%s\" does not exist.\n", username) - os.Exit(1) - } - - err = controller.DeleteTOTP(global.DB, account.ID, totpName) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to create TOTP method: %v\n", err) - os.Exit(1) - } - - fmt.Printf("TOTP method \"%s\" deleted.\n", totpName) - return - - case "listTOTP": - if len(os.Args) < 3 { - fmt.Fprintf(os.Stderr, "FATAL: `username` must be specified for listTOTP.\n") - os.Exit(1) - } - username := os.Args[2] - - account, err := controller.GetAccount(global.DB, username) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to fetch account \"%s\": %v\n", username, err) - os.Exit(1) - } - - if account == nil { - fmt.Fprintf(os.Stderr, "Account \"%s\" does not exist.\n", username) - os.Exit(1) - } - - totps, err := controller.GetTOTPsForAccount(global.DB, account.ID) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to create TOTP methods: %v\n", err) - os.Exit(1) - } - - for i, totp := range totps { - fmt.Printf("%d. %s - Created %s\n", i + 1, totp.Name, totp.CreatedAt) - } - if len(totps) == 0 { - fmt.Printf("\"%s\" has no TOTP methods.\n", account.Username) - } - return - - case "testTOTP": - if len(os.Args) < 4 { - fmt.Fprintf(os.Stderr, "FATAL: `username` and `name` must be specified for testTOTP.\n") - os.Exit(1) - } - username := os.Args[2] - totpName := os.Args[3] - - account, err := controller.GetAccount(global.DB, username) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to fetch account \"%s\": %v\n", username, err) - os.Exit(1) - } - - if account == nil { - fmt.Fprintf(os.Stderr, "Account \"%s\" does not exist.\n", username) - os.Exit(1) - } - - totp, err := controller.GetTOTP(global.DB, account.ID, totpName) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to fetch TOTP method \"%s\": %v\n", totpName, err) - os.Exit(1) - } - - if totp == nil { - 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 "createInvite": fmt.Printf("Creating invite...\n") invite, err := controller.CreateInvite(global.DB, 16, time.Hour * 24) @@ -225,30 +98,9 @@ func main() { fmt.Printf("Invites deleted successfully.\n") return - case "listAccounts": - accounts, err := controller.GetAllAccounts(global.DB) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to fetch accounts: %v\n", err) - os.Exit(1) - } - - for _, account := range accounts { - fmt.Printf( - "User: %s\n" + - "\tID: %s\n" + - "\tEmail: %s\n" + - "\tCreated: %s\n", - account.Username, - account.ID, - account.Email, - account.CreatedAt, - ) - } - return - case "deleteAccount": - if len(os.Args) < 3 { - fmt.Fprintf(os.Stderr, "FATAL: `username` must be specified for deleteAccount\n") + if len(os.Args) < 2 { + fmt.Fprintf(os.Stderr, "FATAL: Account name not specified for -deleteAccount\n") os.Exit(1) } username := os.Args[2] @@ -256,7 +108,7 @@ func main() { account, err := controller.GetAccount(global.DB, username) if err != nil { - fmt.Fprintf(os.Stderr, "Failed to fetch account \"%s\": %v\n", username, err) + fmt.Fprintf(os.Stderr, "Failed to fetch account \"%s\": %s\n", username, err.Error()) os.Exit(1) } @@ -283,17 +135,10 @@ func main() { } - // command help - fmt.Print( + fmt.Printf( "Available commands:\n\n" + - "createTOTP :\n\tCreates a timed one-time passcode method.\n" + - "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" + - "\n" + "createInvite:\n\tCreates an invite code to register new accounts.\n" + "purgeInvites:\n\tDeletes all available invite codes.\n" + - "listAccounts:\n\tLists all active accounts.\n", "deleteAccount :\n\tDeletes an account with a given `username`.\n", ) return diff --git a/model/account.go b/model/account.go index 031cae9..03e95c5 100644 --- a/model/account.go +++ b/model/account.go @@ -1,16 +1,12 @@ package model -import "time" - type ( Account struct { - 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"` - + 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"` Privileges []AccountPrivilege `json:"privileges"` } diff --git a/model/totp.go b/model/totp.go deleted file mode 100644 index 8d8422f..0000000 --- a/model/totp.go +++ /dev/null @@ -1,12 +0,0 @@ -package model - -import ( - "time" -) - -type TOTP struct { - Name string `json:"name" db:"name"` - AccountID string `json:"accountID" db:"account"` - Secret string `json:"-" db:"secret"` - CreatedAt time.Time `json:"created_at" db:"created_at"` -} diff --git a/schema_migration/000-init.sql b/schema_migration/000-init.sql index 2c6e5b1..cd11a5e 100644 --- a/schema_migration/000-init.sql +++ b/schema_migration/000-init.sql @@ -16,8 +16,7 @@ CREATE TABLE arimelody.account ( username text NOT NULL UNIQUE, password text NOT NULL, email text, - avatar_url text, - created_at TIMESTAMP DEFAULT current_timestamp + avatar_url text ); ALTER TABLE arimelody.account ADD CONSTRAINT account_pk PRIMARY KEY (id); @@ -28,6 +27,14 @@ CREATE TABLE arimelody.privilege ( ); ALTER TABLE arimelody.privilege ADD CONSTRAINT privilege_pk PRIMARY KEY (account, privilege); +-- TOTP +CREATE TABLE arimelody.totp ( + account uuid NOT NULL, + name text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +ALTER TABLE arimelody.totp ADD CONSTRAINT totp_pk PRIMARY KEY (account, name); + -- Invites CREATE TABLE arimelody.invite ( code text NOT NULL, @@ -46,16 +53,6 @@ CREATE TABLE arimelody.token ( ); ALTER TABLE arimelody.token ADD CONSTRAINT token_pk PRIMARY KEY (token); --- TOTPs -CREATE TABLE arimelody.totp ( - name TEXT NOT NULL, - account UUID NOT NULL, - secret TEXT, - created_at TIMESTAMP NOT NULL DEFAULT current_timestamp -); -ALTER TABLE arimelody.totp ADD CONSTRAINT totp_pk PRIMARY KEY (account, name); - - -- Artists (should be applicable to all art) CREATE TABLE arimelody.artist ( @@ -124,8 +121,8 @@ 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.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.token ADD CONSTRAINT token_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; ALTER TABLE arimelody.musiccredit ADD CONSTRAINT musiccredit_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE; diff --git a/schema_migration/001-pre-versioning.sql b/schema_migration/001-pre-versioning.sql index 62bc15b..fc730a0 100644 --- a/schema_migration/001-pre-versioning.sql +++ b/schema_migration/001-pre-versioning.sql @@ -22,8 +22,7 @@ CREATE TABLE arimelody.account ( username text NOT NULL UNIQUE, password text NOT NULL, email text, - avatar_url text, - created_at TIMESTAMP DEFAULT current_timestamp + avatar_url text ); ALTER TABLE arimelody.account ADD CONSTRAINT account_pk PRIMARY KEY (id); @@ -34,6 +33,14 @@ CREATE TABLE arimelody.privilege ( ); ALTER TABLE arimelody.privilege ADD CONSTRAINT privilege_pk PRIMARY KEY (account, privilege); +-- TOTP +CREATE TABLE arimelody.totp ( + account uuid NOT NULL, + name text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +ALTER TABLE arimelody.totp ADD CONSTRAINT totp_pk PRIMARY KEY (account, name); + -- Invites CREATE TABLE arimelody.invite ( code text NOT NULL, @@ -52,16 +59,7 @@ CREATE TABLE arimelody.token ( ); ALTER TABLE arimelody.token ADD CONSTRAINT token_pk PRIMARY KEY (token); --- TOTPs -CREATE TABLE arimelody.totp ( - name TEXT NOT NULL, - account UUID NOT NULL, - secret TEXT, - 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.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.token ADD CONSTRAINT token_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE;