diff --git a/server/api/auth/login.post.ts b/server/api/auth/login.post.ts index fc74aa0..ada022d 100644 --- a/server/api/auth/login.post.ts +++ b/server/api/auth/login.post.ts @@ -1,7 +1,14 @@ import { ClientError } from 'nice-grpc'; import { LoginSchema } from '#shared/api-types'; +import type { ApiErrorCodes } from '~~/shared/errors'; import type { ApiAuthLogin } from '#shared/api-types'; +const errors: Record = { + 'INVALID_ARGUMENT: User not found': 'INVALID_USERNAME', + 'INVALID_ARGUMENT: Password is incorrect': 'INVALID_PASSWORD', + 'UNAUTHENTICATED: Account has been deleted': 'ACCOUNT_DELETED' +}; + export default defineEventHandler(async (event): Promise => { const body = await readZodBody(event, LoginSchema); const grpc = useApiGrpc(event); @@ -19,11 +26,9 @@ export default defineEventHandler(async (event): Promise => { }; } catch (error: unknown) { if (error instanceof ClientError) { - if (error.details === 'INVALID_ARGUMENT: User not found') { - throw createApiError('INVALID_USERNAME'); - } - if (error.details === 'INVALID_ARGUMENT: Password is incorrect') { - throw createApiError('INVALID_PASSWORD'); + 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 89e22d9..0fccad3 100644 --- a/server/api/auth/register.post.ts +++ b/server/api/auth/register.post.ts @@ -1,23 +1,57 @@ +import { ClientError } from 'nice-grpc'; import { RegisterSchema } from '#shared/api-types'; +import type { ApiErrorCodes } from '~~/shared/errors'; import type { ApiAuthLogin } from '#shared/api-types'; +const errors: Record = { + 'INVALID_ARGUMENT: Captcha verification failed': 'INVALID_CAPTCHA', + 'INVALID_ARGUMENT: Invalid email address': 'INVALID_EMAIL', + 'INVALID_ARGUMENT: Username is too short': 'USERNAME_TOO_SHORT', + 'INVALID_ARGUMENT: Username is too long': 'USERNAME_TOO_LONG', + 'INVALID_ARGUMENT: Username contains invalid characters': 'USERNAME_INVALID_CHARS', + 'INVALID_ARGUMENT: Username cannot begin with punctuation characters': 'USERNAME_INVALID_CHARS', + 'INVALID_ARGUMENT: Username cannot end with punctuation characters': 'USERNAME_INVALID_CHARS', + '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: Passwords do not match': 'INVALID_PASSWORD_NO_MATCH' +}; + +function getCutoffDateForAge(today: Date, age: number) { + return new Date(today.getFullYear() - age, today.getMonth(), today.getDate()); +} + +function assertAge(birthDate: string | undefined) { + if (!birthDate) { + throw createApiError('INVALID_INPUT'); + } + const date = new Date(birthDate); + const today = new Date(); + + // Prevent users below 13 + if (date > getCutoffDateForAge(today, 13)) { + throw createApiError('UNDER_THIRTEEN'); + } +} + export default defineEventHandler(async (event): Promise => { const body = await readZodBody(event, RegisterSchema); const grpc = useApiGrpc(event); + assertAge(body.birthday); - console.log(body.birthday); - - // eslint-disable-next-line no-useless-catch -- Temp before error handling is implemented try { // TODO Add ip - // TODO Add birthday const res = await grpc.register({ email: body.email, miiName: body.miiName, captchaResponse: body.captchaResponse, username: body.username, password: body.password, - passwordConfirm: body.password, + passwordConfirm: body.password }); return { @@ -25,7 +59,12 @@ export default defineEventHandler(async (event): Promise => { refreshToken: res.refreshToken }; } catch (error: unknown) { - // TODO handle errors + if (error instanceof ClientError) { + const errorCode = errors[error.details]; + if (errorCode) { + throw createApiError(errorCode); + } + } throw error; } }); diff --git a/shared/api-types.ts b/shared/api-types.ts index f7dd122..2989f73 100644 --- a/shared/api-types.ts +++ b/shared/api-types.ts @@ -100,7 +100,7 @@ export const RegisterSchema = z.object({ username: z.string(), miiName: z.string(), password: z.string(), - birthday: z.string(), + birthday: z.iso.date(), captchaResponse: z.string().optional() }); export type ApiAuthRegisterRequest = z.infer; diff --git a/shared/errors.ts b/shared/errors.ts index 4c0373c..3923d6d 100644 --- a/shared/errors.ts +++ b/shared/errors.ts @@ -7,7 +7,17 @@ const apiErrorCodes = { INVALID_CAPTCHA: 'Invalid captcha, try again', INVALID_USERNAME: 'Could not find user', INVALID_PASSWORD: 'Incorrect password', - UNAUTHENTICATED: 'This request needs authentication' + UNAUTHENTICATED: 'This request needs authentication', + UNDER_THIRTEEN: 'Must be 13 or older to use these services', + INVALID_EMAIL: 'Invalid email address', + USERNAME_TOO_SHORT: 'Username is too short', + USERNAME_TOO_LONG: 'Username is too long', + 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_NO_MATCH: 'Passwords do not match', + ACCOUNT_DELETED: 'Account has been deleted' } as const; export type ApiErrorCodes = keyof typeof apiErrorCodes; @@ -21,7 +31,17 @@ export const apiErrorCodeStatus: Record = { INVALID_CAPTCHA: 400, INVALID_USERNAME: 400, INVALID_PASSWORD: 400, - UNAUTHENTICATED: 401 + UNAUTHENTICATED: 401, + 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, + USERNAME_INVALID_CHARS: 400, + USERNAME_TOO_LONG: 400, + USERNAME_TOO_SHORT: 400 }; export function getTextForApiErrorCode(code: ApiErrorCodes): string {