home to your local SPACEGIRL 💫 arimelody.space
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

totp codes don't seem to sync but they're here!!

+345 -27
+63
admin/views/edit-account.html
··· 1 + {{define "head"}} 2 + <title>Account Settings - ari melody 💫</title> 3 + <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> 4 + <link rel="stylesheet" href="/admin/static/index.css"> 5 + {{end}} 6 + 7 + {{define "content"}} 8 + <main> 9 + <h1>Account Settings ({{.Account.Username}})</h1> 10 + 11 + <div class="card-title"> 12 + <h2>Change Password</h2> 13 + 14 + <form action="/api/v1/change-password" method="POST" id="change-password"> 15 + <div> 16 + <label for="current-password">Current Password</label> 17 + <input type="password" name="current-password" value="" autocomplete="current-password"> 18 + 19 + <label for="new-password">Password</label> 20 + <input type="password" name="new-password" value="" autocomplete="new-password"> 21 + 22 + <label for="confirm-password">Confirm Password</label> 23 + <input type="password" name="confirm-password" value="" autocomplete="new-password"> 24 + </div> 25 + 26 + <button type="submit" class="save">Change Password</button> 27 + </form> 28 + </div> 29 + 30 + <div class="card-title"> 31 + <h2>MFA Devices</h2> 32 + </div> 33 + <div class="card mfa-devices"> 34 + {{if .TOTPs}} 35 + {{range .TOTPs}} 36 + <div class="mfa-device"> 37 + <h3 class="mfa-device-name">{{.Name}}</h3> 38 + <p class="mfa-device-date">{{.CreatedAt}}</p> 39 + </div> 40 + {{end}} 41 + {{else}} 42 + <p>You have no MFA devices.</p> 43 + {{end}} 44 + 45 + <a class="create-btn" id="add-mfa-device">Add MFA Device</a> 46 + </div> 47 + 48 + <div class="card-title"> 49 + <h2>Danger Zone</h2> 50 + </div> 51 + <div class="card danger"> 52 + <p> 53 + Clicking the button below will delete your account. 54 + This action is <strong>irreversible</strong>. 55 + You will be prompted to confirm this decision. 56 + </p> 57 + <button class="delete" id="delete">Delete Account</button> 58 + </div> 59 + 60 + </main> 61 + 62 + <script type="module" src="/admin/static/edit-account.js" defer></script> 63 + {{end}}
+3 -3
admin/views/login.html
··· 87 87 <form action="/admin/login" method="POST" id="login"> 88 88 <div> 89 89 <label for="username">Username</label> 90 - <input type="text" name="username" value=""> 90 + <input type="text" name="username" value="" autocomplete="username"> 91 91 92 92 <label for="password">Password</label> 93 - <input type="password" name="password" value=""> 93 + <input type="password" name="password" value="" autocomplete="current-password"> 94 94 95 95 <label for="totp">TOTP</label> 96 - <input type="text" name="totp" value="" disabled> 96 + <input type="text" name="totp" value="" autocomplete="one-time-code"> 97 97 </div> 98 98 99 99 <button type="submit" class="save">Login</button>
+108
controller/totp.go
··· 1 + package controller 2 + 3 + import ( 4 + "arimelody-web/model" 5 + "crypto/hmac" 6 + "crypto/sha1" 7 + "encoding/binary" 8 + "fmt" 9 + "math" 10 + "net/url" 11 + "strings" 12 + "time" 13 + 14 + "github.com/jmoiron/sqlx" 15 + ) 16 + 17 + const TIME_STEP int64 = 30 18 + const CODE_LENGTH = 6 19 + 20 + func GenerateTOTP(secret string, timeStepOffset int) string { 21 + counter := time.Now().Unix() / TIME_STEP - int64(timeStepOffset) 22 + counterBytes := make([]byte, 8) 23 + binary.BigEndian.PutUint64(counterBytes, uint64(counter)) 24 + 25 + mac := hmac.New(sha1.New, []byte(secret)) 26 + mac.Write(counterBytes) 27 + hash := mac.Sum(nil) 28 + 29 + offset := hash[len(hash) - 1] & 0x0f 30 + binaryCode := int32(binary.BigEndian.Uint32(hash[offset : offset + 4]) & 0x7FFFFFFF) 31 + code := binaryCode % int32(math.Pow10(CODE_LENGTH)) 32 + 33 + return fmt.Sprintf(fmt.Sprintf("%%0%dd", CODE_LENGTH), code) 34 + } 35 + 36 + func GenerateTOTPURI(username string, secret string) string { 37 + url := url.URL{ 38 + Scheme: "otpauth", 39 + Host: "totp", 40 + Path: url.QueryEscape("arimelody.me") + ":" + url.QueryEscape(username), 41 + } 42 + 43 + query := url.Query() 44 + query.Set("secret", secret) 45 + query.Set("issuer", "arimelody.me") 46 + query.Set("algorithm", "SHA1") 47 + query.Set("digits", fmt.Sprintf("%d", CODE_LENGTH)) 48 + query.Set("period", fmt.Sprintf("%d", TIME_STEP)) 49 + url.RawQuery = query.Encode() 50 + 51 + return url.String() 52 + } 53 + 54 + func GetTOTPsForAccount(db *sqlx.DB, accountID string) ([]model.TOTP, error) { 55 + totps := []model.TOTP{} 56 + 57 + err := db.Select( 58 + &totps, 59 + "SELECT * FROM totp " + 60 + "WHERE account=$1 " + 61 + "ORDER BY created_at ASC", 62 + accountID, 63 + ) 64 + if err != nil { 65 + return nil, err 66 + } 67 + 68 + return totps, nil 69 + } 70 + 71 + func GetTOTP(db *sqlx.DB, accountID string, name string) (*model.TOTP, error) { 72 + totp := model.TOTP{} 73 + 74 + err := db.Get( 75 + &totp, 76 + "SELECT * FROM totp " + 77 + "WHERE account=$1", 78 + accountID, 79 + ) 80 + if err != nil { 81 + if strings.Contains(err.Error(), "no rows") { 82 + return nil, nil 83 + } 84 + return nil, err 85 + } 86 + 87 + return &totp, nil 88 + } 89 + 90 + func CreateTOTP(db *sqlx.DB, totp *model.TOTP) error { 91 + _, err := db.Exec( 92 + "INSERT INTO totp (account, name, secret) " + 93 + "VALUES ($1,$2,$3)", 94 + totp.AccountID, 95 + totp.Name, 96 + totp.Secret, 97 + ) 98 + return err 99 + } 100 + 101 + func DeleteTOTP(db *sqlx.DB, accountID string, name string) error { 102 + _, err := db.Exec( 103 + "DELETE FROM totp WHERE account=$1 AND name=$2", 104 + accountID, 105 + name, 106 + ) 107 + return err 108 + }
+138 -6
main.go
··· 14 14 "arimelody-web/api" 15 15 "arimelody-web/controller" 16 16 "arimelody-web/global" 17 + "arimelody-web/model" 17 18 "arimelody-web/templates" 18 19 "arimelody-web/view" 19 20 ··· 30 31 fmt.Printf("made with <3 by ari melody\n\n") 31 32 32 33 // initialise database connection 33 - if env := os.Getenv("ARIMELODY_DB_HOST"); env != "" { global.Config.DB.Host = env } 34 - if env := os.Getenv("ARIMELODY_DB_NAME"); env != "" { global.Config.DB.Name = env } 35 - if env := os.Getenv("ARIMELODY_DB_USER"); env != "" { global.Config.DB.User = env } 36 - if env := os.Getenv("ARIMELODY_DB_PASS"); env != "" { global.Config.DB.Pass = env } 37 34 if global.Config.DB.Host == "" { 38 35 fmt.Fprintf(os.Stderr, "FATAL: db.host not provided! Exiting...\n") 39 36 os.Exit(1) ··· 76 73 arg := os.Args[1] 77 74 78 75 switch arg { 76 + case "createTOTP": 77 + if len(os.Args) < 4 { 78 + fmt.Fprintf(os.Stderr, "FATAL: `username` and `name` must be specified for createTOTP.\n") 79 + os.Exit(1) 80 + } 81 + username := os.Args[2] 82 + totpName := os.Args[3] 83 + secret := controller.GenerateAlnumString(32) 84 + 85 + account, err := controller.GetAccount(global.DB, username) 86 + if err != nil { 87 + fmt.Fprintf(os.Stderr, "Failed to fetch account \"%s\": %v\n", username, err) 88 + os.Exit(1) 89 + } 90 + 91 + if account == nil { 92 + fmt.Fprintf(os.Stderr, "Account \"%s\" does not exist.\n", username) 93 + os.Exit(1) 94 + } 95 + 96 + totp := model.TOTP { 97 + AccountID: account.ID, 98 + Name: totpName, 99 + Secret: string(secret), 100 + } 101 + 102 + err = controller.CreateTOTP(global.DB, &totp) 103 + if err != nil { 104 + fmt.Fprintf(os.Stderr, "Failed to create TOTP method: %v\n", err) 105 + os.Exit(1) 106 + } 107 + 108 + url := controller.GenerateTOTPURI(account.Username, totp.Secret) 109 + fmt.Printf("%s\n", url) 110 + return 111 + 112 + case "deleteTOTP": 113 + if len(os.Args) < 4 { 114 + fmt.Fprintf(os.Stderr, "FATAL: `username` and `name` must be specified for deleteTOTP.\n") 115 + os.Exit(1) 116 + } 117 + username := os.Args[2] 118 + totpName := os.Args[3] 119 + 120 + account, err := controller.GetAccount(global.DB, username) 121 + if err != nil { 122 + fmt.Fprintf(os.Stderr, "Failed to fetch account \"%s\": %v\n", username, err) 123 + os.Exit(1) 124 + } 125 + 126 + if account == nil { 127 + fmt.Fprintf(os.Stderr, "Account \"%s\" does not exist.\n", username) 128 + os.Exit(1) 129 + } 130 + 131 + err = controller.DeleteTOTP(global.DB, account.ID, totpName) 132 + if err != nil { 133 + fmt.Fprintf(os.Stderr, "Failed to create TOTP method: %v\n", err) 134 + os.Exit(1) 135 + } 136 + 137 + fmt.Printf("TOTP method \"%s\" deleted.\n", totpName) 138 + return 139 + 140 + case "listTOTP": 141 + if len(os.Args) < 3 { 142 + fmt.Fprintf(os.Stderr, "FATAL: `username` must be specified for listTOTP.\n") 143 + os.Exit(1) 144 + } 145 + username := os.Args[2] 146 + 147 + account, err := controller.GetAccount(global.DB, username) 148 + if err != nil { 149 + fmt.Fprintf(os.Stderr, "Failed to fetch account \"%s\": %v\n", username, err) 150 + os.Exit(1) 151 + } 152 + 153 + if account == nil { 154 + fmt.Fprintf(os.Stderr, "Account \"%s\" does not exist.\n", username) 155 + os.Exit(1) 156 + } 157 + 158 + totps, err := controller.GetTOTPsForAccount(global.DB, account.ID) 159 + if err != nil { 160 + fmt.Fprintf(os.Stderr, "Failed to create TOTP methods: %v\n", err) 161 + os.Exit(1) 162 + } 163 + 164 + for i, totp := range totps { 165 + fmt.Printf("%d. %s - Created %s\n", i + 1, totp.Name, totp.CreatedAt) 166 + } 167 + if len(totps) == 0 { 168 + fmt.Printf("\"%s\" has no TOTP methods.\n", account.Username) 169 + } 170 + return 171 + 172 + case "testTOTP": 173 + if len(os.Args) < 4 { 174 + fmt.Fprintf(os.Stderr, "FATAL: `username` and `name` must be specified for testTOTP.\n") 175 + os.Exit(1) 176 + } 177 + username := os.Args[2] 178 + totpName := os.Args[3] 179 + 180 + account, err := controller.GetAccount(global.DB, username) 181 + if err != nil { 182 + fmt.Fprintf(os.Stderr, "Failed to fetch account \"%s\": %v\n", username, err) 183 + os.Exit(1) 184 + } 185 + 186 + if account == nil { 187 + fmt.Fprintf(os.Stderr, "Account \"%s\" does not exist.\n", username) 188 + os.Exit(1) 189 + } 190 + 191 + totp, err := controller.GetTOTP(global.DB, account.ID, totpName) 192 + if err != nil { 193 + fmt.Fprintf(os.Stderr, "Failed to fetch TOTP method \"%s\": %v\n", totpName, err) 194 + os.Exit(1) 195 + } 196 + 197 + if totp == nil { 198 + fmt.Fprintf(os.Stderr, "TOTP method \"%s\" does not exist for account \"%s\"\n", totpName, username) 199 + os.Exit(1) 200 + } 201 + 202 + code := controller.GenerateTOTP(totp.Secret, 0) 203 + fmt.Printf("%s\n", code) 204 + return 205 + 79 206 case "createInvite": 80 207 fmt.Printf("Creating invite...\n") 81 208 invite, err := controller.CreateInvite(global.DB, 16, time.Hour * 24) ··· 120 247 return 121 248 122 249 case "deleteAccount": 123 - if len(os.Args) < 2 { 124 - fmt.Fprintf(os.Stderr, "FATAL: Account name not specified for -deleteAccount\n") 250 + if len(os.Args) < 3 { 251 + fmt.Fprintf(os.Stderr, "FATAL: `username` must be specified for deleteAccount\n") 125 252 os.Exit(1) 126 253 } 127 254 username := os.Args[2] ··· 159 286 // command help 160 287 fmt.Print( 161 288 "Available commands:\n\n" + 289 + "createTOTP <username> <name>:\n\tCreates a timed one-time passcode method.\n" + 290 + "listTOTP <username>:\n\tLists an account's TOTP methods.\n" + 291 + "deleteTOTP <username> <name>:\n\tDeletes an account's TOTP method.\n" + 292 + "testTOTP <username> <name>:\n\tGenerates the code for an account's TOTP method.\n" + 293 + "\n" + 162 294 "createInvite:\n\tCreates an invite code to register new accounts.\n" + 163 295 "purgeInvites:\n\tDeletes all available invite codes.\n" + 164 296 "listAccounts:\n\tLists all active accounts.\n",
+12
model/totp.go
··· 1 + package model 2 + 3 + import ( 4 + "time" 5 + ) 6 + 7 + type TOTP struct { 8 + Name string `json:"name" db:"name"` 9 + AccountID string `json:"accountID" db:"account"` 10 + Secret string `json:"-" db:"secret"` 11 + CreatedAt time.Time `json:"created_at" db:"created_at"` 12 + }
+11 -9
schema_migration/000-init.sql
··· 28 28 ); 29 29 ALTER TABLE arimelody.privilege ADD CONSTRAINT privilege_pk PRIMARY KEY (account, privilege); 30 30 31 - -- TOTP 32 - CREATE TABLE arimelody.totp ( 33 - account uuid NOT NULL, 34 - name text NOT NULL, 35 - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 36 - ); 37 - ALTER TABLE arimelody.totp ADD CONSTRAINT totp_pk PRIMARY KEY (account, name); 38 - 39 31 -- Invites 40 32 CREATE TABLE arimelody.invite ( 41 33 code text NOT NULL, ··· 53 45 expires_at TIMESTAMP DEFAULT NULL 54 46 ); 55 47 ALTER TABLE arimelody.token ADD CONSTRAINT token_pk PRIMARY KEY (token); 48 + 49 + -- TOTPs 50 + CREATE TABLE arimelody.totp ( 51 + name TEXT NOT NULL, 52 + account UUID NOT NULL, 53 + secret TEXT, 54 + created_at TIMESTAMP NOT NULL DEFAULT current_timestamp 55 + ); 56 + ALTER TABLE arimelody.totp ADD CONSTRAINT totp_pk PRIMARY KEY (account, name); 57 + 56 58 57 59 58 60 -- Artists (should be applicable to all art) ··· 122 124 -- 123 125 124 126 ALTER TABLE arimelody.privilege ADD CONSTRAINT privilege_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; 125 - ALTER TABLE arimelody.totp ADD CONSTRAINT totp_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; 126 127 ALTER TABLE arimelody.token ADD CONSTRAINT token_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; 128 + ALTER TABLE arimelody.totp ADD CONSTRAINT totp_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; 127 129 128 130 ALTER TABLE arimelody.musiccredit ADD CONSTRAINT musiccredit_artist_fk FOREIGN KEY (artist) REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE; 129 131 ALTER TABLE arimelody.musiccredit ADD CONSTRAINT musiccredit_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE;
+10 -9
schema_migration/001-pre-versioning.sql
··· 34 34 ); 35 35 ALTER TABLE arimelody.privilege ADD CONSTRAINT privilege_pk PRIMARY KEY (account, privilege); 36 36 37 - -- TOTP 38 - CREATE TABLE arimelody.totp ( 39 - account uuid NOT NULL, 40 - name text NOT NULL, 41 - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 42 - ); 43 - ALTER TABLE arimelody.totp ADD CONSTRAINT totp_pk PRIMARY KEY (account, name); 44 - 45 37 -- Invites 46 38 CREATE TABLE arimelody.invite ( 47 39 code text NOT NULL, ··· 60 52 ); 61 53 ALTER TABLE arimelody.token ADD CONSTRAINT token_pk PRIMARY KEY (token); 62 54 55 + -- TOTPs 56 + CREATE TABLE arimelody.totp ( 57 + name TEXT NOT NULL, 58 + account UUID NOT NULL, 59 + secret TEXT, 60 + created_at TIMESTAMP NOT NULL DEFAULT current_timestamp 61 + ); 62 + ALTER TABLE arimelody.totp ADD CONSTRAINT totp_pk PRIMARY KEY (account, name); 63 + 63 64 -- Foreign keys 64 65 ALTER TABLE arimelody.privilege ADD CONSTRAINT privilege_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; 65 - ALTER TABLE arimelody.totp ADD CONSTRAINT totp_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; 66 66 ALTER TABLE arimelody.token ADD CONSTRAINT token_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE; 67 + ALTER TABLE arimelody.totp ADD CONSTRAINT totp_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE;