chore: remove signup and account routes
This commit is contained in:
@@ -3,9 +3,7 @@ package auth
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
|
|
||||||
"mal/internal/database"
|
|
||||||
"mal/internal/templates"
|
"mal/internal/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,38 +13,6 @@ type Handler struct {
|
|||||||
|
|
||||||
const rateLimitFormError = "Too many attempts in a short time. Please wait a minute and try again."
|
const rateLimitFormError = "Too many attempts in a short time. Please wait a minute and try again."
|
||||||
|
|
||||||
const (
|
|
||||||
accountPasswordChangedMessage = "Password updated successfully."
|
|
||||||
accountRecoveryKeyRotatedMessage = "Recovery key rotated. Save this new key now."
|
|
||||||
accountPasswordErrorMessage = "Unable to update password with those details."
|
|
||||||
accountRecoveryErrorMessage = "Unable to rotate recovery key with those details."
|
|
||||||
accountUnexpectedErrorMessage = "Something went wrong. Please try again."
|
|
||||||
accountMissingFieldsErrorMessage = "Please complete all required fields."
|
|
||||||
accountPasswordMismatchErrorMessage = "New password and confirm password must match."
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h *Handler) accountUserFromRequest(r *http.Request) (*database.User, bool) {
|
|
||||||
cookie, err := r.Cookie("session_id")
|
|
||||||
if err != nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := h.authService.ValidateSession(r.Context(), cookie.Value)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return user, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func accountCreatedAt(createdAt time.Time) string {
|
|
||||||
return createdAt.Local().Format("Jan 2, 2006 at 15:04")
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderAccountPage(w http.ResponseWriter, r *http.Request, user *database.User, passwordError string, passwordSuccess string, recoveryError string, recoverySuccess string, recoveryKey string) {
|
|
||||||
templates.Account(user.Username, accountCreatedAt(user.CreatedAt), passwordError, passwordSuccess, recoveryError, recoverySuccess, recoveryKey).Render(r.Context(), w)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHandler(authService *Service) *Handler {
|
func NewHandler(authService *Service) *Handler {
|
||||||
return &Handler{authService: authService}
|
return &Handler{authService: authService}
|
||||||
}
|
}
|
||||||
@@ -86,41 +52,6 @@ func (h *Handler) HandleLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(w, r, "/", http.StatusFound)
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) HandleRegister(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if err := r.ParseForm(); err != nil {
|
|
||||||
templates.Register("Something went wrong. Please try again.", "").Render(r.Context(), w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
username := r.FormValue("username")
|
|
||||||
password := r.FormValue("password")
|
|
||||||
|
|
||||||
if username == "" || password == "" {
|
|
||||||
templates.Register("Please enter both email and password.", username).Render(r.Context(), w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, recoveryKey, err := h.authService.RegisterUser(r.Context(), username, password)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, ErrInvalidPassword) || errors.Is(err, ErrUserExists) {
|
|
||||||
templates.Register("Unable to create account with those details.", username).Render(r.Context(), w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
templates.Register("Something went wrong. Please try again.", username).Render(r.Context(), w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-login after successful registration
|
|
||||||
session, err := h.authService.Login(r.Context(), username, password)
|
|
||||||
if err != nil {
|
|
||||||
http.Redirect(w, r, "/login", http.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
SetSessionCookie(w, session.ID, session.ExpiresAt)
|
|
||||||
templates.RegistrationRecoveryKey(recoveryKey).Render(r.Context(), w)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) HandleLogout(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) HandleLogout(w http.ResponseWriter, r *http.Request) {
|
||||||
cookie, err := r.Cookie("session_id")
|
cookie, err := r.Cookie("session_id")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -136,10 +67,6 @@ func (h *Handler) HandleLoginPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
templates.Login(rateLimitErrorFromQuery(r), "").Render(r.Context(), w)
|
templates.Login(rateLimitErrorFromQuery(r), "").Render(r.Context(), w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) HandleRegisterPage(w http.ResponseWriter, r *http.Request) {
|
|
||||||
templates.Register(rateLimitErrorFromQuery(r), "").Render(r.Context(), w)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) HandleRecoverPage(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) HandleRecoverPage(w http.ResponseWriter, r *http.Request) {
|
||||||
templates.Recover(rateLimitErrorFromQuery(r), "", "").Render(r.Context(), w)
|
templates.Recover(rateLimitErrorFromQuery(r), "", "").Render(r.Context(), w)
|
||||||
}
|
}
|
||||||
@@ -171,98 +98,3 @@ func (h *Handler) HandleRecover(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
templates.RecoveryComplete(newRecoveryKey).Render(r.Context(), w)
|
templates.RecoveryComplete(newRecoveryKey).Render(r.Context(), w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) HandleAccountPage(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, ok := h.accountUserFromRequest(r)
|
|
||||||
if !ok {
|
|
||||||
http.Redirect(w, r, "/login", http.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
renderAccountPage(w, r, user, "", "", "", "", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) HandleAccountPassword(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, ok := h.accountUserFromRequest(r)
|
|
||||||
if !ok {
|
|
||||||
http.Redirect(w, r, "/login", http.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.ParseForm(); err != nil {
|
|
||||||
renderAccountPage(w, r, user, accountUnexpectedErrorMessage, "", "", "", "")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPassword := r.FormValue("current_password")
|
|
||||||
newPassword := r.FormValue("new_password")
|
|
||||||
confirmNewPassword := r.FormValue("confirm_new_password")
|
|
||||||
|
|
||||||
if currentPassword == "" || newPassword == "" || confirmNewPassword == "" {
|
|
||||||
renderAccountPage(w, r, user, accountMissingFieldsErrorMessage, "", "", "", "")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if newPassword != confirmNewPassword {
|
|
||||||
renderAccountPage(w, r, user, accountPasswordMismatchErrorMessage, "", "", "", "")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := h.authService.ChangePassword(r.Context(), user.ID, currentPassword, newPassword)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, ErrInvalidCredentials) || errors.Is(err, ErrInvalidPassword) {
|
|
||||||
renderAccountPage(w, r, user, accountPasswordErrorMessage, "", "", "", "")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
renderAccountPage(w, r, user, accountUnexpectedErrorMessage, "", "", "", "")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
renderAccountPage(w, r, user, "", accountPasswordChangedMessage, "", "", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) HandleAccountRecoveryKey(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, ok := h.accountUserFromRequest(r)
|
|
||||||
if !ok {
|
|
||||||
http.Redirect(w, r, "/login", http.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.ParseForm(); err != nil {
|
|
||||||
renderAccountPage(w, r, user, "", "", accountUnexpectedErrorMessage, "", "")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
password := r.FormValue("password")
|
|
||||||
if password == "" {
|
|
||||||
renderAccountPage(w, r, user, "", "", accountMissingFieldsErrorMessage, "", "")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
newRecoveryKey, err := h.authService.RegenerateRecoveryKey(r.Context(), user.ID, password)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, ErrInvalidCredentials) {
|
|
||||||
renderAccountPage(w, r, user, "", "", accountRecoveryErrorMessage, "", "")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
renderAccountPage(w, r, user, "", "", accountUnexpectedErrorMessage, "", "")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
renderAccountPage(w, r, user, "", "", "", accountRecoveryKeyRotatedMessage, newRecoveryKey)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -68,13 +68,6 @@ func NewRouter(cfg Config) http.Handler {
|
|||||||
middleware.RateLimitAuth(middleware.VerifyOrigin(http.HandlerFunc(authHandler.HandleLogin))).ServeHTTP(w, r)
|
middleware.RateLimitAuth(middleware.VerifyOrigin(http.HandlerFunc(authHandler.HandleLogin))).ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
mux.HandleFunc("/register", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method == http.MethodGet {
|
|
||||||
authHandler.HandleRegisterPage(w, r)
|
|
||||||
} else {
|
|
||||||
middleware.RateLimitAuth(middleware.VerifyOrigin(http.HandlerFunc(authHandler.HandleRegister))).ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
mux.HandleFunc("/recover", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/recover", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method == http.MethodGet {
|
if r.Method == http.MethodGet {
|
||||||
authHandler.HandleRecoverPage(w, r)
|
authHandler.HandleRecoverPage(w, r)
|
||||||
@@ -85,13 +78,6 @@ func NewRouter(cfg Config) http.Handler {
|
|||||||
mux.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) {
|
||||||
middleware.VerifyOrigin(http.HandlerFunc(authHandler.HandleLogout)).ServeHTTP(w, r)
|
middleware.VerifyOrigin(http.HandlerFunc(authHandler.HandleLogout)).ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
mux.HandleFunc("/account", authHandler.HandleAccountPage)
|
|
||||||
mux.HandleFunc("/account/password", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
middleware.VerifyOrigin(http.HandlerFunc(authHandler.HandleAccountPassword)).ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
mux.HandleFunc("/account/recovery-key", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
middleware.VerifyOrigin(http.HandlerFunc(authHandler.HandleAccountRecoveryKey)).ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Watchlist Endpoints
|
// Watchlist Endpoints
|
||||||
mux.HandleFunc("/api/watchlist/export", watchlistHandler.HandleExportWatchlist)
|
mux.HandleFunc("/api/watchlist/export", watchlistHandler.HandleExportWatchlist)
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ func RequireAuth(next http.Handler) http.Handler {
|
|||||||
|
|
||||||
func RequireGlobalAuth(next http.Handler) http.Handler {
|
func RequireGlobalAuth(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Allow unauthenticated access to login, register, search, and static files
|
// Allow unauthenticated access to auth pages, search, and static files
|
||||||
if r.URL.Path == "/login" || r.URL.Path == "/register" || r.URL.Path == "/recover" ||
|
if r.URL.Path == "/login" || r.URL.Path == "/recover" ||
|
||||||
strings.HasPrefix(r.URL.Path, "/static/") || strings.HasPrefix(r.URL.Path, "/dist/") ||
|
strings.HasPrefix(r.URL.Path, "/static/") || strings.HasPrefix(r.URL.Path, "/dist/") ||
|
||||||
r.URL.Path == "/search" || r.URL.Path == "/api/search" || r.URL.Path == "/api/search-quick" ||
|
r.URL.Path == "/search" || r.URL.Path == "/api/search" || r.URL.Path == "/api/search-quick" ||
|
||||||
r.URL.Path == "/" {
|
r.URL.Path == "/" {
|
||||||
|
|||||||
@@ -20,9 +20,6 @@ templ Login(formError string, username string) {
|
|||||||
<p class="mt-2 text-xs text-(--danger)" role="alert" aria-live="polite">{ formError }</p>
|
<p class="mt-2 text-xs text-(--danger)" role="alert" aria-live="polite">{ formError }</p>
|
||||||
}
|
}
|
||||||
</form>
|
</form>
|
||||||
<p class="mt-5 mb-0 text-center text-sm text-(--text-muted)">
|
|
||||||
Don't have an account? <a class="text-(--accent)" href="/register">Register</a>
|
|
||||||
</p>
|
|
||||||
<p class="mt-5 mb-0 text-center text-sm text-(--text-muted)">
|
<p class="mt-5 mb-0 text-center text-sm text-(--text-muted)">
|
||||||
Lost access? <a class="text-(--accent)" href="/recover">Recover account</a>
|
Lost access? <a class="text-(--accent)" href="/recover">Recover account</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -31,56 +28,6 @@ templ Login(formError string, username string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
templ Register(formError string, username string) {
|
|
||||||
@Layout("Register", false) {
|
|
||||||
<div class="w-full max-w-xl">
|
|
||||||
<div class="mx-auto w-full bg-(--panel) p-6">
|
|
||||||
<h2 class="m-0 text-2xl">Register</h2>
|
|
||||||
<p class="my-3 mb-5 text-sm text-(--text-muted)">Create a new account to track anime.</p>
|
|
||||||
<form action="/register" method="POST" class="grid gap-4">
|
|
||||||
<div class="grid gap-1">
|
|
||||||
<label for="username">Username / Email</label>
|
|
||||||
<input class="h-10 border border-transparent bg-(--surface-search) px-3 text-(--text) transition-colors duration-120 focus:border-(--surface-search-focus-border) focus:outline-none" type="text" id="username" name="username" required placeholder="you@example.com" value={ username }/>
|
|
||||||
</div>
|
|
||||||
<div class="grid gap-1">
|
|
||||||
<label for="password">Password</label>
|
|
||||||
<input class="h-10 border border-transparent bg-(--surface-search) px-3 text-(--text) transition-colors duration-120 focus:border-(--surface-search-focus-border) focus:outline-none" type="password" id="password" name="password" required placeholder="Minimum 12 chars"/>
|
|
||||||
</div>
|
|
||||||
<p class="m-0 text-xs leading-normal text-(--text-faint)">
|
|
||||||
Password must be at least 12 characters and include an uppercase letter, lowercase letter, number, and special character.
|
|
||||||
</p>
|
|
||||||
<button type="submit" class="h-10 cursor-pointer border-0 bg-(--accent) text-sm font-semibold text-(--text-on-accent) hover:brightness-95">Create account</button>
|
|
||||||
if formError != "" {
|
|
||||||
<p class="mt-2 text-xs text-(--danger)" role="alert" aria-live="polite">{ formError }</p>
|
|
||||||
}
|
|
||||||
</form>
|
|
||||||
<p class="mt-5 mb-0 text-center text-sm text-(--text-muted)">
|
|
||||||
Already have an account? <a class="text-(--accent)" href="/login">Sign in</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
templ RegistrationRecoveryKey(recoveryKey string) {
|
|
||||||
@Layout("Save recovery key", false) {
|
|
||||||
<div class="w-full max-w-xl">
|
|
||||||
<div class="mx-auto w-full bg-(--panel) p-6">
|
|
||||||
<h2>Save your recovery key</h2>
|
|
||||||
<p class="my-3 mb-5 text-sm text-(--text-muted)">Store this key somewhere safe. It is shown only once.</p>
|
|
||||||
<div class="grid gap-2">
|
|
||||||
<p class="m-0 break-all border border-(--surface-search-focus-border) bg-(--surface-search) p-3 text-sm text-(--text)" id="registration-recovery-key">{ recoveryKey }</p>
|
|
||||||
<button type="button" class="min-w-0 justify-self-start border border-(--surface-search-focus-border) bg-(--surface-search) px-3 py-1.5 text-xs leading-none text-(--text) hover:bg-(--surface-input-focus)" onclick="copyRecoveryKey('registration-recovery-key', 'registration-copy-feedback')">Copy key</button>
|
|
||||||
</div>
|
|
||||||
<p class="mt-2 min-h-5 text-xs leading-normal text-(--text-faint)" id="registration-copy-feedback" aria-live="polite"></p>
|
|
||||||
<p class="m-0 text-xs leading-normal text-(--text-faint)">If you lose your password, this key is the only way to recover your account without email.</p>
|
|
||||||
<p class="mt-5 mb-0 text-center text-sm text-(--text-muted)">
|
|
||||||
<a href="/" class="inline-flex min-h-10 items-center justify-center border border-(--surface-search-focus-border) bg-(--surface-search) px-4 text-(--text) no-underline hover:bg-(--panel-soft) hover:no-underline">I saved it, continue</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
templ Recover(formError string, username string, recoveryKey string) {
|
templ Recover(formError string, username string, recoveryKey string) {
|
||||||
@Layout("Recover account", false) {
|
@Layout("Recover account", false) {
|
||||||
@@ -133,80 +80,3 @@ templ RecoveryComplete(newRecoveryKey string) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
templ Account(username string, createdAt string, passwordError string, passwordSuccess string, recoveryError string, recoverySuccess string, recoveryKey string) {
|
|
||||||
@Layout("mal - account", true) {
|
|
||||||
<div class="mx-auto grid w-full max-w-3xl gap-4">
|
|
||||||
<section class="grid gap-3 bg-(--panel) p-5">
|
|
||||||
<h2>Account</h2>
|
|
||||||
<div class="grid gap-2">
|
|
||||||
<div class="grid gap-1">
|
|
||||||
<span class="text-xs text-(--text-faint)">Email / Username</span>
|
|
||||||
<span class="text-sm text-(--text)">{ username }</span>
|
|
||||||
</div>
|
|
||||||
<div class="grid gap-1">
|
|
||||||
<span class="text-xs text-(--text-faint)">Created</span>
|
|
||||||
<span class="text-sm text-(--text)">{ createdAt }</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="grid gap-3 bg-(--panel) p-5">
|
|
||||||
<h3>Change password</h3>
|
|
||||||
<form action="/account/password" method="POST" class="grid gap-3" onsubmit="return confirmDangerAction('Change your password now?')">
|
|
||||||
<div class="grid gap-1">
|
|
||||||
<label for="current_password">Current password</label>
|
|
||||||
<input class="h-10 w-full border border-transparent bg-(--surface-search) px-3 text-(--text) transition-colors duration-120 focus:border-(--surface-search-focus-border) focus:outline-none" type="password" id="current_password" name="current_password" required/>
|
|
||||||
</div>
|
|
||||||
<div class="grid gap-1">
|
|
||||||
<label for="new_password">New password</label>
|
|
||||||
<input class="h-10 w-full border border-transparent bg-(--surface-search) px-3 text-(--text) transition-colors duration-120 focus:border-(--surface-search-focus-border) focus:outline-none" type="password" id="new_password" name="new_password" required placeholder="Minimum 12 chars"/>
|
|
||||||
</div>
|
|
||||||
<div class="grid gap-1">
|
|
||||||
<label for="confirm_new_password">Confirm new password</label>
|
|
||||||
<input class="h-10 w-full border border-transparent bg-(--surface-search) px-3 text-(--text) transition-colors duration-120 focus:border-(--surface-search-focus-border) focus:outline-none" type="password" id="confirm_new_password" name="confirm_new_password" required/>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="h-10 cursor-pointer border border-(--surface-search-focus-border) bg-(--surface-search) px-4 text-(--text) hover:bg-(--panel-soft)">Update password</button>
|
|
||||||
if passwordError != "" {
|
|
||||||
<p class="mt-2 text-xs text-(--danger)" role="alert" aria-live="polite">{ passwordError }</p>
|
|
||||||
}
|
|
||||||
if passwordSuccess != "" {
|
|
||||||
<p class="m-0 text-xs text-(--accent)" role="status" aria-live="polite">{ passwordSuccess }</p>
|
|
||||||
}
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="grid gap-3 bg-(--panel) p-5">
|
|
||||||
<h3>Recovery key</h3>
|
|
||||||
<p class="m-0 text-xs leading-normal text-(--text-faint)">To view a new recovery key, confirm your current password. This rotates your old key.</p>
|
|
||||||
<form action="/account/recovery-key" method="POST" class="grid gap-3" onsubmit="return confirmDangerAction('Rotate recovery key now? Your old key will stop working.')">
|
|
||||||
<div class="grid gap-1">
|
|
||||||
<label for="recovery_password">Current password</label>
|
|
||||||
<input class="h-10 w-full border border-transparent bg-(--surface-search) px-3 text-(--text) transition-colors duration-120 focus:border-(--surface-search-focus-border) focus:outline-none" type="password" id="recovery_password" name="password" required/>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="h-10 cursor-pointer border border-(--surface-search-focus-border) bg-(--surface-search) px-4 text-(--text) hover:bg-(--panel-soft)">Show new recovery key</button>
|
|
||||||
if recoveryError != "" {
|
|
||||||
<p class="mt-2 text-xs text-(--danger)" role="alert" aria-live="polite">{ recoveryError }</p>
|
|
||||||
}
|
|
||||||
if recoverySuccess != "" {
|
|
||||||
<p class="m-0 text-xs text-(--accent)" role="status" aria-live="polite">{ recoverySuccess }</p>
|
|
||||||
}
|
|
||||||
</form>
|
|
||||||
if recoveryKey != "" {
|
|
||||||
<div class="grid gap-2">
|
|
||||||
<p class="m-0 break-all border border-(--surface-search-focus-border) bg-(--surface-search) p-3 text-sm text-(--text)" id="account-recovery-key">{ recoveryKey }</p>
|
|
||||||
<button type="button" class="min-w-0 justify-self-start border border-(--surface-search-focus-border) bg-(--surface-search) px-3 py-1.5 text-xs leading-none text-(--text) hover:bg-(--surface-input-focus)" onclick="copyRecoveryKey('account-recovery-key', 'account-copy-feedback')">Copy key</button>
|
|
||||||
</div>
|
|
||||||
<p class="mt-2 min-h-5 text-xs leading-normal text-(--text-faint)" id="account-copy-feedback" aria-live="polite"></p>
|
|
||||||
}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="grid gap-3 bg-(--panel) p-5">
|
|
||||||
<h3>Danger zone</h3>
|
|
||||||
<form action="/logout" method="POST" class="inline-flex" onsubmit="return confirmDangerAction('Log out of this account now?')">
|
|
||||||
<button type="submit" class="h-10 cursor-pointer border border-(--surface-search-focus-border) bg-(--surface-search) px-4 text-(--text) hover:bg-(--panel-soft)">Log out</button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ templ Layout(title string, showHeader bool) {
|
|||||||
<a class="text-(--text-muted) no-underline hover:text-(--text) hover:no-underline" href="/discover">Discover</a>
|
<a class="text-(--text-muted) no-underline hover:text-(--text) hover:no-underline" href="/discover">Discover</a>
|
||||||
<a class="text-(--text-muted) no-underline hover:text-(--text) hover:no-underline" href="/notifications">Notifications</a>
|
<a class="text-(--text-muted) no-underline hover:text-(--text) hover:no-underline" href="/notifications">Notifications</a>
|
||||||
<a class="text-(--text-muted) no-underline hover:text-(--text) hover:no-underline" href="/watchlist">Watchlist</a>
|
<a class="text-(--text-muted) no-underline hover:text-(--text) hover:no-underline" href="/watchlist">Watchlist</a>
|
||||||
<a class="text-(--text-muted) no-underline hover:text-(--text) hover:no-underline" href="/account">Account</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative ml-auto min-w-60 w-full max-w-md max-lg:ml-0" data-search-root>
|
<div class="relative ml-auto min-w-60 w-full max-w-md max-lg:ml-0" data-search-root>
|
||||||
|
|||||||
Reference in New Issue
Block a user