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 ), }" >