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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user