From d793cf9726958ab4d6166e8d59d7a354d01dac97 Mon Sep 17 00:00:00 2001 From: limes Date: Sat, 29 Aug 2026 19:59:16 +0200 Subject: [PATCH 1/4] feat: add password change from account page --- package-lock.json | 8 +- package.json | 2 +- server/api/account/update-password.patch.ts | 43 +++++ server/api/auth/register.post.ts | 8 +- shared/api-types.ts | 7 + shared/errors.ts | 12 +- src/components/UpdatePasswordModal.vue | 174 ++++++++++++++++++++ src/locales/en_US.json | 8 + src/pages/account/index.vue | 8 +- 9 files changed, 257 insertions(+), 13 deletions(-) create mode 100644 server/api/account/update-password.patch.ts create mode 100644 src/components/UpdatePasswordModal.vue 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..dc7afbd --- /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': 'INVALID_PASSWORD_LENGTH', + 'INVALID_ARGUMENT: Password cannot be the same as username': 'INVALID_PASSWORD_USERNAME', + 'INVALID_ARGUMENT: Password must have combination of letters, numbers, and/or punctuation characters': 'INVALID_PASSWORD_COMBOS', + 'INVALID_ARGUMENT: Password may not have 3 repeating characters': 'INVALID_PASSWORD_REPEATING', + 'INVALID_ARGUMENT: Passwords do not match': 'INVALID_PASSWORD_NO_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 b62b6a6..bfa81d4 100644 --- a/server/api/auth/register.post.ts +++ b/server/api/auth/register.post.ts @@ -21,10 +21,10 @@ const errors: Record = { 'INVALID_ARGUMENT: Two or more punctuation characters cannot be used in a row': 'USERNAME_INVALID_CHARS', 'INVALID_ARGUMENT: PNID already in use': 'USERNAME_IN_USE', 'INVALID_ARGUMENT: Mii name too long': 'MIINAME_TOO_LONG', - 'INVALID_ARGUMENT: Password must be between 6 and 16 characters long': 'INVALID_PASSWORD_INPUT', - 'INVALID_ARGUMENT: Password cannot be the same as username': 'INVALID_PASSWORD_INPUT', - 'INVALID_ARGUMENT: Password must have combination of letters, numbers, and/or punctuation characters': 'INVALID_PASSWORD_INPUT', - 'INVALID_ARGUMENT: Password may not have 3 repeating characters': 'INVALID_PASSWORD_INPUT', + 'INVALID_ARGUMENT: Password must be between 6 and 16 characters long': 'INVALID_PASSWORD_LENGTH', + 'INVALID_ARGUMENT: Password cannot be the same as username': 'INVALID_PASSWORD_USERNAME', + 'INVALID_ARGUMENT: Password must have combination of letters, numbers, and/or punctuation characters': 'INVALID_PASSWORD_COMBOS', + 'INVALID_ARGUMENT: Password may not have 3 repeating characters': 'INVALID_PASSWORD_REPEATING', 'INVALID_ARGUMENT: Passwords do not match': 'INVALID_PASSWORD_NO_MATCH' }; 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/shared/errors.ts b/shared/errors.ts index 5e5e95e..d1d9fa6 100644 --- a/shared/errors.ts +++ b/shared/errors.ts @@ -16,7 +16,10 @@ const apiErrorCodes = { USERNAME_INVALID_CHARS: 'Username contains invalid characters', USERNAME_IN_USE: 'PNID already in use', MIINAME_TOO_LONG: 'Mii name too long', - INVALID_PASSWORD_INPUT: 'Password must be between 6 and 16 characters long', + INVALID_PASSWORD_LENGTH: 'Password must be between 6 and 16 characters long', + INVALID_PASSWORD_USERNAME: 'Password cannot be the same as username', + INVALID_PASSWORD_COMBOS: 'Password must have combination of letters, numbers, and/or punctuation characters', + INVALID_PASSWORD_REPEATING: 'Password may not have 3 repeating characters', INVALID_PASSWORD_NO_MATCH: 'Passwords do not match', ACCOUNT_DELETED: 'Account has been deleted', INVALID_ACCESS_LEVEL: 'Invalid access level', @@ -48,7 +51,6 @@ export const apiErrorCodeStatus: Record = { UNDER_THIRTEEN: 400, ACCOUNT_DELETED: 400, INVALID_EMAIL: 400, - INVALID_PASSWORD_INPUT: 400, INVALID_PASSWORD_NO_MATCH: 400, MIINAME_TOO_LONG: 400, USERNAME_IN_USE: 400, @@ -65,7 +67,11 @@ export const apiErrorCodeStatus: Record = { INVALID_MII_DATA: 400, EMAIL_UNCHANGED: 400, INVALID_EMAIL_TOKEN: 400, - MISSING_EMAIL_TOKEN: 400 + MISSING_EMAIL_TOKEN: 400, + INVALID_PASSWORD_LENGTH: 400, + INVALID_PASSWORD_USERNAME: 400, + INVALID_PASSWORD_COMBOS: 400, + INVALID_PASSWORD_REPEATING: 400 }; export function getTextForApiErrorCode(code: ApiErrorCodes): string { diff --git a/src/components/UpdatePasswordModal.vue b/src/components/UpdatePasswordModal.vue new file mode 100644 index 0000000..bdd7e05 --- /dev/null +++ b/src/components/UpdatePasswordModal.vue @@ -0,0 +1,174 @@ + + + + diff --git a/src/locales/en_US.json b/src/locales/en_US.json index 1c23134..b2c883e 100644 --- a/src/locales/en_US.json +++ b/src/locales/en_US.json @@ -271,6 +271,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" } }, "accountLevel": [ diff --git a/src/pages/account/index.vue b/src/pages/account/index.vue index 37a5240..6bd0213 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 ); @@ -467,6 +468,10 @@ useHead({

●●●●●●●●

+ @@ -544,7 +549,8 @@ useHead({ hidden: !( deleteModalOpen || profileEditModalOpen || - emailEditModalOpen + emailEditModalOpen || + passwordEditModalOpen ), }" > From f316d201b453d8b9587a26e9d2883c34e4819610 Mon Sep 17 00:00:00 2001 From: limes Date: Sun, 30 Aug 2026 20:24:50 +0200 Subject: [PATCH 2/4] fix: update error codes --- server/api/account/update-password.patch.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/api/account/update-password.patch.ts b/server/api/account/update-password.patch.ts index dc7afbd..008cf95 100644 --- a/server/api/account/update-password.patch.ts +++ b/server/api/account/update-password.patch.ts @@ -10,11 +10,11 @@ const bucket = createRatelimitBucket({ }); const errors: Record = { - 'INVALID_ARGUMENT: Password must be between 6 and 16 characters long': 'INVALID_PASSWORD_LENGTH', - 'INVALID_ARGUMENT: Password cannot be the same as username': 'INVALID_PASSWORD_USERNAME', - 'INVALID_ARGUMENT: Password must have combination of letters, numbers, and/or punctuation characters': 'INVALID_PASSWORD_COMBOS', - 'INVALID_ARGUMENT: Password may not have 3 repeating characters': 'INVALID_PASSWORD_REPEATING', - 'INVALID_ARGUMENT: Passwords do not match': 'INVALID_PASSWORD_NO_MATCH', + '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' }; From 40c20afb8de6a2ff5bfe61016845c2390e313519 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Mon, 7 Sep 2026 19:32:57 +0200 Subject: [PATCH 3/4] chore: update account server image --- .docker/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 64f460b555d93beb73d8c20ab2c57781cd813089 Mon Sep 17 00:00:00 2001 From: limes Date: Mon, 7 Sep 2026 22:02:58 +0200 Subject: [PATCH 4/4] feat: add delay after password change before logout --- src/components/UpdatePasswordModal.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/UpdatePasswordModal.vue b/src/components/UpdatePasswordModal.vue index bdd7e05..765194e 100644 --- a/src/components/UpdatePasswordModal.vue +++ b/src/components/UpdatePasswordModal.vue @@ -43,6 +43,8 @@ const { text: t('account.settings.passwordModal.successNotice') }); + await new Promise(r => setTimeout(r, 3000)); + emit('change'); authUtils.logout(); },