From a27ae4f24aac42e0c5be23bd8ba9aa889f2a0c03 Mon Sep 17 00:00:00 2001 From: hywax Date: Mon, 5 Aug 2024 16:20:39 +0500 Subject: [PATCH] chore: create account change password form --- .../components/account/ChangePasswordForm.vue | 85 +++++++++++++++++++ ...ULocaleSelector.vue => LocaleSelector.vue} | 0 apps/web/app/layouts/default.vue | 2 +- apps/web/app/locales/en-US.ts | 25 ++++++ apps/web/app/locales/ru-RU.ts | 25 ++++++ apps/web/app/pages/account/security.vue | 4 +- apps/web/app/schema/account.ts | 11 +++ apps/web/app/schema/index.ts | 1 + apps/web/package.json | 1 + .../server/api/account/change-password.put.ts | 20 +++++ apps/web/server/constants/errors.ts | 3 + apps/web/server/middleware/01.access.ts | 1 + apps/web/server/services/account.ts | 39 +++++++++ apps/web/server/utils/errors.ts | 5 ++ pnpm-lock.yaml | 3 + 15 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 apps/web/app/components/account/ChangePasswordForm.vue rename apps/web/app/components/ui/i18n/{ULocaleSelector.vue => LocaleSelector.vue} (100%) create mode 100644 apps/web/app/schema/account.ts create mode 100644 apps/web/server/api/account/change-password.put.ts create mode 100644 apps/web/server/services/account.ts diff --git a/apps/web/app/components/account/ChangePasswordForm.vue b/apps/web/app/components/account/ChangePasswordForm.vue new file mode 100644 index 0000000..7010284 --- /dev/null +++ b/apps/web/app/components/account/ChangePasswordForm.vue @@ -0,0 +1,85 @@ + + + diff --git a/apps/web/app/components/ui/i18n/ULocaleSelector.vue b/apps/web/app/components/ui/i18n/LocaleSelector.vue similarity index 100% rename from apps/web/app/components/ui/i18n/ULocaleSelector.vue rename to apps/web/app/components/ui/i18n/LocaleSelector.vue diff --git a/apps/web/app/layouts/default.vue b/apps/web/app/layouts/default.vue index c09b4a6..8e987b9 100644 --- a/apps/web/app/layouts/default.vue +++ b/apps/web/app/layouts/default.vue @@ -5,7 +5,7 @@
diff --git a/apps/web/app/locales/en-US.ts b/apps/web/app/locales/en-US.ts index adb4212..db70c6e 100644 --- a/apps/web/app/locales/en-US.ts +++ b/apps/web/app/locales/en-US.ts @@ -25,6 +25,8 @@ export default { 400000: 'Bad request', 400001: 'Invalid user data', 400002: 'Invalid email or password', + 400003: 'Invalid password data for change', + 400004: 'Passwords do not match', 401000: 'Unauthorized', 403000: 'Forbidden', 403001: 'Access only for unauthorized users', @@ -32,6 +34,7 @@ export default { 403003: 'Access only for admin', 403004: 'Registration is disabled', 404000: 'Not found', + 404001: 'User not found', 409001: 'User with this email already exists', 500000: 'Internal server error', 501000: 'Not implemented', @@ -46,6 +49,28 @@ export default { settings: 'Settings', signOut: 'Sign Out', }, + changePassword: { + title: 'Change Password', + description: 'Update your password to keep your account secure.', + form: { + currentPassword: { + label: 'Current Password', + placeholder: 'Enter your current password', + }, + newPassword: { + label: 'New Password', + placeholder: 'Enter your new password', + }, + confirmPassword: { + label: 'Confirm Password', + placeholder: 'Enter your new password again', + }, + action: { + change: 'Change Password', + changed: 'Password changed successfully', + }, + }, + }, }, auth: { login: { diff --git a/apps/web/app/locales/ru-RU.ts b/apps/web/app/locales/ru-RU.ts index 406902e..df91f71 100644 --- a/apps/web/app/locales/ru-RU.ts +++ b/apps/web/app/locales/ru-RU.ts @@ -25,6 +25,8 @@ export default { 400000: 'Неверный запрос', 400001: 'Неверные данные пользователя', 400002: 'Неверный email или пароль', + 400003: 'Неверные данные для смены пароля', + 400004: 'Пароли не совпадают', 401000: 'Не авторизован', 403000: 'Доступ запрещен', 403001: 'Доступ только для неавторизованных пользователей', @@ -32,6 +34,7 @@ export default { 403003: 'Доступ только для администраторов', 403004: 'Регистрация отключена', 404000: 'Страница не найдена', + 404001: 'Пользователь не найден', 409001: 'Пользователь с таким email уже существует', 500000: 'Внутренняя ошибка сервера', 501000: 'Не реализовано', @@ -46,6 +49,28 @@ export default { settings: 'Настройки', signOut: 'Выйти', }, + changePassword: { + title: 'Изменить пароль', + description: 'Обновите свой пароль, чтобы обеспечить безопасность вашего аккаунта.', + form: { + currentPassword: { + label: 'Текущий пароль', + placeholder: 'Введите ваш текущий пароль', + }, + newPassword: { + label: 'Новый пароль', + placeholder: 'Введите ваш новый пароль', + }, + confirmPassword: { + label: 'Подтвердите пароль', + placeholder: 'Введите ваш новый пароль еще раз', + }, + action: { + change: 'Изменить пароль', + changed: 'Пароль успешно изменен', + }, + }, + }, }, auth: { login: { diff --git a/apps/web/app/pages/account/security.vue b/apps/web/app/pages/account/security.vue index c06131c..e505633 100644 --- a/apps/web/app/pages/account/security.vue +++ b/apps/web/app/pages/account/security.vue @@ -1,6 +1,6 @@ diff --git a/apps/web/app/schema/account.ts b/apps/web/app/schema/account.ts new file mode 100644 index 0000000..0bf63ec --- /dev/null +++ b/apps/web/app/schema/account.ts @@ -0,0 +1,11 @@ +import { z } from 'zod' + +export const accountChangePasswordSchema = z.object({ + currentPassword: z.string().min(6), + newPassword: z.string().min(6), + confirmPassword: z.string().min(6), +}).refine((data) => data.newPassword === data.confirmPassword, { + path: ['confirmPassword'], +}) + +export type AccountChangePasswordSchema = z.input diff --git a/apps/web/app/schema/index.ts b/apps/web/app/schema/index.ts index f140b2e..17539d5 100644 --- a/apps/web/app/schema/index.ts +++ b/apps/web/app/schema/index.ts @@ -1 +1,2 @@ export * from './auth' +export * from './account' diff --git a/apps/web/package.json b/apps/web/package.json index dfd2241..eca2f92 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -32,6 +32,7 @@ "nanoid": "^5.0.7", "nuxt-auth-utils": "^0.3.2", "nuxt-zod-i18n": "^1.9.0", + "scule": "^1.3.0", "vue": "^3.4.33", "zod": "^3.23.8" }, diff --git a/apps/web/server/api/account/change-password.put.ts b/apps/web/server/api/account/change-password.put.ts new file mode 100644 index 0000000..9b2c66b --- /dev/null +++ b/apps/web/server/api/account/change-password.put.ts @@ -0,0 +1,20 @@ +import { ERROR_ACCOUNT_PASSWORD_INVALID_DATA, ERROR_ACCOUNT_PASSWORD_NOT_MATCH } from '#constants/errors' +import { accountChangePassword } from '#services/account' + +export default defineEventHandler(async (event) => { + try { + const data = await readBody(event) + const session = await getUserSession(event) + + await accountChangePassword({ + userId: session.user!.id, + currentPassword: data.currentPassword, + newPassword: data.newPassword, + }) + } catch (e) { + throw errorResolver(e, { + ZOD: ERROR_ACCOUNT_PASSWORD_INVALID_DATA, + ERROR_INVALID_PASSWORD: ERROR_ACCOUNT_PASSWORD_NOT_MATCH, + }) + } +}) diff --git a/apps/web/server/constants/errors.ts b/apps/web/server/constants/errors.ts index 01cfe08..58a663f 100644 --- a/apps/web/server/constants/errors.ts +++ b/apps/web/server/constants/errors.ts @@ -1,6 +1,8 @@ export const ERROR_BAD_REQUEST = '400000: Bad request' export const ERROR_USER_INVALID_DATA = '400001: Invalid user data' export const ERROR_USER_INVALID_CREDENTIALS = '400002: Invalid email or password' +export const ERROR_ACCOUNT_PASSWORD_INVALID_DATA = '400003: Invalid password data' +export const ERROR_ACCOUNT_PASSWORD_NOT_MATCH = '400004: Passwords do not match' export const ERROR_UNAUTHORIZED = '401000: Unauthorized' export const ERROR_FORBIDDEN = '403000: Forbidden' export const ERROR_ACCESS_ONLY_GUEST = '403001: Access only for guest' @@ -8,6 +10,7 @@ export const ERROR_ACCESS_ONLY_AUTHORIZED = '403002: Access only for authorized export const ERROR_ACCESS_ONLY_ADMIN = '403003: Access only for admin' export const ERROR_REGISTRATION_DISABLED = '403004: Registration is disabled' export const ERROR_NOT_FOUND = '404000: Not found' +export const ERROR_USER_NOT_FOUND = '404001: User not found' export const ERROR_USER_ALL_READY_EXISTS = '409001: User with this email already exists' export const ERROR_INTERNAL_ERROR = '500000: Internal server error' export const ERROR_NOT_IMPLEMENTED = '501000: Not implemented' diff --git a/apps/web/server/middleware/01.access.ts b/apps/web/server/middleware/01.access.ts index de16103..663ee1d 100644 --- a/apps/web/server/middleware/01.access.ts +++ b/apps/web/server/middleware/01.access.ts @@ -7,6 +7,7 @@ export default defineEventHandler(async (event) => { ] const protectedRoutes: string[] = [ '/api/users*', + '/api/account*', ] const session = await getUserSession(event) diff --git a/apps/web/server/services/account.ts b/apps/web/server/services/account.ts new file mode 100644 index 0000000..fb90f00 --- /dev/null +++ b/apps/web/server/services/account.ts @@ -0,0 +1,39 @@ +import { z } from 'zod' +import bcrypt from 'bcrypt' +import { eq } from 'drizzle-orm' +import { tables, useDatabase } from '#db' +import type { User } from '#db' + +interface AccountChangePasswordData { + userId: User['id'] + currentPassword: string + newPassword: string +} + +export async function accountChangePassword(data: AccountChangePasswordData): Promise { + const db = useDatabase() + const changePasswordSchema = z.object({ + userId: z.string(), + currentPassword: z.string().min(6), + newPassword: z.string().min(6), + }) + + const { userId, currentPassword, newPassword } = changePasswordSchema.parse(data) + const user = await db.query.users.findFirst({ + where: (user, { eq }) => eq(user.id, userId), + }) + + if (!user) { + throw new Error('User not found') + } + + const isMatch = await bcrypt.compare(currentPassword, user.password) + + if (!isMatch) { + throw new Error('Invalid password') + } + + await db.update(tables.users) + .set({ password: await bcrypt.hash(newPassword, 10) }) + .where(eq(tables.users.id, userId)) +} diff --git a/apps/web/server/utils/errors.ts b/apps/web/server/utils/errors.ts index 3789bff..d15b863 100644 --- a/apps/web/server/utils/errors.ts +++ b/apps/web/server/utils/errors.ts @@ -51,6 +51,11 @@ function getErrorCode(exception: unknown, codes: ErrorMapCodes): string { errorString = codesMap.ZOD } else if (exception instanceof SqliteError) { errorString = Object.hasOwn(codesMap, exception.code) ? codesMap[exception.code]! : codesMap.SQLITE + } else if (exception instanceof Error) { + const normalizedMessage = exception.message.trim().replaceAll(' ', '_').toUpperCase() + const errorMaybeCode = `ERROR_${normalizedMessage}` + + errorString = Object.hasOwn(codesMap, errorMaybeCode) ? codesMap[errorMaybeCode]! : codesMap.DEFAULT } else { errorString = codesMap.DEFAULT } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c24ac4..37d0f92 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,6 +77,9 @@ importers: nuxt-zod-i18n: specifier: ^1.9.0 version: 1.9.0(magicast@0.3.4)(rollup@4.19.0) + scule: + specifier: ^1.3.0 + version: 1.3.0 vue: specifier: ^3.4.33 version: 3.4.33(typescript@5.5.3)