diff --git a/.docker/docker-compose.yml b/.docker/docker-compose.yml index d69973d..4c1aefd 100644 --- a/.docker/docker-compose.yml +++ b/.docker/docker-compose.yml @@ -67,7 +67,7 @@ services: volumes: - "./assets/garage-init.sh:/etc/init.sh" account: - image: ghcr.io/pretendonetwork/account:sha-34dc075 + image: ghcr.io/pretendonetwork/account:sha-b857d73 restart: unless-stopped networks: - net diff --git a/package-lock.json b/package-lock.json index 79e816b..b0a670a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@nuxtjs/i18n": "^10.6.0", "@pinia/nuxt": "^1.0.1", "@pretendonetwork/error-codes": "^1.2.2", - "@pretendonetwork/grpc": "^2.5.10", + "@pretendonetwork/grpc": "^2.6.1", "@pretendonetwork/mii-js": "^1.0.11", "@vueuse/core": "^14.4.0", "better-sqlite3": "^12.11.1", @@ -5347,9 +5347,9 @@ } }, "node_modules/@pretendonetwork/grpc": { - "version": "2.5.10", - "resolved": "https://registry.npmjs.org/@pretendonetwork/grpc/-/grpc-2.5.10.tgz", - "integrity": "sha512-1nd/nsRU+tdi08O6fHFuYvUJqByJh/qToQxvsDgWonwZLR/RsBFi3o2MU1/4ketbeGu2KmStLN7vBLDcQ+c44w==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@pretendonetwork/grpc/-/grpc-2.6.1.tgz", + "integrity": "sha512-+HKJ8cTV4AV8PJgUqg2Tocz4EJTajqErY9r/Oul8WIZM2lO+0gRPSf3Mlxi9Ghvjq88ZS9pWWccAKtn77ePrew==", "license": "AGPL-3.0-only", "dependencies": { "@bufbuild/protobuf": "^2.2.2", diff --git a/package.json b/package.json index dafd3cf..85d5255 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@nuxtjs/i18n": "^10.6.0", "@pinia/nuxt": "^1.0.1", "@pretendonetwork/error-codes": "^1.2.2", - "@pretendonetwork/grpc": "^2.5.10", + "@pretendonetwork/grpc": "^2.6.1", "@pretendonetwork/mii-js": "^1.0.11", "@vueuse/core": "^14.4.0", "better-sqlite3": "^12.11.1", diff --git a/server/api/account/update-password.patch.ts b/server/api/account/update-password.patch.ts new file mode 100644 index 0000000..008cf95 --- /dev/null +++ b/server/api/account/update-password.patch.ts @@ -0,0 +1,43 @@ +import { ClientError } from 'nice-grpc'; +import { PasswordUpdateSchema } from '~~/shared/api-types'; +import type { ApiErrorCodes } from '~~/shared/errors'; + +const bucket = createRatelimitBucket({ + id: 'update-password', + points: 15, + durationSec: 5 * 60, // 5 minutes + blockDurationSec: 1 * 60 * 60 // 1 hour +}); + +const errors: Record = { + 'INVALID_ARGUMENT: Password must be between 6 and 16 characters long': 'PASSWORD_INVALID_LENGTH', + 'INVALID_ARGUMENT: Password cannot be the same as username': 'PASSWORD_NOT_USERNAME', + 'INVALID_ARGUMENT: Password must have combination of letters, numbers, and/or punctuation characters': 'PASSWORD_NEEDS_CHARS', + 'INVALID_ARGUMENT: Password may not have 3 repeating characters': 'PASSWORD_REPEATED_CHARS', + 'INVALID_ARGUMENT: Passwords do not match': 'PASSWORDS_DO_NOT_MATCH', + 'INVALID_ARGUMENT: Password is incorrect': 'INVALID_PASSWORD' +}; + +export default defineEventHandler(async (event): Promise => { + await enforceRatelimit(event, bucket); + + const body = await readZodBody(event, PasswordUpdateSchema); + const auth = enforceLoggedIn(event); + const grpc = useApiGrpcWithToken(event, auth.token); + + try { + await grpc.updatePassword({ + oldPassword: body.oldPassword, + newPassword: body.newPassword, + newPasswordConfirm: body.newPasswordConfirm + }); + } catch (error: unknown) { + if (error instanceof ClientError) { + const errorCode = errors[error.details]; + if (errorCode) { + throw createApiError(errorCode); + } + } + throw error; + } +}); diff --git a/server/api/auth/register.post.ts b/server/api/auth/register.post.ts index c6061ec..e443307 100644 --- a/server/api/auth/register.post.ts +++ b/server/api/auth/register.post.ts @@ -26,6 +26,7 @@ const errors: Record = { 'INVALID_ARGUMENT: Password must have combination of letters, numbers, and/or punctuation characters': 'PASSWORD_NEEDS_CHARS', 'INVALID_ARGUMENT: Password may not have 3 repeating characters': 'PASSWORD_REPEATED_CHARS', 'INVALID_ARGUMENT: Passwords do not match': 'PASSWORDS_DO_NOT_MATCH' + }; function getCutoffDateForAge(today: Date, age: number) { diff --git a/shared/api-types.ts b/shared/api-types.ts index aa6b42e..c044daa 100644 --- a/shared/api-types.ts +++ b/shared/api-types.ts @@ -123,6 +123,13 @@ export const EmailUpdateSchema = z.object({ }); export type ApiAccountEmailUpdateRequest = z.infer; +export const PasswordUpdateSchema = z.object({ + oldPassword: z.string(), + newPassword: z.string(), + newPasswordConfirm: z.string() +}); +export type ApiAccountPasswordUpdateRequest = z.infer; + export const EmailVerifySchema = z.object({ token: z.string() }); diff --git a/src/components/UpdatePasswordModal.vue b/src/components/UpdatePasswordModal.vue new file mode 100644 index 0000000..765194e --- /dev/null +++ b/src/components/UpdatePasswordModal.vue @@ -0,0 +1,176 @@ + + + + diff --git a/src/locales/en_US.json b/src/locales/en_US.json index ee9de5a..937bcac 100644 --- a/src/locales/en_US.json +++ b/src/locales/en_US.json @@ -272,6 +272,14 @@ "caption": "To verify your email address, click on the link in the verification email.", "lostEmail": "Can't find it?", "resend": "Resend verification email" + }, + "passwordModal": { + "title": "Change password", + "caption": "Input your new password.", + "oldPassword": "Current password", + "newPassword": "New password", + "newPasswordConfirm": "Confirm new password", + "successNotice": "Password updated successfully" } }, "emailVerification": { diff --git a/src/pages/account/index.vue b/src/pages/account/index.vue index e09727d..2bf1a08 100644 --- a/src/pages/account/index.vue +++ b/src/pages/account/index.vue @@ -66,6 +66,7 @@ const dialogContainer = ref(null); const deleteModalOpen = ref(false); const profileEditModalOpen = ref(false); const emailEditModalOpen = ref(false); +const passwordEditModalOpen = ref(false); const selectedServerEnv = ref<'dev' | 'test' | 'prod' | undefined>( profile.value?.serverAccessLevel ); @@ -477,6 +478,10 @@ customSeoMeta({ subsection: 'account' });

●●●●●●●●

+ @@ -554,7 +559,8 @@ customSeoMeta({ subsection: 'account' }); hidden: !( deleteModalOpen || profileEditModalOpen || - emailEditModalOpen + emailEditModalOpen || + passwordEditModalOpen ), }" >