All checks were successful
check / check (push) Successful in 4s
Adds an authenticated, CSRF-protected flow that lets a user change their own password from the profile page.
## Route
- New `POST /password` under the `/user/{username}` group in `setupUserRoutes` (`internal/server/routes.go`). That group already applies `CSRF`, `NoCache`, and `RequireAuth`, so the new endpoint inherits all three.
## Handler (`internal/handlers/profile.go`)
- `HandlePasswordChange` enforces own-user access: the `{username}` path parameter must equal the session username (same 403 rule `HandleProfile` uses). This check plus the session lookup is factored into a shared `profileOwnerOrDeny` helper now used by both handlers.
- Parses `current_password`, `new_password`, and `confirm_password` (body size limited via `http.MaxBytesReader`).
- Verifies the current password with `database.VerifyPassword` against the stored hash.
- Requires the new password to be non-empty and equal to the confirmation.
- Hashes the new password with `database.HashPassword` — the same Argon2id helper used to bootstrap the admin user — and persists it on the user row. No new crypto.
- Re-renders the profile page with a clear success or error message. Wrong current password, empty new password, and mismatched confirmation are each rejected with their own message and leave the stored hash unchanged.
## Template (`templates/profile.html`)
- Adds a "Change Password" card with current / new / confirm password fields plus the hidden `csrf_token` (matching the login form's CSRF embedding).
- Renders success/error alerts using the existing `alert-success` / `alert-error` styles. No new CSS classes, so no Tailwind rebuild is required.
## Tests (`internal/handlers/profile_test.go`)
- `TestHandlePasswordChange_Success`: seeds a user, posts a valid change, asserts success message and that the stored hash changed and verifies against the new password.
- `TestHandlePasswordChange_WrongCurrentPassword`: posts a wrong current password, asserts the rejection message and that the stored hash is unchanged.
Validated with `docker build .` (fmt-check, lint, test, build) — exit 0.
Closes #65
Co-authored-by: sneak <sneak@sneak.berlin>
Co-authored-by: Jeffrey Paul <sneak@noreply.example.org>
Reviewed-on: #83
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
107 lines
3.8 KiB
HTML
107 lines
3.8 KiB
HTML
{{template "base" .}}
|
|
|
|
{{define "title"}}Profile - Webhooker{{end}}
|
|
|
|
{{define "content"}}
|
|
<div class="max-w-4xl mx-auto px-6 py-12">
|
|
<h1 class="text-2xl font-medium text-gray-900 mb-6">User Profile</h1>
|
|
|
|
{{if .SuccessMessage}}
|
|
<div class="alert-success">
|
|
<span>{{.SuccessMessage}}</span>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .ErrorMessage}}
|
|
<div class="alert-error">
|
|
<span>{{.ErrorMessage}}</span>
|
|
</div>
|
|
{{end}}
|
|
|
|
<div class="card p-6">
|
|
<div class="flex items-center mb-6">
|
|
<div class="mr-4">
|
|
<svg class="w-16 h-16 text-primary-500" fill="currentColor" viewBox="0 0 16 16">
|
|
<path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
|
|
<path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z"/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h2 class="text-xl font-medium text-gray-900">{{.User.Username}}</h2>
|
|
<p class="text-sm text-gray-500">User ID: {{.User.ID}}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="border-gray-200 mb-6">
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
<div>
|
|
<h3 class="text-lg font-medium text-gray-900 mb-3">Account Information</h3>
|
|
<dl class="space-y-3">
|
|
<div class="flex">
|
|
<dt class="w-32 text-sm font-medium text-gray-500">Username</dt>
|
|
<dd class="text-sm text-gray-900">{{.User.Username}}</dd>
|
|
</div>
|
|
<div class="flex">
|
|
<dt class="w-32 text-sm font-medium text-gray-500">Account Type</dt>
|
|
<dd class="text-sm text-gray-900">Standard User</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-lg font-medium text-gray-900 mb-3">Settings</h3>
|
|
<p class="text-sm text-gray-500">Profile settings and preferences will be available here.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card p-6 mt-6">
|
|
<h3 class="text-lg font-medium text-gray-900 mb-3">Change Password</h3>
|
|
<form method="POST" action="/user/{{.User.Username}}/password" class="space-y-6">
|
|
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
|
|
<div class="form-group">
|
|
<label for="current_password" class="label">Current Password</label>
|
|
<input
|
|
type="password"
|
|
id="current_password"
|
|
name="current_password"
|
|
required
|
|
autocomplete="current-password"
|
|
class="input"
|
|
>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="new_password" class="label">New Password</label>
|
|
<input
|
|
type="password"
|
|
id="new_password"
|
|
name="new_password"
|
|
required
|
|
autocomplete="new-password"
|
|
class="input"
|
|
>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="confirm_password" class="label">Confirm New Password</label>
|
|
<input
|
|
type="password"
|
|
id="confirm_password"
|
|
name="confirm_password"
|
|
required
|
|
autocomplete="new-password"
|
|
class="input"
|
|
>
|
|
</div>
|
|
|
|
<button type="submit" class="btn-primary">Change Password</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="mt-6">
|
|
<a href="/" class="btn-secondary">Back to Home</a>
|
|
</div>
|
|
</div>
|
|
{{end}}
|