Add account management page

Add a new /web/account page, reachable by clicking the username in
the nav, with two forms: updating display name/email, and changing
the password (requires the current password, min 8 chars, confirm
match). Add db.SetProfile to persist display name/email.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-08-21 06:32:18 +02:00
co-authored by Copilot
parent 1c039e6396
commit 8b10386c91
9 changed files with 527 additions and 5 deletions
+11
View File
@@ -80,6 +80,17 @@ func (d *DB) SetPassword(username, password string) error {
return requireRowsAffected(res, ErrUserNotFound)
}
// SetProfile updates username's display name and email address. Returns
// ErrUserNotFound if the user doesn't exist.
func (d *DB) SetProfile(username, displayName, email string) error {
res, err := d.conn.Exec(`UPDATE users SET display_name = ?, email = ? WHERE username = ?`, displayName, email, username)
if err != nil {
return fmt.Errorf("setting profile: %w", err)
}
return requireRowsAffected(res, ErrUserNotFound)
}
// DeleteUser removes username along with all of its calendars, address
// books, and sharing grants (calendars/addressbooks cascade via foreign
// key; shares are cleaned up explicitly since they reference usernames as
+144
View File
@@ -0,0 +1,144 @@
package web
import (
"context"
"errors"
"net/http"
"strings"
"github.com/yourusername/caldav-server/internal/db"
"github.com/yourusername/caldav-server/internal/web/templates"
)
// handleAccount serves GET /account: the current user's own profile and
// password-change forms, reached by clicking the username in the nav.
func (s *Server) handleAccount(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.Header().Set("Allow", "GET")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
username := userFromContext(r.Context())
data, err := s.accountData(username)
if err != nil {
s.logger.Error("loading account", "error", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_ = templates.Account(data).Render(context.Background(), w)
}
// accountData loads the current profile fields for username, with the
// message fields left empty (used for the initial GET render).
func (s *Server) accountData(username string) (templates.AccountData, error) {
u, err := s.dbase.GetUser(username)
if err != nil {
return templates.AccountData{}, err
}
return templates.AccountData{
Username: u.Username,
DisplayName: u.DisplayName,
Email: u.Email,
}, nil
}
// handleAccountProfile handles POST /account/profile: updates the
// current user's display name and email, then re-renders the account
// page with a success or error message.
func (s *Server) handleAccountProfile(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.Header().Set("Allow", "POST")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
username := userFromContext(r.Context())
data, err := s.accountData(username)
if err != nil {
s.logger.Error("loading account", "error", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
if err := r.ParseForm(); err != nil {
data.ProfileMsg, data.ProfileErr = "invalid form submission", true
s.renderAccount(w, data)
return
}
displayName := strings.TrimSpace(r.PostForm.Get("display_name"))
email := strings.TrimSpace(r.PostForm.Get("email"))
if err := s.dbase.SetProfile(username, displayName, email); err != nil {
s.logger.Error("updating profile", "error", err)
data.DisplayName, data.Email = displayName, email
data.ProfileMsg, data.ProfileErr = "could not update profile", true
s.renderAccount(w, data)
return
}
data.DisplayName, data.Email = displayName, email
data.ProfileMsg = "Profile updated."
s.renderAccount(w, data)
}
// handleAccountPassword handles POST /account/password: verifies the
// current password, then updates it to the requested new password, and
// re-renders the account page with a success or error message.
func (s *Server) handleAccountPassword(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.Header().Set("Allow", "POST")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
username := userFromContext(r.Context())
data, err := s.accountData(username)
if err != nil {
s.logger.Error("loading account", "error", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
if err := r.ParseForm(); err != nil {
data.PasswordMsg, data.PasswordErr = "invalid form submission", true
s.renderAccount(w, data)
return
}
current := r.PostForm.Get("current_password")
newPassword := r.PostForm.Get("new_password")
confirm := r.PostForm.Get("confirm_password")
if !s.dbase.VerifyPassword(username, current) {
data.PasswordMsg, data.PasswordErr = "current password is incorrect", true
s.renderAccount(w, data)
return
}
if len(newPassword) < 8 {
data.PasswordMsg, data.PasswordErr = "new password must be at least 8 characters", true
s.renderAccount(w, data)
return
}
if newPassword != confirm {
data.PasswordMsg, data.PasswordErr = "new password and confirmation don't match", true
s.renderAccount(w, data)
return
}
if err := s.dbase.SetPassword(username, newPassword); err != nil && !errors.Is(err, db.ErrUserNotFound) {
s.logger.Error("changing password", "error", err)
data.PasswordMsg, data.PasswordErr = "could not change password", true
s.renderAccount(w, data)
return
}
data.PasswordMsg = "Password changed."
s.renderAccount(w, data)
}
func (s *Server) renderAccount(w http.ResponseWriter, data templates.AccountData) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_ = templates.Account(data).Render(context.Background(), w)
}
+3
View File
@@ -42,6 +42,9 @@ func (s *Server) Handler(staticFS http.FileSystem) http.Handler {
mux.HandleFunc("/login", s.handleLogin)
mux.HandleFunc("/logout", s.handleLogout)
mux.HandleFunc("/", s.requireLogin(s.handleDashboard))
mux.HandleFunc("/account", s.requireLogin(s.handleAccount))
mux.HandleFunc("/account/profile", s.requireLogin(s.handleAccountProfile))
mux.HandleFunc("/account/password", s.requireLogin(s.handleAccountPassword))
mux.HandleFunc("/shares/calendar", s.requireLogin(s.handleCalendarShare))
mux.HandleFunc("/shares/addressbook", s.requireLogin(s.handleAddressBookShare))
mux.HandleFunc("/resources/calendar", s.requireLogin(s.handleCalendarResource))
+94
View File
@@ -223,3 +223,97 @@ type emptyStaticFS struct{}
func (emptyStaticFS) Open(name string) (http.File, error) {
return nil, os.ErrNotExist
}
func TestAccountRequiresLogin(t *testing.T) {
s := newTestServer(t)
handler := s.Handler(emptyStaticFS{})
req := httptest.NewRequest(http.MethodGet, "/account", nil)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusSeeOther {
t.Fatalf("expected redirect to login, got %d", rr.Code)
}
}
func TestAccountShowsOwnProfile(t *testing.T) {
s := newTestServer(t)
handler := s.Handler(emptyStaticFS{})
cookie := loginAs(t, handler, "alice", "password")
req := httptest.NewRequest(http.MethodGet, "/account", nil)
req.AddCookie(cookie)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rr.Code)
}
if !strings.Contains(rr.Body.String(), "alice") {
t.Fatalf("expected account page to show username, got: %s", rr.Body.String())
}
}
func TestAccountUpdateProfile(t *testing.T) {
s := newTestServer(t)
handler := s.Handler(emptyStaticFS{})
cookie := loginAs(t, handler, "alice", "password")
form := url.Values{"display_name": {"Alice Example"}, "email": {"alice@example.com"}}
req := httptest.NewRequest(http.MethodPost, "/account/profile", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(cookie)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rr.Code)
}
if !strings.Contains(rr.Body.String(), "Profile updated.") {
t.Fatalf("expected success message, got: %s", rr.Body.String())
}
u, err := s.dbase.GetUser("alice")
if err != nil {
t.Fatalf("GetUser: %v", err)
}
if u.DisplayName != "Alice Example" || u.Email != "alice@example.com" {
t.Fatalf("expected profile to be persisted, got %+v", u)
}
}
func TestAccountChangePassword(t *testing.T) {
s := newTestServer(t)
handler := s.Handler(emptyStaticFS{})
cookie := loginAs(t, handler, "alice", "password")
// Wrong current password is rejected.
form := url.Values{"current_password": {"wrong"}, "new_password": {"newpassword123"}, "confirm_password": {"newpassword123"}}
req := httptest.NewRequest(http.MethodPost, "/account/password", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(cookie)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "current password is incorrect") {
t.Fatalf("expected error for wrong current password, got: %s", rr.Body.String())
}
if s.dbase.VerifyPassword("alice", "newpassword123") {
t.Fatal("password should not have changed")
}
// Correct current password succeeds.
form = url.Values{"current_password": {"password"}, "new_password": {"newpassword123"}, "confirm_password": {"newpassword123"}}
req = httptest.NewRequest(http.MethodPost, "/account/password", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(cookie)
rr = httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if !strings.Contains(rr.Body.String(), "Password changed.") {
t.Fatalf("expected success message, got: %s", rr.Body.String())
}
if !s.dbase.VerifyPassword("alice", "newpassword123") {
t.Fatal("expected password to have changed")
}
}
+79
View File
@@ -0,0 +1,79 @@
package templates
// AccountData is everything the account page needs to render the
// current user's profile and password forms.
type AccountData struct {
Username string
DisplayName string
Email string
ProfileMsg string // success/error message shown above the profile form, "" for none
ProfileErr bool // true if ProfileMsg is an error (red) rather than a success (green)
PasswordMsg string // success/error message shown above the password form, "" for none
PasswordErr bool
}
templ Account(data AccountData) {
@Layout("Account", data.Username) {
<h1 class="text-2xl font-semibold mb-6">Account</h1>
<div class="bg-white rounded-lg border border-gray-200 p-6 mb-6 max-w-lg">
<h2 class="text-lg font-medium mb-4">Profile</h2>
if data.ProfileMsg != "" {
<p class={ "mb-4 text-sm rounded px-3 py-2 border", templ.KV("text-red-600 bg-red-50 border-red-200", data.ProfileErr), templ.KV("text-green-700 bg-green-50 border-green-200", !data.ProfileErr) }>
{ data.ProfileMsg }
</p>
}
<form method="POST" action="/web/account/profile" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">Username</label>
<input type="text" value={ data.Username } disabled
class="mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 bg-gray-50 text-gray-500 shadow-sm"/>
</div>
<div>
<label for="display_name" class="block text-sm font-medium text-gray-700">Display name</label>
<input id="display_name" name="display_name" type="text" value={ data.DisplayName }
class="mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"/>
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-700">Email</label>
<input id="email" name="email" type="email" value={ data.Email }
class="mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"/>
</div>
<button type="submit"
class="bg-indigo-600 text-white rounded-md px-4 py-2 text-sm font-medium hover:bg-indigo-700">
Save profile
</button>
</form>
</div>
<div class="bg-white rounded-lg border border-gray-200 p-6 max-w-lg">
<h2 class="text-lg font-medium mb-4">Change password</h2>
if data.PasswordMsg != "" {
<p class={ "mb-4 text-sm rounded px-3 py-2 border", templ.KV("text-red-600 bg-red-50 border-red-200", data.PasswordErr), templ.KV("text-green-700 bg-green-50 border-green-200", !data.PasswordErr) }>
{ data.PasswordMsg }
</p>
}
<form method="POST" action="/web/account/password" class="space-y-4">
<div>
<label for="current_password" class="block text-sm font-medium text-gray-700">Current password</label>
<input id="current_password" name="current_password" type="password" required
class="mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"/>
</div>
<div>
<label for="new_password" class="block text-sm font-medium text-gray-700">New password</label>
<input id="new_password" name="new_password" type="password" required minlength="8"
class="mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"/>
</div>
<div>
<label for="confirm_password" class="block text-sm font-medium text-gray-700">Confirm new password</label>
<input id="confirm_password" name="confirm_password" type="password" required minlength="8"
class="mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"/>
</div>
<button type="submit"
class="bg-indigo-600 text-white rounded-md px-4 py-2 text-sm font-medium hover:bg-indigo-700">
Change password
</button>
</form>
</div>
}
}
+191
View File
@@ -0,0 +1,191 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// AccountData is everything the account page needs to render the
// current user's profile and password forms.
type AccountData struct {
Username string
DisplayName string
Email string
ProfileMsg string // success/error message shown above the profile form, "" for none
ProfileErr bool // true if ProfileMsg is an error (red) rather than a success (green)
PasswordMsg string // success/error message shown above the password form, "" for none
PasswordErr bool
}
func Account(data AccountData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<h1 class=\"text-2xl font-semibold mb-6\">Account</h1><div class=\"bg-white rounded-lg border border-gray-200 p-6 mb-6 max-w-lg\"><h2 class=\"text-lg font-medium mb-4\">Profile</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.ProfileMsg != "" {
var templ_7745c5c3_Var3 = []any{"mb-4 text-sm rounded px-3 py-2 border", templ.KV("text-red-600 bg-red-50 border-red-200", data.ProfileErr), templ.KV("text-green-700 bg-green-50 border-green-200", !data.ProfileErr)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var3...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<p class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var3).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/account.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.ProfileMsg)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/account.templ`, Line: 23, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<form method=\"POST\" action=\"/web/account/profile\" class=\"space-y-4\"><div><label class=\"block text-sm font-medium text-gray-700\">Username</label> <input type=\"text\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Username)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/account.templ`, Line: 29, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" disabled class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 bg-gray-50 text-gray-500 shadow-sm\"></div><div><label for=\"display_name\" class=\"block text-sm font-medium text-gray-700\">Display name</label> <input id=\"display_name\" name=\"display_name\" type=\"text\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.DisplayName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/account.templ`, Line: 34, Col: 86}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500\"></div><div><label for=\"email\" class=\"block text-sm font-medium text-gray-700\">Email</label> <input id=\"email\" name=\"email\" type=\"email\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Email)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/account.templ`, Line: 39, Col: 67}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500\"></div><button type=\"submit\" class=\"bg-indigo-600 text-white rounded-md px-4 py-2 text-sm font-medium hover:bg-indigo-700\">Save profile</button></form></div><div class=\"bg-white rounded-lg border border-gray-200 p-6 max-w-lg\"><h2 class=\"text-lg font-medium mb-4\">Change password</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.PasswordMsg != "" {
var templ_7745c5c3_Var9 = []any{"mb-4 text-sm rounded px-3 py-2 border", templ.KV("text-red-600 bg-red-50 border-red-200", data.PasswordErr), templ.KV("text-green-700 bg-green-50 border-green-200", !data.PasswordErr)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<p class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var9).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/account.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(data.PasswordMsg)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/account.templ`, Line: 53, Col: 23}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<form method=\"POST\" action=\"/web/account/password\" class=\"space-y-4\"><div><label for=\"current_password\" class=\"block text-sm font-medium text-gray-700\">Current password</label> <input id=\"current_password\" name=\"current_password\" type=\"password\" required class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500\"></div><div><label for=\"new_password\" class=\"block text-sm font-medium text-gray-700\">New password</label> <input id=\"new_password\" name=\"new_password\" type=\"password\" required minlength=\"8\" class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500\"></div><div><label for=\"confirm_password\" class=\"block text-sm font-medium text-gray-700\">Confirm new password</label> <input id=\"confirm_password\" name=\"confirm_password\" type=\"password\" required minlength=\"8\" class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500\"></div><button type=\"submit\" class=\"bg-indigo-600 text-white rounded-md px-4 py-2 text-sm font-medium hover:bg-indigo-700\">Change password</button></form></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = Layout("Account", data.Username).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
+1 -1
View File
@@ -23,7 +23,7 @@ templ Layout(title string, username string) {
</div>
if username != "" {
<div class="flex items-center gap-4 text-sm text-gray-600">
<span>{ username }</span>
<a href="/web/account" class="hover:text-indigo-600">{ username }</a>
<a href="/web/logout" class="text-red-600 hover:underline">Logout</a>
</div>
}
+3 -3
View File
@@ -57,20 +57,20 @@ func Layout(title string, username string) templ.Component {
return templ_7745c5c3_Err
}
if username != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"flex items-center gap-4 text-sm text-gray-600\"><span>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"flex items-center gap-4 text-sm text-gray-600\"><a href=\"/web/account\" class=\"hover:text-indigo-600\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(username)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/layout.templ`, Line: 26, Col: 23}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/layout.templ`, Line: 26, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</span> <a href=\"/web/logout\" class=\"text-red-600 hover:underline\">Logout</a></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</a> <a href=\"/web/logout\" class=\"text-red-600 hover:underline\">Logout</a></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}