diff --git a/src/app/components/form/PasswordField.vue b/src/app/components/form/PasswordField.vue new file mode 100644 index 00000000..e2bca565 --- /dev/null +++ b/src/app/components/form/PasswordField.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/app/components/form/TextField.vue b/src/app/components/form/TextField.vue index f3271074..6a1a79fa 100644 --- a/src/app/components/form/TextField.vue +++ b/src/app/components/form/TextField.vue @@ -4,7 +4,7 @@ {{ $t('me.sectionGeneral') }} - + @@ -26,12 +26,12 @@ Address diff --git a/src/app/pages/me.vue b/src/app/pages/me.vue index a3bb970d..bbe2989e 100644 --- a/src/app/pages/me.vue +++ b/src/app/pages/me.vue @@ -5,116 +5,126 @@ -
-

- {{ $t('me.sectionGeneral') }} -

- - - - - - -
- {{ $t('save') }} -
-
-
-

- {{ $t('me.sectionPassword') }} -

- - - - - - -
- {{ - $t('updatePassword') - }} -
-
+ + + {{ $t('me.sectionGeneral') }} + + + + + + + + {{ $t('me.sectionPassword') }} + + + + + +
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 62083d46..3d40afe5 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -61,7 +61,13 @@ "passwordNumber": "Password must have at least 1 number", "passwordSpecial": "Password must have at least 1 special character", "remember": "Remember must be a valid boolean", - "accept": "Please accept the condition" + "accept": "Please accept the condition", + "name": "Name must be a valid string", + "nameMin": "Name must be at least 1 Character", + "email": "Email must be a valid string", + "emailMin": "Email must be at least 1 Character", + "emailInvalid": "Email must be a valid email", + "passwordMatch": "Passwords must match" }, "userConfig": { "host": "Host must be a valid string", diff --git a/src/server/api/me/index.post.ts b/src/server/api/me/index.post.ts new file mode 100644 index 00000000..324b4552 --- /dev/null +++ b/src/server/api/me/index.post.ts @@ -0,0 +1,13 @@ +import { UserUpdateSchema } from '#db/repositories/user/types'; + +export default definePermissionEventHandler( + actions.CLIENT, + async ({ event, user }) => { + const { name, email } = await readValidatedBody( + event, + validateZod(UserUpdateSchema) + ); + await Database.users.update(user.id, name, email); + return { success: true }; + } +); diff --git a/src/server/api/me/password.post.ts b/src/server/api/me/password.post.ts new file mode 100644 index 00000000..50a593a4 --- /dev/null +++ b/src/server/api/me/password.post.ts @@ -0,0 +1,13 @@ +import { UserUpdatePasswordSchema } from '#db/repositories/user/types'; + +export default definePermissionEventHandler( + actions.CLIENT, + async ({ event, user }) => { + const { newPassword, currentPassword } = await readValidatedBody( + event, + validateZod(UserUpdatePasswordSchema) + ); + await Database.users.updatePassword(user.id, currentPassword, newPassword); + return { success: true }; + } +); diff --git a/src/server/api/setup/4.post.ts b/src/server/api/setup/4.post.ts index 0dd73670..4c8eb82a 100644 --- a/src/server/api/setup/4.post.ts +++ b/src/server/api/setup/4.post.ts @@ -1,9 +1,9 @@ -import { UserSetupType } from '#db/repositories/user/types'; +import { UserSetupSchema } from '#db/repositories/user/types'; export default defineSetupEventHandler(async ({ event }) => { const { username, password } = await readValidatedBody( event, - validateZod(UserSetupType, event) + validateZod(UserSetupSchema, event) ); await Database.users.create(username, password); diff --git a/src/server/api/setup/5.post.ts b/src/server/api/setup/5.post.ts index 1e0b36d6..b62d42d9 100644 --- a/src/server/api/setup/5.post.ts +++ b/src/server/api/setup/5.post.ts @@ -1,9 +1,9 @@ -import { UserConfigSetupType } from '#db/repositories/userConfig/types'; +import { UserConfigSetupSchema } from '#db/repositories/userConfig/types'; export default defineSetupEventHandler(async ({ event }) => { const { host, port } = await readValidatedBody( event, - validateZod(UserConfigSetupType, event) + validateZod(UserConfigSetupSchema, event) ); await Database.userConfigs.updateHostPort('wg0', host, port); await Database.general.setSetupStep(0); diff --git a/src/server/database/repositories/user/service.ts b/src/server/database/repositories/user/service.ts index 209fe99d..dd92287c 100644 --- a/src/server/database/repositories/user/service.ts +++ b/src/server/database/repositories/user/service.ts @@ -14,6 +14,14 @@ function createPreparedStatement(db: DBType) { where: eq(user.username, sql.placeholder('username')), }) .prepare(), + update: db + .update(user) + .set({ + name: sql.placeholder('name') as never as string, + email: sql.placeholder('email') as never as string, + }) + .where(eq(user.id, sql.placeholder('id'))) + .prepare(), }; } @@ -42,7 +50,11 @@ export class UserService { const hash = await hashPassword(password); return this.#db.transaction(async (tx) => { - const oldUser = await this.getByUsername(username); + const oldUser = await tx.query.user + .findFirst({ + where: eq(user.username, username), + }) + .execute(); if (oldUser) { throw new Error('User already exists'); @@ -60,4 +72,38 @@ export class UserService { }); }); } + + async update(id: ID, name: string, email: string | null) { + return this.#statements.update.execute({ id, name, email }); + } + + async updatePassword(id: ID, currentPassword: string, newPassword: string) { + const hash = await hashPassword(newPassword); + + return this.#db.transaction(async (tx) => { + // get user again to avoid password changing while request + const txUser = await tx.query.user + .findFirst({ where: eq(user.id, id) }) + .execute(); + + if (!txUser) { + throw new Error('User not found'); + } + + const passwordValid = await isPasswordValid( + currentPassword, + txUser.password + ); + + if (!passwordValid) { + throw new Error('Invalid password'); + } + + await tx + .update(user) + .set({ password: hash }) + .where(eq(user.id, id)) + .execute(); + }); + } } diff --git a/src/server/database/repositories/user/types.ts b/src/server/database/repositories/user/types.ts index 7b629365..5837b19e 100644 --- a/src/server/database/repositories/user/types.ts +++ b/src/server/database/repositories/user/types.ts @@ -20,6 +20,18 @@ const password = z const remember = z.boolean({ message: 'zod.user.remember' }); +const name = z + .string({ message: 'zod.user.name' }) + .min(1, 'zod.user.nameMin') + .pipe(safeStringRefine); + +const email = z + .string({ message: 'zod.user.email' }) + .min(5, 'zod.user.emailMin') + .email({ message: 'zod.user.emailInvalid' }) + .pipe(safeStringRefine) + .nullable(); + export const UserLoginSchema = z.object( { username: username, @@ -33,7 +45,7 @@ const accept = z.boolean().refine((val) => val === true, { message: 'zod.user.accept', }); -export const UserSetupType = z.object( +export const UserSetupSchema = z.object( { username: username, password: password, @@ -41,3 +53,24 @@ export const UserSetupType = z.object( }, { message: objectMessage } ); + +export const UserUpdateSchema = z.object( + { + name: name, + email: email, + }, + { message: objectMessage } +); + +export const UserUpdatePasswordSchema = z + .object( + { + currentPassword: password, + newPassword: password, + confirmPassword: password, + }, + { message: objectMessage } + ) + .refine((val) => val.newPassword === val.confirmPassword, { + message: 'zod.user.passwordMatch', + }); diff --git a/src/server/database/repositories/userConfig/types.ts b/src/server/database/repositories/userConfig/types.ts index 165f3f27..89e59ef2 100644 --- a/src/server/database/repositories/userConfig/types.ts +++ b/src/server/database/repositories/userConfig/types.ts @@ -9,7 +9,7 @@ const host = z .min(1, 'zod.userConfig.hostMin') .pipe(safeStringRefine); -export const UserConfigSetupType = z.object({ +export const UserConfigSetupSchema = z.object({ host: host, port: PortSchema, });