totp codes don't seem to sync but they're here!!
This commit is contained in:
parent
5e493554dc
commit
ae254dd731
63
admin/views/edit-account.html
Normal file
63
admin/views/edit-account.html
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
{{define "head"}}
|
||||||
|
<title>Account Settings - ari melody 💫</title>
|
||||||
|
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
|
||||||
|
<link rel="stylesheet" href="/admin/static/index.css">
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "content"}}
|
||||||
|
<main>
|
||||||
|
<h1>Account Settings ({{.Account.Username}})</h1>
|
||||||
|
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>Change Password</h2>
|
||||||
|
|
||||||
|
<form action="/api/v1/change-password" method="POST" id="change-password">
|
||||||
|
<div>
|
||||||
|
<label for="current-password">Current Password</label>
|
||||||
|
<input type="password" name="current-password" value="" autocomplete="current-password">
|
||||||
|
|
||||||
|
<label for="new-password">Password</label>
|
||||||
|
<input type="password" name="new-password" value="" autocomplete="new-password">
|
||||||
|
|
||||||
|
<label for="confirm-password">Confirm Password</label>
|
||||||
|
<input type="password" name="confirm-password" value="" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="save">Change Password</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>MFA Devices</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card mfa-devices">
|
||||||
|
{{if .TOTPs}}
|
||||||
|
{{range .TOTPs}}
|
||||||
|
<div class="mfa-device">
|
||||||
|
<h3 class="mfa-device-name">{{.Name}}</h3>
|
||||||
|
<p class="mfa-device-date">{{.CreatedAt}}</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<p>You have no MFA devices.</p>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<a class="create-btn" id="add-mfa-device">Add MFA Device</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-title">
|
||||||
|
<h2>Danger Zone</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card danger">
|
||||||
|
<p>
|
||||||
|
Clicking the button below will delete your account.
|
||||||
|
This action is <strong>irreversible</strong>.
|
||||||
|
You will be prompted to confirm this decision.
|
||||||
|
</p>
|
||||||
|
<button class="delete" id="delete">Delete Account</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script type="module" src="/admin/static/edit-account.js" defer></script>
|
||||||
|
{{end}}
|
|
@ -87,13 +87,13 @@ button:active {
|
||||||
<form action="/admin/login" method="POST" id="login">
|
<form action="/admin/login" method="POST" id="login">
|
||||||
<div>
|
<div>
|
||||||
<label for="username">Username</label>
|
<label for="username">Username</label>
|
||||||
<input type="text" name="username" value="">
|
<input type="text" name="username" value="" autocomplete="username">
|
||||||
|
|
||||||
<label for="password">Password</label>
|
<label for="password">Password</label>
|
||||||
<input type="password" name="password" value="">
|
<input type="password" name="password" value="" autocomplete="current-password">
|
||||||
|
|
||||||
<label for="totp">TOTP</label>
|
<label for="totp">TOTP</label>
|
||||||
<input type="text" name="totp" value="" disabled>
|
<input type="text" name="totp" value="" autocomplete="one-time-code">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="save">Login</button>
|
<button type="submit" class="save">Login</button>
|
||||||
|
|
108
controller/totp.go
Normal file
108
controller/totp.go
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"arimelody-web/model"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"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 {
|
||||||
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
|
return nil, 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
|
||||||
|
}
|
144
main.go
144
main.go
|
@ -14,6 +14,7 @@ import (
|
||||||
"arimelody-web/api"
|
"arimelody-web/api"
|
||||||
"arimelody-web/controller"
|
"arimelody-web/controller"
|
||||||
"arimelody-web/global"
|
"arimelody-web/global"
|
||||||
|
"arimelody-web/model"
|
||||||
"arimelody-web/templates"
|
"arimelody-web/templates"
|
||||||
"arimelody-web/view"
|
"arimelody-web/view"
|
||||||
|
|
||||||
|
@ -30,10 +31,6 @@ func main() {
|
||||||
fmt.Printf("made with <3 by ari melody\n\n")
|
fmt.Printf("made with <3 by ari melody\n\n")
|
||||||
|
|
||||||
// initialise database connection
|
// 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 == "" {
|
if global.Config.DB.Host == "" {
|
||||||
fmt.Fprintf(os.Stderr, "FATAL: db.host not provided! Exiting...\n")
|
fmt.Fprintf(os.Stderr, "FATAL: db.host not provided! Exiting...\n")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
@ -76,6 +73,136 @@ func main() {
|
||||||
arg := os.Args[1]
|
arg := os.Args[1]
|
||||||
|
|
||||||
switch arg {
|
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":
|
case "createInvite":
|
||||||
fmt.Printf("Creating invite...\n")
|
fmt.Printf("Creating invite...\n")
|
||||||
invite, err := controller.CreateInvite(global.DB, 16, time.Hour * 24)
|
invite, err := controller.CreateInvite(global.DB, 16, time.Hour * 24)
|
||||||
|
@ -120,8 +247,8 @@ func main() {
|
||||||
return
|
return
|
||||||
|
|
||||||
case "deleteAccount":
|
case "deleteAccount":
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 3 {
|
||||||
fmt.Fprintf(os.Stderr, "FATAL: Account name not specified for -deleteAccount\n")
|
fmt.Fprintf(os.Stderr, "FATAL: `username` must be specified for deleteAccount\n")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
username := os.Args[2]
|
username := os.Args[2]
|
||||||
|
@ -159,6 +286,11 @@ func main() {
|
||||||
// command help
|
// command help
|
||||||
fmt.Print(
|
fmt.Print(
|
||||||
"Available commands:\n\n" +
|
"Available commands:\n\n" +
|
||||||
|
"createTOTP <username> <name>:\n\tCreates a timed one-time passcode method.\n" +
|
||||||
|
"listTOTP <username>:\n\tLists an account's TOTP methods.\n" +
|
||||||
|
"deleteTOTP <username> <name>:\n\tDeletes an account's TOTP method.\n" +
|
||||||
|
"testTOTP <username> <name>:\n\tGenerates the code for an account's TOTP method.\n" +
|
||||||
|
"\n" +
|
||||||
"createInvite:\n\tCreates an invite code to register new accounts.\n" +
|
"createInvite:\n\tCreates an invite code to register new accounts.\n" +
|
||||||
"purgeInvites:\n\tDeletes all available invite codes.\n" +
|
"purgeInvites:\n\tDeletes all available invite codes.\n" +
|
||||||
"listAccounts:\n\tLists all active accounts.\n",
|
"listAccounts:\n\tLists all active accounts.\n",
|
||||||
|
|
12
model/totp.go
Normal file
12
model/totp.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
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"`
|
||||||
|
}
|
|
@ -28,14 +28,6 @@ CREATE TABLE arimelody.privilege (
|
||||||
);
|
);
|
||||||
ALTER TABLE arimelody.privilege ADD CONSTRAINT privilege_pk PRIMARY KEY (account, 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
|
-- Invites
|
||||||
CREATE TABLE arimelody.invite (
|
CREATE TABLE arimelody.invite (
|
||||||
code text NOT NULL,
|
code text NOT NULL,
|
||||||
|
@ -54,6 +46,16 @@ CREATE TABLE arimelody.token (
|
||||||
);
|
);
|
||||||
ALTER TABLE arimelody.token ADD CONSTRAINT token_pk PRIMARY KEY (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)
|
-- Artists (should be applicable to all art)
|
||||||
CREATE TABLE arimelody.artist (
|
CREATE TABLE arimelody.artist (
|
||||||
|
@ -122,8 +124,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.privilege ADD CONSTRAINT privilege_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.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;
|
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;
|
ALTER TABLE arimelody.musiccredit ADD CONSTRAINT musiccredit_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE;
|
||||||
|
|
|
@ -34,14 +34,6 @@ CREATE TABLE arimelody.privilege (
|
||||||
);
|
);
|
||||||
ALTER TABLE arimelody.privilege ADD CONSTRAINT privilege_pk PRIMARY KEY (account, 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
|
-- Invites
|
||||||
CREATE TABLE arimelody.invite (
|
CREATE TABLE arimelody.invite (
|
||||||
code text NOT NULL,
|
code text NOT NULL,
|
||||||
|
@ -60,7 +52,16 @@ CREATE TABLE arimelody.token (
|
||||||
);
|
);
|
||||||
ALTER TABLE arimelody.token ADD CONSTRAINT token_pk PRIMARY KEY (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
|
-- Foreign keys
|
||||||
ALTER TABLE arimelody.privilege ADD CONSTRAINT privilege_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE;
|
ALTER TABLE arimelody.privilege ADD CONSTRAINT privilege_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.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;
|
||||||
|
|
Loading…
Reference in a new issue