diff --git a/web/.woodpecker.yml b/web/.woodpecker.yml
index 1fd98ba..40966cc 100644
--- a/web/.woodpecker.yml
+++ b/web/.woodpecker.yml
@@ -29,7 +29,7 @@ steps:
from_secret: registry_password
build_args:
- PUBLIC_API_BASE_URL=https://api.marktvogt.de
- - PUBLIC_TURNSTILE_SITE_KEY=0x4AAAAAAACjLCV-78Q1loTPz
+ - PUBLIC_TURNSTILE_SITE_KEY=0x4AAAAAACjLCV-78Ql1oTPz
when:
- event: push
branch: main
diff --git a/web/deploy/helm/values.yaml b/web/deploy/helm/values.yaml
index 5efe563..c50f78d 100644
--- a/web/deploy/helm/values.yaml
+++ b/web/deploy/helm/values.yaml
@@ -62,7 +62,7 @@ config:
PORT: "3000"
HOST: "0.0.0.0"
# Cloudflare Turnstile — read at runtime via $env/dynamic/public
- PUBLIC_TURNSTILE_SITE_KEY: "0x4AAAAAAACjLCV-78Q1loTPz"
+ PUBLIC_TURNSTILE_SITE_KEY: "0x4AAAAAACjLCV-78Ql1oTPz"
nodeSelector: {}
tolerations: []
diff --git a/web/src/lib/api/types.ts b/web/src/lib/api/types.ts
index a1ec59f..40d3b99 100644
--- a/web/src/lib/api/types.ts
+++ b/web/src/lib/api/types.ts
@@ -94,6 +94,7 @@ export interface ProfileData {
display_name: string;
avatar_url: string;
role: string;
+ has_password: boolean;
created_at: string;
}
diff --git a/web/src/lib/components/layout/Header.svelte b/web/src/lib/components/layout/Header.svelte
index a0d4886..46a7474 100644
--- a/web/src/lib/components/layout/Header.svelte
+++ b/web/src/lib/components/layout/Header.svelte
@@ -1,6 +1,7 @@
+
+
+
+
+
+ {#if open}
+
+ {/if}
+
diff --git a/web/src/routes/profile/+page.server.ts b/web/src/routes/profile/+page.server.ts
index b5974fd..32a42bd 100644
--- a/web/src/routes/profile/+page.server.ts
+++ b/web/src/routes/profile/+page.server.ts
@@ -38,6 +38,38 @@ export const actions: Actions = {
}
},
+ password: async ({ request, cookies, fetch }) => {
+ const form = await request.formData();
+ const currentPassword = form.get('current_password') as string;
+ const newPassword = form.get('new_password') as string;
+ const confirmPassword = form.get('confirm_password') as string;
+
+ if (!newPassword || newPassword.length < 8) {
+ return fail(400, { error: 'Passwort muss mindestens 8 Zeichen lang sein.' });
+ }
+
+ if (newPassword !== confirmPassword) {
+ return fail(400, { error: 'Passwörter stimmen nicht überein.' });
+ }
+
+ const body: Record = { new_password: newPassword };
+ if (currentPassword) body.current_password = currentPassword;
+
+ try {
+ await serverFetch('/auth/password', cookies, {
+ method: 'PUT',
+ body: JSON.stringify(body),
+ fetch
+ });
+ return { success: 'Passwort aktualisiert.' };
+ } catch (e) {
+ if (e instanceof ApiClientError) {
+ return fail(e.status, { error: e.message });
+ }
+ return fail(500, { error: 'Ein Fehler ist aufgetreten.' });
+ }
+ },
+
delete: async ({ cookies, fetch }) => {
try {
await serverFetch('/users/me', cookies, {
diff --git a/web/src/routes/profile/+page.svelte b/web/src/routes/profile/+page.svelte
index 98a1fc1..c571d17 100644
--- a/web/src/routes/profile/+page.svelte
+++ b/web/src/routes/profile/+page.svelte
@@ -8,6 +8,7 @@
let showDeleteConfirm = $state(false);
let updateLoading = $state(false);
+ let passwordLoading = $state(false);
let deleteLoading = $state(false);
@@ -66,12 +67,49 @@