From e84c022bcfb50b84b452963fa97010817e45ef1a Mon Sep 17 00:00:00 2001
From: Pascal Syma
Date: Thu, 14 Aug 2025 21:31:48 +0200
Subject: [PATCH] Add updatable profile
---
TODO.md | 2 +-
app/pages/profile.vue | 262 ++++++++++++++++++++++++++++++-
server/api/auth/password.post.ts | 44 ++++++
server/api/auth/profile.get.ts | 42 +++++
server/api/auth/profile.post.ts | 38 +++++
5 files changed, 386 insertions(+), 2 deletions(-)
create mode 100644 server/api/auth/password.post.ts
create mode 100644 server/api/auth/profile.get.ts
create mode 100644 server/api/auth/profile.post.ts
diff --git a/TODO.md b/TODO.md
index 98fd626..680c489 100644
--- a/TODO.md
+++ b/TODO.md
@@ -2,7 +2,7 @@
- **Auth**
- [x] Flawless! Login (Email+Pw ohne bestätigung?)
- [x] Default random name
- - [ ] Change display name
+ - [x] Change display name
- **Hunt Admin**
- [x] Start/Stop
- [x] Add Reviews/Score
diff --git a/app/pages/profile.vue b/app/pages/profile.vue
index aaea34e..1d20bc7 100644
--- a/app/pages/profile.vue
+++ b/app/pages/profile.vue
@@ -2,10 +2,270 @@
definePageMeta({
middleware: ['auth']
});
+
+const router = useRouter();
+const { t } = useI18n();
+const { loggedIn, fetch } = useUserSession();
+const localePath = useLocalePath();
+const validation = useValidation();
+watch(loggedIn, (isLoggedIn) => {
+ if (!isLoggedIn) {
+ router.push(localePath('/login'));
+ }
+});
+const { data, refresh, pending } = await useFetch('/api/auth/profile');
+
+const form = reactive({
+ email: data.value!.email,
+ name: data.value!.name
+});
+const uploading = ref(false);
+const changed = computed(
+ () => form.name !== data.value?.name || form.email !== data.value?.email
+);
+
+const profileError = ref('');
+async function submitProfile() {
+ uploading.value = true;
+ profileError.value = '';
+ try {
+ await $fetch(`/api/auth/profile`, {
+ method: 'POST',
+ body: form
+ });
+
+ await refresh();
+ form.email = data.value!.email;
+ form.name = data.value!.name;
+ await fetch();
+ } catch (e) {
+ if (e instanceof Error && 'statusCode' in e && e.statusCode === 409) {
+ // ToDo: i18n
+ profileError.value = 'Email already exists!';
+ } else {
+ profileError.value = parseError(e);
+ }
+ }
+
+ uploading.value = false;
+}
+
+const password = reactive({
+ password: '',
+ old: ''
+});
+
+const passwordError = ref('');
+async function submitPassword() {
+ uploading.value = true;
+ passwordError.value = '';
+ try {
+ await $fetch(`/api/auth/password`, {
+ method: 'POST',
+ body: password
+ });
+
+ password.password = '';
+ password.old = '';
+ } catch (e) {
+ if (e instanceof Error && 'statusCode' in e && e.statusCode === 404) {
+ // ToDo: i18n
+ passwordError.value = 'Old password incorrect!';
+ } else {
+ passwordError.value = parseError(e);
+ }
+ }
+
+ uploading.value = false;
+}
- profile
+
+
+ Public profile
+
+
+
+
+ {{ t(profileError) }}
+ Update
+
+
+
+
+ Change password
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t(profileError) }}
+ Update
+
+
+
+
+ Uploaded pictures
+
+
+
+
+ {{ item.answer.quest.title }} —
+ {{ item.answer.quest.hunt.name }}
+
+
+
+
+
diff --git a/server/api/auth/password.post.ts b/server/api/auth/password.post.ts
new file mode 100644
index 0000000..30f21e6
--- /dev/null
+++ b/server/api/auth/password.post.ts
@@ -0,0 +1,44 @@
+import { hash, verify } from 'argon2';
+import { useValidatedBody, z } from 'h3-zod';
+import prisma from '~~/lib/prisma';
+
+export default defineEventHandler(async (event) => {
+ const { user } = await requireUserSession(event);
+
+ const { old, password } = await useValidatedBody(
+ event,
+ z.object({
+ password: z.string().min(8),
+ old: z.string().min(8)
+ })
+ );
+
+ const dbUser = await prisma.user.findUniqueOrThrow({
+ where: {
+ deletedAt: null,
+ id: user.id
+ },
+ select: {
+ id: true,
+ password: true
+ }
+ });
+ if (!(await verify(dbUser.password, old))) {
+ throw createError({ status: 404, statusText: 'Not found!' });
+ }
+
+ const passwordHash = await hash(password);
+
+ await prisma.user.update({
+ where: {
+ id: user.id
+ },
+ data: {
+ password: passwordHash
+ }
+ });
+
+ console.warn(`Successful changed password for user ${user.id}`);
+
+ return { ok: true };
+});
diff --git a/server/api/auth/profile.get.ts b/server/api/auth/profile.get.ts
new file mode 100644
index 0000000..e640a76
--- /dev/null
+++ b/server/api/auth/profile.get.ts
@@ -0,0 +1,42 @@
+import prisma from '~~/lib/prisma';
+
+export default defineEventHandler(async (event) => {
+ const user = await requireUserSession(event);
+
+ return prisma.user.findUnique({
+ where: { id: user.user.id },
+ select: {
+ name: true,
+ email: true,
+ uploadedFiles: {
+ select: {
+ url: true,
+ createdAt: true,
+ answer: {
+ select: {
+ id: true,
+ quest: {
+ select: {
+ id: true,
+ title: true,
+ hunt: {
+ select: {
+ id: true,
+ name: true
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+
+ where: {
+ NOT: {
+ answerId: null
+ }
+ }
+ }
+ }
+ });
+});
diff --git a/server/api/auth/profile.post.ts b/server/api/auth/profile.post.ts
new file mode 100644
index 0000000..fcaf7a2
--- /dev/null
+++ b/server/api/auth/profile.post.ts
@@ -0,0 +1,38 @@
+import { useValidatedBody, z } from 'h3-zod';
+import prisma from '~~/lib/prisma';
+
+export default defineEventHandler(async (event) => {
+ const user = await requireUserSession(event);
+
+ const data = await useValidatedBody(
+ event,
+ z.object({
+ name: z.string().min(3).max(256).trim(),
+ email: z.email().max(256).trim()
+ })
+ );
+
+ try {
+ const dbUser = await prisma.user.update({
+ where: {
+ id: user.user.id
+ },
+ data,
+ select: {
+ id: true,
+ name: true,
+ role: true
+ }
+ });
+ await setUserSession(event, {
+ user: dbUser,
+ loggedInAt: user.loggedInAt
+ });
+ } catch (error) {
+ throw createError({
+ status: 409,
+ statusText: 'User with that email already exists!'
+ });
+ }
+ return { ok: true };
+});