home to your local SPACEGIRL 💫 arimelody.space
1
fork

Configure Feed

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

polished up TOTP enrolment

+81 -31
+33 -24
admin/accounthttp.go
··· 1 1 package admin 2 2 3 3 import ( 4 + "database/sql" 4 5 "fmt" 5 6 "net/http" 6 7 "net/url" ··· 190 191 }) 191 192 } 192 193 194 + type totpConfirmData struct { 195 + Session *model.Session 196 + TOTP *model.TOTP 197 + NameEscaped string 198 + QRBase64Image string 199 + } 200 + 193 201 func totpSetupHandler(app *model.AppState) http.Handler { 194 202 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 195 203 if r.Method == http.MethodGet { ··· 210 218 if r.Method != http.MethodPost { 211 219 http.NotFound(w, r) 212 220 return 213 - } 214 - 215 - type totpSetupData struct { 216 - Session *model.Session 217 - TOTP *model.TOTP 218 - NameEscaped string 219 - QRBase64Image string 220 221 } 221 222 222 223 err := r.ParseForm() ··· 243 244 if err != nil { 244 245 fmt.Printf("WARN: Failed to create TOTP method: %s\n", err) 245 246 controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") 246 - err := totpSetupTemplate.Execute(w, totpSetupData{ Session: session }) 247 + err := totpSetupTemplate.Execute(w, totpConfirmData{ Session: session }) 247 248 if err != nil { 248 249 fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err) 249 250 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) ··· 254 255 qrBase64Image, err := controller.GenerateQRCode( 255 256 controller.GenerateTOTPURI(session.Account.Username, totp.Secret)) 256 257 if err != nil { 257 - fmt.Printf("WARN: Failed to generate TOTP setup QR code: %s\n", err) 258 - controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") 259 - err := totpSetupTemplate.Execute(w, totpSetupData{ Session: session }) 260 - if err != nil { 261 - fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err) 262 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 263 - } 264 - return 258 + fmt.Fprintf(os.Stderr, "WARN: Failed to generate TOTP QR code: %v\n", err) 265 259 } 266 260 267 - err = totpConfirmTemplate.Execute(w, totpSetupData{ 261 + err = totpConfirmTemplate.Execute(w, totpConfirmData{ 268 262 Session: session, 269 263 TOTP: &totp, 270 264 NameEscaped: url.PathEscape(totp.Name), ··· 284 278 return 285 279 } 286 280 287 - type totpConfirmData struct { 288 - Session *model.Session 289 - TOTP *model.TOTP 290 - } 291 - 292 281 session := r.Context().Value("session").(*model.Session) 293 282 294 283 err := r.ParseForm() ··· 309 298 310 299 totp, err := controller.GetTOTP(app.DB, session.Account.ID, name) 311 300 if err != nil { 312 - fmt.Printf("WARN: Failed to fetch TOTP method: %s\n", err) 301 + fmt.Printf("WARN: Failed to fetch TOTP method: %v\n", err) 313 302 controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") 314 303 http.Redirect(w, r, "/admin/account", http.StatusFound) 315 304 return ··· 319 308 return 320 309 } 321 310 311 + qrBase64Image, err := controller.GenerateQRCode( 312 + controller.GenerateTOTPURI(session.Account.Username, totp.Secret)) 313 + if err != nil { 314 + fmt.Fprintf(os.Stderr, "WARN: Failed to generate TOTP QR code: %v\n", err) 315 + } 316 + 322 317 confirmCode := controller.GenerateTOTP(totp.Secret, 0) 323 318 if code != confirmCode { 324 319 confirmCodeOffset := controller.GenerateTOTP(totp.Secret, 1) 325 320 if code != confirmCodeOffset { 326 - controller.SetSessionError(app.DB, session, "Incorrect TOTP code. Please try again.") 321 + session.Error = sql.NullString{ Valid: true, String: "Incorrect TOTP code. Please try again." } 327 322 err = totpConfirmTemplate.Execute(w, totpConfirmData{ 328 323 Session: session, 329 324 TOTP: totp, 325 + NameEscaped: url.PathEscape(totp.Name), 326 + QRBase64Image: qrBase64Image, 330 327 }) 328 + if err != nil { 329 + fmt.Fprintf(os.Stderr, "WARN: Failed to render TOTP setup page: %v\n", err) 330 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 331 + } 331 332 return 332 333 } 334 + } 335 + 336 + err = controller.ConfirmTOTP(app.DB, session.Account.ID, name) 337 + if err != nil { 338 + fmt.Printf("WARN: Failed to confirm TOTP method: %s\n", err) 339 + controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") 340 + http.Redirect(w, r, "/admin/account", http.StatusFound) 341 + return 333 342 } 334 343 335 344 controller.SetSessionError(app.DB, session, "")
+7
admin/views/totp-confirm.html
··· 19 19 {{end}} 20 20 21 21 <form action="/admin/account/totp-confirm?totp-name={{.NameEscaped}}" method="POST" id="totp-setup"> 22 + {{if .QRBase64Image}} 22 23 <img src="data:image/png;base64,{{.QRBase64Image}}" alt="" class="qr-code"> 23 24 24 25 <p> ··· 29 30 <p> 30 31 If the QR code does not work, you may also enter this secret code: 31 32 </p> 33 + {{else}} 34 + <p> 35 + Paste the below secret code into your authentication app or password manager, 36 + then enter your 2FA code below: 37 + </p> 38 + {{end}} 32 39 33 40 <p><code>{{.TOTP.Secret}}</code></p> 34 41
+15 -1
controller/totp.go
··· 78 78 err := db.Select( 79 79 &totps, 80 80 "SELECT * FROM totp " + 81 - "WHERE account=$1 " + 81 + "WHERE account=$1 AND confirmed=true " + 82 82 "ORDER BY created_at ASC", 83 83 accountID, 84 84 ) ··· 130 130 return &totp, nil 131 131 } 132 132 133 + func ConfirmTOTP(db *sqlx.DB, accountID string, name string) error { 134 + _, err := db.Exec( 135 + "UPDATE totp SET confirmed=true WHERE account=$1 AND name=$2", 136 + accountID, 137 + name, 138 + ) 139 + return err 140 + } 141 + 133 142 func CreateTOTP(db *sqlx.DB, totp *model.TOTP) error { 134 143 _, err := db.Exec( 135 144 "INSERT INTO totp (account, name, secret) " + ··· 149 158 ) 150 159 return err 151 160 } 161 + 162 + func DeleteUnconfirmedTOTPs(db *sqlx.DB) error { 163 + _, err := db.Exec("DELETE FROM totp WHERE confirmed=false") 164 + return err 165 + }
+17
main.go
··· 215 215 code := controller.GenerateTOTP(totp.Secret, 0) 216 216 fmt.Printf("%s\n", code) 217 217 return 218 + 219 + case "cleanTOTP": 220 + err := controller.DeleteUnconfirmedTOTPs(app.DB) 221 + if err != nil { 222 + fmt.Fprintf(os.Stderr, "FATAL: Failed to clean up TOTP methods: %v\n", err) 223 + os.Exit(1) 224 + } 225 + fmt.Printf("Cleaned up dangling TOTP methods successfully.\n") 226 + return 218 227 219 228 case "createInvite": 220 229 fmt.Printf("Creating invite...\n") ··· 342 351 "listTOTP <username>:\n\tLists an account's TOTP methods.\n" + 343 352 "deleteTOTP <username> <name>:\n\tDeletes an account's TOTP method.\n" + 344 353 "testTOTP <username> <name>:\n\tGenerates the code for an account's TOTP method.\n" + 354 + "cleanTOTP:\n\tCleans up unconfirmed (dangling) TOTP methods.\n" + 345 355 "\n" + 346 356 "createInvite:\n\tCreates an invite code to register new accounts.\n" + 347 357 "purgeInvites:\n\tDeletes all available invite codes.\n" + ··· 378 388 err = controller.DeleteExpiredInvites(app.DB) 379 389 if err != nil { 380 390 fmt.Fprintf(os.Stderr, "FATAL: Failed to clear expired invite codes: %v\n", err) 391 + os.Exit(1) 392 + } 393 + 394 + // clean up unconfirmed TOTP methods 395 + err = controller.DeleteUnconfirmedTOTPs(app.DB) 396 + if err != nil { 397 + fmt.Fprintf(os.Stderr, "FATAL: Failed to clean up unconfirmed TOTP methods: %v\n", err) 381 398 os.Exit(1) 382 399 } 383 400
+1
model/totp.go
··· 9 9 AccountID string `json:"accountID" db:"account"` 10 10 Secret string `json:"-" db:"secret"` 11 11 CreatedAt time.Time `json:"created_at" db:"created_at"` 12 + Confirmed bool `json:"-" db:"confirmed"` 12 13 }
+4 -3
schema_migration/000-init.sql
··· 23 23 -- Invites 24 24 CREATE TABLE arimelody.invite ( 25 25 code text NOT NULL, 26 - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 26 + created_at TIMESTAMP NOT NULL DEFAULT current_timestamp, 27 27 expires_at TIMESTAMP NOT NULL 28 28 ); 29 29 ALTER TABLE arimelody.invite ADD CONSTRAINT invite_pk PRIMARY KEY (code); 30 30 31 - -- Session 31 + -- Sessions 32 32 CREATE TABLE arimelody.session ( 33 33 token TEXT, 34 34 user_agent TEXT NOT NULL, ··· 40 40 ); 41 41 ALTER TABLE arimelody.session ADD CONSTRAINT session_pk PRIMARY KEY (token); 42 42 43 - -- TOTPs 43 + -- TOTP methods 44 44 CREATE TABLE arimelody.totp ( 45 45 name TEXT NOT NULL, 46 46 account UUID NOT NULL, 47 47 secret TEXT, 48 48 created_at TIMESTAMP NOT NULL DEFAULT current_timestamp 49 + confirmed BOOLEAN DEFAULT false, 49 50 ); 50 51 ALTER TABLE arimelody.totp ADD CONSTRAINT totp_pk PRIMARY KEY (account, name); 51 52
+4 -3
schema_migration/001-pre-versioning.sql
··· 23 23 -- Invites 24 24 CREATE TABLE arimelody.invite ( 25 25 code text NOT NULL, 26 - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 26 + created_at TIMESTAMP NOT NULL DEFAULT current_timestamp, 27 27 expires_at TIMESTAMP NOT NULL 28 28 ); 29 29 ALTER TABLE arimelody.invite ADD CONSTRAINT invite_pk PRIMARY KEY (code); 30 30 31 - -- Session 31 + -- Sessions 32 32 CREATE TABLE arimelody.session ( 33 33 token TEXT, 34 34 user_agent TEXT NOT NULL, ··· 40 40 ); 41 41 ALTER TABLE arimelody.session ADD CONSTRAINT session_pk PRIMARY KEY (token); 42 42 43 - -- TOTPs 43 + -- TOTP methods 44 44 CREATE TABLE arimelody.totp ( 45 45 name TEXT NOT NULL, 46 46 account UUID NOT NULL, 47 47 secret TEXT, 48 48 created_at TIMESTAMP NOT NULL DEFAULT current_timestamp 49 + confirmed BOOLEAN DEFAULT false, 49 50 ); 50 51 ALTER TABLE arimelody.totp ADD CONSTRAINT totp_pk PRIMARY KEY (account, name); 51 52