admin account/TOTP fixes

This commit is contained in:
ari melody 2025-10-21 21:00:54 +01:00
parent e5689ce950
commit ad50b9e4fa
Signed by: ari
GPG key ID: CF99829C92678188
14 changed files with 67 additions and 43 deletions

View file

@ -20,7 +20,7 @@ func accountHandler(app *model.AppState) http.Handler {
mux.Handle("/account/totp-setup", totpSetupHandler(app)) mux.Handle("/account/totp-setup", totpSetupHandler(app))
mux.Handle("/account/totp-confirm", totpConfirmHandler(app)) mux.Handle("/account/totp-confirm", totpConfirmHandler(app))
mux.Handle("/account/totp-delete/", http.StripPrefix("/totp-delete", totpDeleteHandler(app))) mux.Handle("/account/totp-delete", totpDeleteHandler(app))
mux.Handle("/account/password", changePasswordHandler(app)) mux.Handle("/account/password", changePasswordHandler(app))
mux.Handle("/account/delete", deleteAccountHandler(app)) mux.Handle("/account/delete", deleteAccountHandler(app))
@ -266,11 +266,6 @@ func totpConfirmHandler(app *model.AppState) http.Handler {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return return
} }
code := r.FormValue("totp")
if len(code) != controller.TOTP_CODE_LENGTH {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
totp, err := controller.GetTOTP(app.DB, session.Account.ID, name) totp, err := controller.GetTOTP(app.DB, session.Account.ID, name)
if err != nil { if err != nil {
@ -290,23 +285,22 @@ func totpConfirmHandler(app *model.AppState) http.Handler {
fmt.Fprintf(os.Stderr, "WARN: Failed to generate TOTP QR code: %v\n", err) fmt.Fprintf(os.Stderr, "WARN: Failed to generate TOTP QR code: %v\n", err)
} }
code := r.FormValue("totp")
confirmCode := controller.GenerateTOTP(totp.Secret, 0) confirmCode := controller.GenerateTOTP(totp.Secret, 0)
if code != confirmCode { confirmCodeOffset := controller.GenerateTOTP(totp.Secret, 1)
confirmCodeOffset := controller.GenerateTOTP(totp.Secret, 1) if len(code) != controller.TOTP_CODE_LENGTH || (code != confirmCode && code != confirmCodeOffset) {
if code != confirmCodeOffset { session.Error = sql.NullString{ Valid: true, String: "Incorrect TOTP code. Please try again." }
session.Error = sql.NullString{ Valid: true, String: "Incorrect TOTP code. Please try again." } err = templates.TOTPConfirmTemplate.Execute(w, totpConfirmData{
err = templates.TOTPConfirmTemplate.Execute(w, totpConfirmData{ adminPageData: adminPageData{ Path: r.URL.Path, Session: session },
adminPageData: adminPageData{ Path: r.URL.Path, Session: session }, TOTP: totp,
TOTP: totp, NameEscaped: url.PathEscape(totp.Name),
NameEscaped: url.PathEscape(totp.Name), QRBase64Image: qrBase64Image,
QRBase64Image: qrBase64Image, })
}) if err != nil {
if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to render TOTP setup page: %v\n", err)
fmt.Fprintf(os.Stderr, "WARN: Failed to render TOTP setup page: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return
} }
return
} }
err = controller.ConfirmTOTP(app.DB, session.Account.ID, name) err = controller.ConfirmTOTP(app.DB, session.Account.ID, name)
@ -327,18 +321,23 @@ func totpConfirmHandler(app *model.AppState) http.Handler {
func totpDeleteHandler(app *model.AppState) http.Handler { func totpDeleteHandler(app *model.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodPost {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
if len(r.URL.Path) < 2 { session := r.Context().Value("session").(*model.Session)
err := r.ParseForm()
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
name := r.FormValue("totp-name")
if len(name) == 0 {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return return
} }
name := r.URL.Path[1:]
session := r.Context().Value("session").(*model.Session)
totp, err := controller.GetTOTP(app.DB, session.Account.ID, name) totp, err := controller.GetTOTP(app.DB, session.Account.ID, name)
if err != nil { if err != nil {

View file

@ -309,6 +309,7 @@ header :is(h1, h2, h3) small,
margin: 0 0 1em 0; margin: 0 0 1em 0;
padding: 1em; padding: 1em;
border-radius: 8px; border-radius: 8px;
color: #101010;
background: #ffffff; background: #ffffff;
} }
#message { #message {
@ -379,21 +380,25 @@ button:active, .button:active {
form { form {
width: 100%; width: 100%;
display: block; display: block;
color: var(--fg-0);
} }
form label { form label {
width: 100%; width: 100%;
margin: 1rem 0 .5rem 0; margin: 1rem 0 .5rem 0;
display: block; display: block;
color: #10101080;
} }
form input { form input {
margin: .5rem 0; min-width: 20rem;
padding: .3rem .5rem; max-width: calc(100% - 1em));
margin: .5em 0;
padding: .3em .5em;
display: block; display: block;
border-radius: 4px; border-radius: 4px;
border: 1px solid #808080;
font-size: inherit; font-size: inherit;
font-family: inherit; font-family: inherit;
color: inherit; color: inherit;
background-color: var(--bg-0);
} }
input[disabled] { input[disabled] {
opacity: .5; opacity: .5;

View file

@ -11,7 +11,8 @@ label {
align-items: center; align-items: center;
color: inherit; color: inherit;
} }
input { form#change-password input,
form#delete-account input {
width: min(20rem, calc(100% - 1rem)); width: min(20rem, calc(100% - 1rem));
margin: .5rem 0; margin: .5rem 0;
padding: .3rem .5rem; padding: .3rem .5rem;
@ -48,3 +49,7 @@ input {
.mfa-device .mfa-device-name { .mfa-device .mfa-device-name {
font-weight: bold; font-weight: bold;
} }
.mfa-device form input {
display: none;
}

View file

@ -1,7 +1,6 @@
{{define "head"}} {{define "head"}}
<title>Artists - ari melody 💫</title> <title>Artists - ari melody 💫</title>
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
<link rel="stylesheet" href="/admin/static/admin.css">
<link rel="stylesheet" href="/admin/static/artists.css"> <link rel="stylesheet" href="/admin/static/artists.css">
{{end}} {{end}}

View file

@ -28,6 +28,8 @@
<label for="confirm-password">Confirm Password</label> <label for="confirm-password">Confirm Password</label>
<input type="password" id="confirm-password" value="" autocomplete="new-password" required> <input type="password" id="confirm-password" value="" autocomplete="new-password" required>
<br>
<button type="submit" class="save">Change Password</button> <button type="submit" class="save">Change Password</button>
</form> </form>
</div> </div>
@ -44,7 +46,10 @@
<p class="mfa-device-date">Added: {{.CreatedAtString}}</p> <p class="mfa-device-date">Added: {{.CreatedAtString}}</p>
</div> </div>
<div> <div>
<a class="button delete" href="/admin/account/totp-delete/{{.TOTP.Name}}">Delete</a> <form method="POST" action="/admin/account/totp-delete">
<input type="text" name="totp-name" value="{{.TOTP.Name}}" hidden>
<button type="submit" class="delete">Delete</button>
</form>
</div> </div>
</div> </div>
{{end}} {{end}}
@ -67,13 +72,15 @@
This action is <strong>irreversible</strong>. This action is <strong>irreversible</strong>.
You will need to enter your password and TOTP below. You will need to enter your password and TOTP below.
</p> </p>
<form action="/admin/account/delete" method="POST"> <form action="/admin/account/delete" method="POST" id="delete-account">
<label for="password">Password</label> <label for="password">Password</label>
<input type="password" name="password" value="" autocomplete="current-password" required> <input type="password" name="password" value="" autocomplete="current-password" required>
<label for="totp">TOTP</label> <label for="totp">TOTP</label>
<input type="text" name="totp" value="" autocomplete="one-time-code" required> <input type="text" name="totp" value="" autocomplete="one-time-code" required>
<br>
<button type="submit" class="delete">Delete Account</button> <button type="submit" class="delete">Delete Account</button>
</form> </form>
</div> </div>

View file

@ -1,7 +1,6 @@
{{define "head"}} {{define "head"}}
<title>Admin - ari melody 💫</title> <title>Admin - ari melody 💫</title>
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
<link rel="stylesheet" href="/admin/static/admin.css">
<link rel="stylesheet" href="/admin/static/releases.css"> <link rel="stylesheet" href="/admin/static/releases.css">
<link rel="stylesheet" href="/admin/static/artists.css"> <link rel="stylesheet" href="/admin/static/artists.css">
<link rel="stylesheet" href="/admin/static/tracks.css"> <link rel="stylesheet" href="/admin/static/tracks.css">

View file

@ -1,7 +1,6 @@
{{define "head"}} {{define "head"}}
<title>Login - ari melody 💫</title> <title>Login - ari melody 💫</title>
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
<link rel="stylesheet" href="/admin/static/admin.css">
<style> <style>
form#login-totp { form#login-totp {
width: 100%; width: 100%;

View file

@ -1,7 +1,6 @@
{{define "head"}} {{define "head"}}
<title>Login - ari melody 💫</title> <title>Login - ari melody 💫</title>
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
<link rel="stylesheet" href="/admin/static/admin.css">
<style> <style>
form#login { form#login {
width: 100%; width: 100%;

View file

@ -1,7 +1,6 @@
{{define "head"}} {{define "head"}}
<title>Audit Logs - ari melody 💫</title> <title>Audit Logs - ari melody 💫</title>
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
<link rel="stylesheet" href="/admin/static/admin.css">
<link rel="stylesheet" href="/admin/static/logs.css"> <link rel="stylesheet" href="/admin/static/logs.css">
{{end}} {{end}}

View file

@ -1,7 +1,6 @@
{{define "head"}} {{define "head"}}
<title>Register - ari melody 💫</title> <title>Register - ari melody 💫</title>
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
<link rel="stylesheet" href="/admin/static/admin.css">
<style> <style>
p a { p a {
color: #2a67c8; color: #2a67c8;

View file

@ -1,7 +1,6 @@
{{define "head"}} {{define "head"}}
<title>Releases - ari melody 💫</title> <title>Releases - ari melody 💫</title>
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
<link rel="stylesheet" href="/admin/static/admin.css">
<link rel="stylesheet" href="/admin/static/releases.css"> <link rel="stylesheet" href="/admin/static/releases.css">
{{end}} {{end}}

View file

@ -1,7 +1,6 @@
{{define "head"}} {{define "head"}}
<title>TOTP Confirmation - ari melody 💫</title> <title>TOTP Confirmation - ari melody 💫</title>
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
<link rel="stylesheet" href="/admin/static/admin.css">
<style> <style>
.qr-code { .qr-code {
border: 1px solid #8888; border: 1px solid #8888;
@ -9,11 +8,20 @@
code { code {
user-select: all; user-select: all;
} }
#totp-setup input {
width: 3.8em;
min-width: auto;
font-size: 32px;
font-family: 'Monaspace Argon', monospace;
text-align: center;
}
</style> </style>
{{end}} {{end}}
{{define "content"}} {{define "content"}}
<main> <main>
<h1>Two-Factor Authentication</h1>
{{if .Session.Error.Valid}} {{if .Session.Error.Valid}}
<p id="error">{{html .Session.Error.String}}</p> <p id="error">{{html .Session.Error.String}}</p>
{{end}} {{end}}
@ -40,7 +48,14 @@ code {
<p><code>{{.TOTP.Secret}}</code></p> <p><code>{{.TOTP.Secret}}</code></p>
<label for="totp">TOTP:</label> <label for="totp">TOTP:</label>
<input type="text" name="totp" value="" autocomplete="one-time-code" required autofocus> <input type="text"
name="totp"
value=""
minlength="6"
maxlength="6"
autocomplete="one-time-code"
required
autofocus>
<button type="submit" class="new">Create</button> <button type="submit" class="new">Create</button>
</form> </form>

View file

@ -1,11 +1,12 @@
{{define "head"}} {{define "head"}}
<title>TOTP Setup - ari melody 💫</title> <title>TOTP Setup - ari melody 💫</title>
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
<link rel="stylesheet" href="/admin/static/admin.css">
{{end}} {{end}}
{{define "content"}} {{define "content"}}
<main> <main>
<h1>Two-Factor Authentication</h1>
{{if .Session.Error.Valid}} {{if .Session.Error.Valid}}
<p id="error">{{html .Session.Error.String}}</p> <p id="error">{{html .Session.Error.String}}</p>
{{end}} {{end}}

View file

@ -1,7 +1,6 @@
{{define "head"}} {{define "head"}}
<title>Releases - ari melody 💫</title> <title>Releases - ari melody 💫</title>
<link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon">
<link rel="stylesheet" href="/admin/static/admin.css">
<link rel="stylesheet" href="/admin/static/tracks.css"> <link rel="stylesheet" href="/admin/static/tracks.css">
{{end}} {{end}}