From 33a602cdd0fd882954925358e00a09cf68078ef3 Mon Sep 17 00:00:00 2001 From: creations Date: Sat, 14 Jun 2025 09:19:55 -0400 Subject: [PATCH] move alot to constants, fix html --- src/environment/constants/cache.ts | 26 ++++ src/environment/constants/http.ts | 54 +++++++ src/environment/constants/index.ts | 2 + src/environment/constants/user/index.ts | 8 +- src/environment/constants/user/update.ts | 6 - .../email-change-completed-notification.html | 2 +- .../email-change-request-notification.html | 2 +- .../templates/email-change-verification.html | 2 +- .../mailer/templates/forgot-password.html | 2 +- .../mailer/templates/register.html | 4 +- src/lib/utils/index.ts | 1 + src/lib/utils/string.ts | 133 +++++++++++++++++ src/lib/validation/name.ts | 2 +- src/routes/health.ts | 5 +- src/routes/user/[id].ts | 41 +++--- src/routes/user/forgot/password.ts | 56 +++++--- src/routes/user/login.ts | 62 ++++---- src/routes/user/logout.ts | 29 ++-- src/routes/user/register.ts | 16 ++- src/routes/user/reset/password.ts | 85 ++++++----- src/routes/user/update/email.ts | 134 ++++++++++-------- src/routes/user/update/info.ts | 65 +++++---- src/routes/user/update/password.ts | 86 ++++++----- src/routes/user/verify.ts | 70 +++++---- types/server/requests/user/index.ts | 1 + types/server/requests/user/logout.ts | 5 + 26 files changed, 603 insertions(+), 296 deletions(-) create mode 100644 src/environment/constants/cache.ts create mode 100644 src/environment/constants/http.ts delete mode 100644 src/environment/constants/user/update.ts create mode 100644 src/lib/utils/string.ts create mode 100644 types/server/requests/user/logout.ts diff --git a/src/environment/constants/cache.ts b/src/environment/constants/cache.ts new file mode 100644 index 0000000..e042c47 --- /dev/null +++ b/src/environment/constants/cache.ts @@ -0,0 +1,26 @@ +const cacheKeys = { + session: "session", + mailVerification: "mail-verification", + passwordReset: "password-reset", + emailChange: "email-change", + emailChangeCooldown: "email-change-cooldown", +} as const; + +const cacheTTL = { + passwordReset: 1 * 60 * 60, // 1h + mailVerification: 3 * 60 * 60, // 3h + emailChange: 3 * 60 * 60, // 3h + emailChangeCooldown: 5 * 60, // 5m +} as const; + +const generateCacheKey = { + session: (userId: string, token: string) => + `${cacheKeys.session}:${userId}:${token}`, + mailVerification: (token: string) => `${cacheKeys.mailVerification}:${token}`, + passwordReset: (token: string) => `${cacheKeys.passwordReset}:${token}`, + emailChange: (token: string) => `${cacheKeys.emailChange}:${token}`, + emailChangeCooldown: (userId: string) => + `${cacheKeys.emailChangeCooldown}:${userId}`, +} as const; + +export { cacheKeys, cacheTTL, generateCacheKey }; diff --git a/src/environment/constants/http.ts b/src/environment/constants/http.ts new file mode 100644 index 0000000..fcaa614 --- /dev/null +++ b/src/environment/constants/http.ts @@ -0,0 +1,54 @@ +const httpStatus = { + OK: 200, + CREATED: 201, + BAD_REQUEST: 400, + UNAUTHORIZED: 401, + FORBIDDEN: 403, + NOT_FOUND: 404, + METHOD_NOT_ALLOWED: 405, + NOT_ACCEPTABLE: 406, + CONFLICT: 409, + TOO_MANY_REQUESTS: 429, + INTERNAL_SERVER_ERROR: 500, + SERVICE_UNAVAILABLE: 503, +}; + +const errorMessages = { + NOT_AUTHENTICATED: "Not authenticated", + INVALID_CREDENTIALS: "Invalid credentials", + USER_ALREADY_LOGGED_IN: "User already logged in", + + USER_NOT_FOUND: "User not found", + USERNAME_ALREADY_EXISTS: "Username already exists", + EMAIL_ALREADY_EXISTS: "Email already exists", + + MISSING_REQUIRED_FIELDS: "Missing required fields", + INVALID_TOKEN: "Invalid or expired token", + INVALID_TOKEN_FORMAT: "Invalid token format", + + INTERNAL_SERVER_ERROR: "Internal server error", + DATABASE_QUERY_FAILED: "Database query failed", + NOT_FOUND: "Not Found", + METHOD_NOT_ALLOWED: "Method Not Allowed", + + EMAIL_SEND_FAILED: "Failed to send email. Please try again.", + EMAIL_ALREADY_VERIFIED: "Email is already verified", + + PASSWORD_SAME_AS_CURRENT: + "New password must be different from current password", + CURRENT_PASSWORD_INCORRECT: "Current password is incorrect", +}; + +const successMessages = { + LOGIN_SUCCESSFUL: "Login successful", + LOGOUT_SUCCESSFUL: "Logged out successfully", + EMAIL_VERIFIED: "Email verified successfully", + PASSWORD_UPDATED: "Password updated successfully", + USER_INFO_UPDATED: "User information updated successfully", + PASSWORD_RESET_SENT: + "If the email exists, a password reset link has been sent", + REGISTRATION_SUCCESSFUL: + "User registered successfully - please check your email to verify your account", +}; + +export { httpStatus, errorMessages, successMessages }; diff --git a/src/environment/constants/index.ts b/src/environment/constants/index.ts index 6ca789c..062632b 100644 --- a/src/environment/constants/index.ts +++ b/src/environment/constants/index.ts @@ -32,3 +32,5 @@ export * from "./validation"; export * from "./database"; export * from "./mailer"; export * from "./user"; +export * from "./cache"; +export * from "./http"; diff --git a/src/environment/constants/user/index.ts b/src/environment/constants/user/index.ts index 635be64..082e610 100644 --- a/src/environment/constants/user/index.ts +++ b/src/environment/constants/user/index.ts @@ -1 +1,7 @@ -export * from "./update"; +const passwordHashing = { + algorithm: "argon2id" as const, + memoryCost: 4096, + timeCost: 3, +} as const; + +export { passwordHashing }; diff --git a/src/environment/constants/user/update.ts b/src/environment/constants/user/update.ts deleted file mode 100644 index f69fe57..0000000 --- a/src/environment/constants/user/update.ts +++ /dev/null @@ -1,6 +0,0 @@ -const emailUpdateTimes = { - coolDownMinutes: 5, - tokenExpiryHours: 3, -}; - -export { emailUpdateTimes }; diff --git a/src/environment/mailer/templates/email-change-completed-notification.html b/src/environment/mailer/templates/email-change-completed-notification.html index 87cb9c4..c89439d 100644 --- a/src/environment/mailer/templates/email-change-completed-notification.html +++ b/src/environment/mailer/templates/email-change-completed-notification.html @@ -35,6 +35,6 @@

If this change was not authorized by you: Contact our support team immediately at {{supportEmail}}. Your account may have been compromised and we will help you recover it.


-

User ID: {{id}} | {{companyName}}

+

User ID: {{id}}

diff --git a/src/environment/mailer/templates/email-change-request-notification.html b/src/environment/mailer/templates/email-change-request-notification.html index 5d7216b..3cb2a0e 100644 --- a/src/environment/mailer/templates/email-change-request-notification.html +++ b/src/environment/mailer/templates/email-change-request-notification.html @@ -36,6 +36,6 @@

Questions? Contact {{supportEmail}}


-

User ID: {{id}} | {{companyName}}

+

User ID: {{id}}

diff --git a/src/environment/mailer/templates/email-change-verification.html b/src/environment/mailer/templates/email-change-verification.html index 61e43c2..f0b855c 100644 --- a/src/environment/mailer/templates/email-change-verification.html +++ b/src/environment/mailer/templates/email-change-verification.html @@ -36,6 +36,6 @@

If you did not request this email change, contact {{supportEmail}} immediately.

Questions? Contact {{supportEmail}}


-

User ID: {{id}} | {{companyName}}

+

User ID: {{id}}

diff --git a/src/environment/mailer/templates/forgot-password.html b/src/environment/mailer/templates/forgot-password.html index 03d682d..b7773af 100644 --- a/src/environment/mailer/templates/forgot-password.html +++ b/src/environment/mailer/templates/forgot-password.html @@ -34,6 +34,6 @@

If you did not request this password reset, please contact {{supportEmail}} immediately. Your password will remain unchanged.

Questions? Contact {{supportEmail}}


-

User ID: {{id}} | {{companyName}}

+

User ID: {{id}}

diff --git a/src/environment/mailer/templates/register.html b/src/environment/mailer/templates/register.html index c603819..87ee955 100644 --- a/src/environment/mailer/templates/register.html +++ b/src/environment/mailer/templates/register.html @@ -32,9 +32,7 @@

{{willExpire}} for security reasons.

Questions? Contact {{supportEmail}}

-

Best regards,
- The {{companyName}} team


-

User ID: {{id}} | {{companyName}}

+

User ID: {{id}}

diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 389fb9f..8c164ba 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -1,2 +1,3 @@ export * from "./idGenerator"; export * from "./time"; +export * from "./string"; diff --git a/src/lib/utils/string.ts b/src/lib/utils/string.ts new file mode 100644 index 0000000..7f98163 --- /dev/null +++ b/src/lib/utils/string.ts @@ -0,0 +1,133 @@ +function formatSecondsToHighestUnit( + seconds: number, + options: { + capitalize?: boolean; + pluralize?: boolean; + short?: boolean; + } = {}, +): string { + const { capitalize = false, pluralize = true, short = false } = options; + + const timeUnits = [ + { + value: 365 * 24 * 60 * 60, + singular: "year", + plural: "years", + short: "y", + }, + { + value: 30 * 24 * 60 * 60, + singular: "month", + plural: "months", + short: "mo", + }, + { value: 7 * 24 * 60 * 60, singular: "week", plural: "weeks", short: "w" }, + { value: 24 * 60 * 60, singular: "day", plural: "days", short: "d" }, + { value: 60 * 60, singular: "hour", plural: "hours", short: "h" }, + { value: 60, singular: "minute", plural: "minutes", short: "m" }, + { value: 1, singular: "second", plural: "seconds", short: "s" }, + ]; + + if (seconds === 0) { + const unit = short ? "s" : pluralize ? "seconds" : "second"; + return `0 ${capitalize ? capitalizeFirst(unit) : unit}`; + } + + if (seconds < 0) { + return formatSecondsToHighestUnit(Math.abs(seconds), options); + } + + for (const unit of timeUnits) { + const count = Math.floor(seconds / unit.value); + if (count >= 1) { + let unitName: string; + + if (short) { + unitName = unit.short; + } else if (pluralize && count !== 1) { + unitName = unit.plural; + } else { + unitName = unit.singular; + } + + if (capitalize && !short) { + unitName = capitalizeFirst(unitName); + } + + return `${count} ${unitName}`; + } + } + + const unit = short ? "s" : pluralize ? "seconds" : "second"; + return `${seconds} ${capitalize ? capitalizeFirst(unit) : unit}`; +} + +function capitalizeFirst(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +function formatSecondsToMultipleUnits( + seconds: number, + maxUnits = 2, + options: { + capitalize?: boolean; + short?: boolean; + separator?: string; + } = {}, +): string { + const { capitalize = true, short = false, separator = " " } = options; + + const timeUnits = [ + { + value: 365 * 24 * 60 * 60, + singular: "year", + plural: "years", + short: "y", + }, + { + value: 30 * 24 * 60 * 60, + singular: "month", + plural: "months", + short: "mo", + }, + { value: 7 * 24 * 60 * 60, singular: "week", plural: "weeks", short: "w" }, + { value: 24 * 60 * 60, singular: "day", plural: "days", short: "d" }, + { value: 60 * 60, singular: "hour", plural: "hours", short: "h" }, + { value: 60, singular: "minute", plural: "minutes", short: "m" }, + { value: 1, singular: "second", plural: "seconds", short: "s" }, + ]; + + if (seconds === 0) { + const unit = short ? "s" : "seconds"; + return `0 ${capitalize ? capitalizeFirst(unit) : unit}`; + } + + const parts: string[] = []; + let remaining = Math.abs(seconds); + + for (const unit of timeUnits) { + if (parts.length >= maxUnits) break; + + const count = Math.floor(remaining / unit.value); + if (count > 0) { + let unitName: string; + + if (short) { + unitName = unit.short; + } else { + unitName = count === 1 ? unit.singular : unit.plural; + } + + if (capitalize && !short) { + unitName = capitalizeFirst(unitName); + } + + parts.push(`${count} ${unitName}`); + remaining -= count * unit.value; + } + } + + return parts.join(separator) || "0 seconds"; +} + +export { formatSecondsToHighestUnit, formatSecondsToMultipleUnits }; diff --git a/src/lib/validation/name.ts b/src/lib/validation/name.ts index bebaa06..91d9233 100644 --- a/src/lib/validation/name.ts +++ b/src/lib/validation/name.ts @@ -1,7 +1,7 @@ import { displayNameRestrictions, forbiddenDisplayNamePatterns, - nameRestrictions, + nameRestrictions } from "#environment/constants"; import type { validationResult } from "#types/lib"; diff --git a/src/routes/health.ts b/src/routes/health.ts index 19e36cc..8994901 100644 --- a/src/routes/health.ts +++ b/src/routes/health.ts @@ -1,4 +1,5 @@ import { redis } from "bun"; +import { httpStatus } from "#environment/constants"; import { cassandra } from "#lib/database"; import type { ExtendedRequest, HealthResponse, RouteDef } from "#types/server"; @@ -25,7 +26,7 @@ async function handler(request: ExtendedRequest): Promise { const isHealthy = cassandraHealth.connected && redisHealth === "healthy"; const response: HealthResponse = { - code: isHealthy ? 200 : 503, + code: isHealthy ? httpStatus.OK : httpStatus.SERVICE_UNAVAILABLE, success: isHealthy, message: isHealthy ? "All services are healthy" @@ -44,7 +45,7 @@ async function handler(request: ExtendedRequest): Promise { }; return Response.json(response, { - status: isHealthy ? 200 : 503, + status: isHealthy ? httpStatus.OK : httpStatus.SERVICE_UNAVAILABLE, }); } diff --git a/src/routes/user/[id].ts b/src/routes/user/[id].ts index c0678c7..8c86741 100644 --- a/src/routes/user/[id].ts +++ b/src/routes/user/[id].ts @@ -1,4 +1,5 @@ import { echo } from "@atums/echo"; +import { errorMessages, httpStatus } from "#environment/constants"; import { cassandra } from "#lib/database"; import type { @@ -18,9 +19,7 @@ const routeDef: RouteDef = { async function handler(request: ExtendedRequest): Promise { try { const { id: identifier } = request.params; - const { session } = request; - let userQuery: string; let queryParams: string[]; let targetUser: UserRow | null = null; @@ -28,11 +27,11 @@ async function handler(request: ExtendedRequest): Promise { if (!identifier) { if (!session) { const response: UserInfoResponse = { - code: 401, + code: httpStatus.UNAUTHORIZED, success: false, - error: "Not authenticated", + error: errorMessages.NOT_AUTHENTICATED, }; - return Response.json(response, { status: 401 }); + return Response.json(response, { status: httpStatus.UNAUTHORIZED }); } userQuery = ` @@ -64,11 +63,13 @@ async function handler(request: ExtendedRequest): Promise { if (!userResult?.rows || !Array.isArray(userResult.rows)) { const response: UserInfoResponse = { - code: 500, + code: httpStatus.INTERNAL_SERVER_ERROR, success: false, - error: "Database query failed", + error: errorMessages.DATABASE_QUERY_FAILED, }; - return Response.json(response, { status: 500 }); + return Response.json(response, { + status: httpStatus.INTERNAL_SERVER_ERROR, + }); } if (userResult.rows.length === 0) { @@ -91,11 +92,11 @@ async function handler(request: ExtendedRequest): Promise { if (!targetUser) { const response: UserInfoResponse = { - code: 404, + code: httpStatus.NOT_FOUND, success: false, - error: "User not found", + error: errorMessages.USER_NOT_FOUND, }; - return Response.json(response, { status: 404 }); + return Response.json(response, { status: httpStatus.NOT_FOUND }); } } else { targetUser = userResult.rows[0] || null; @@ -103,11 +104,11 @@ async function handler(request: ExtendedRequest): Promise { if (!targetUser) { const response: UserInfoResponse = { - code: 404, + code: httpStatus.NOT_FOUND, success: false, - error: "User not found", + error: errorMessages.USER_NOT_FOUND, }; - return Response.json(response, { status: 404 }); + return Response.json(response, { status: httpStatus.NOT_FOUND }); } const isOwnProfile = session?.id === targetUser.id; @@ -135,7 +136,7 @@ async function handler(request: ExtendedRequest): Promise { } const response: UserInfoResponse = { - code: 200, + code: httpStatus.OK, success: true, message: isOwnProfile ? "User information retrieved successfully" @@ -143,7 +144,7 @@ async function handler(request: ExtendedRequest): Promise { user: responseUser, }; - return Response.json(response, { status: 200 }); + return Response.json(response, { status: httpStatus.OK }); } catch (error) { echo.error({ message: "Error retrieving user information", @@ -151,11 +152,13 @@ async function handler(request: ExtendedRequest): Promise { }); const response: UserInfoResponse = { - code: 500, + code: httpStatus.INTERNAL_SERVER_ERROR, success: false, - error: "Internal server error", + error: errorMessages.INTERNAL_SERVER_ERROR, }; - return Response.json(response, { status: 500 }); + return Response.json(response, { + status: httpStatus.INTERNAL_SERVER_ERROR, + }); } } diff --git a/src/routes/user/forgot/password.ts b/src/routes/user/forgot/password.ts index 3f9fbd7..fe00ff1 100644 --- a/src/routes/user/forgot/password.ts +++ b/src/routes/user/forgot/password.ts @@ -1,9 +1,17 @@ import { echo } from "@atums/echo"; import { redis } from "bun"; import { environment } from "#environment/config"; +import { + cacheTTL, + errorMessages, + generateCacheKey, + httpStatus, + successMessages, +} from "#environment/constants"; import { extraValues } from "#environment/extra"; import { cassandra } from "#lib/database"; import { mailerService } from "#lib/mailer"; +import { formatSecondsToHighestUnit } from "#lib/utils"; import { isValidEmail } from "#lib/validation"; import type { @@ -29,21 +37,21 @@ async function handler( if (!email) { const response: BaseResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, error: "Email is required", }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } const emailValidation = isValidEmail(email); if (!emailValidation.valid) { const response: BaseResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, error: emailValidation.error || "Invalid email", }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } const userQuery = ` @@ -64,25 +72,25 @@ async function handler( if (!userResult?.rows || userResult.rows.length === 0) { const response: BaseResponse = { - code: 200, + code: httpStatus.OK, success: true, - message: "If the email exists, a password reset link has been sent", + message: successMessages.PASSWORD_RESET_SENT, }; - return Response.json(response, { status: 200 }); + return Response.json(response, { status: httpStatus.OK }); } const user = userResult.rows[0]; if (!user) { const response: BaseResponse = { - code: 200, + code: httpStatus.OK, success: true, - message: "If the email exists, a password reset link has been sent", + message: successMessages.PASSWORD_RESET_SENT, }; - return Response.json(response, { status: 200 }); + return Response.json(response, { status: httpStatus.OK }); } const resetToken = Bun.randomUUIDv7(); - const resetKey = `password-reset:${resetToken}`; + const resetKey = generateCacheKey.passwordReset(resetToken); try { await redis.set( @@ -92,7 +100,7 @@ async function handler( email: user.email, }), "EX", - 1 * 60 * 60, // 1 hour + cacheTTL.passwordReset, ); const emailVariables = { @@ -100,7 +108,7 @@ async function handler( companyName: extraValues.companyName, id: user.id, displayName: user.display_name || user.username, - willExpire: "This link will expire in 1 hour", + willExpire: `This link will expire in ${formatSecondsToHighestUnit(cacheTTL.passwordReset)}`, resetUrl: `${environment.frontendFqdn}/user/reset-password?token=${resetToken}`, supportEmail: extraValues.supportEmail, }; @@ -113,11 +121,11 @@ async function handler( ); const response: BaseResponse = { - code: 200, + code: httpStatus.OK, success: true, - message: "If the email exists, a password reset link has been sent", + message: successMessages.PASSWORD_RESET_SENT, }; - return Response.json(response, { status: 200 }); + return Response.json(response, { status: httpStatus.OK }); } catch (error) { await redis.del(resetKey).catch(() => {}); @@ -129,11 +137,13 @@ async function handler( }); const response: BaseResponse = { - code: 500, + code: httpStatus.INTERNAL_SERVER_ERROR, success: false, - error: "Failed to send password reset email. Please try again.", + error: errorMessages.EMAIL_SEND_FAILED, }; - return Response.json(response, { status: 500 }); + return Response.json(response, { + status: httpStatus.INTERNAL_SERVER_ERROR, + }); } } catch (error) { echo.error({ @@ -142,11 +152,13 @@ async function handler( }); const response: BaseResponse = { - code: 500, + code: httpStatus.INTERNAL_SERVER_ERROR, success: false, - error: "Internal server error", + error: errorMessages.INTERNAL_SERVER_ERROR, }; - return Response.json(response, { status: 500 }); + return Response.json(response, { + status: httpStatus.INTERNAL_SERVER_ERROR, + }); } } diff --git a/src/routes/user/login.ts b/src/routes/user/login.ts index 5b809e2..a3957ac 100644 --- a/src/routes/user/login.ts +++ b/src/routes/user/login.ts @@ -1,4 +1,9 @@ import { echo } from "@atums/echo"; +import { + errorMessages, + httpStatus, + successMessages, +} from "#environment/constants"; import { sessionManager } from "#lib/auth"; import { cassandra } from "#lib/database"; import { isValidEmail, isValidUsername } from "#lib/validation"; @@ -30,22 +35,21 @@ async function handler( const existingSession = await sessionManager.getSession(request); if (existingSession) { const response: LoginResponse = { - code: 409, + code: httpStatus.CONFLICT, success: false, - error: "User already logged in", + error: errorMessages.USER_ALREADY_LOGGED_IN, }; - return Response.json(response, { status: 409 }); + return Response.json(response, { status: httpStatus.CONFLICT }); } } if (!identifier || !password) { const response: LoginResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, - error: - "Missing required fields: identifier (username or email), password", + error: errorMessages.MISSING_REQUIRED_FIELDS, }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } const isEmail = isValidEmail(identifier).valid; @@ -53,11 +57,11 @@ async function handler( if (!isEmail && !isUsername) { const response: LoginResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, error: "Invalid identifier format - must be a valid username or email", }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } let userQuery: string; @@ -83,43 +87,45 @@ async function handler( if (!userResult?.rows || !Array.isArray(userResult.rows)) { const response: LoginResponse = { - code: 500, + code: httpStatus.INTERNAL_SERVER_ERROR, success: false, - error: "Database query failed", + error: errorMessages.DATABASE_QUERY_FAILED, }; - return Response.json(response, { status: 500 }); + return Response.json(response, { + status: httpStatus.INTERNAL_SERVER_ERROR, + }); } if (userResult.rows.length === 0) { const response: LoginResponse = { - code: 401, + code: httpStatus.UNAUTHORIZED, success: false, - error: "Invalid credentials", + error: errorMessages.INVALID_CREDENTIALS, }; - return Response.json(response, { status: 401 }); + return Response.json(response, { status: httpStatus.UNAUTHORIZED }); } const user = userResult.rows[0]; if (!user) { const response: LoginResponse = { - code: 401, + code: httpStatus.UNAUTHORIZED, success: false, - error: "Invalid credentials", + error: errorMessages.INVALID_CREDENTIALS, }; - return Response.json(response, { status: 401 }); + return Response.json(response, { status: httpStatus.UNAUTHORIZED }); } const isPasswordValid = await Bun.password.verify(password, user.password); if (!isPasswordValid) { const response: LoginResponse = { - code: 401, + code: httpStatus.UNAUTHORIZED, success: false, - error: "Invalid credentials", + error: errorMessages.INVALID_CREDENTIALS, }; - return Response.json(response, { status: 401 }); + return Response.json(response, { status: httpStatus.UNAUTHORIZED }); } const userAgent = request.headers.get("User-Agent") || "Unknown"; @@ -148,14 +154,14 @@ async function handler( }; const response: LoginResponse = { - code: 200, + code: httpStatus.OK, success: true, - message: "Login successful", + message: successMessages.LOGIN_SUCCESSFUL, user: responseUser, }; return Response.json(response, { - status: 200, + status: httpStatus.OK, headers: { "Set-Cookie": sessionCookie, }, @@ -167,11 +173,13 @@ async function handler( }); const response: LoginResponse = { - code: 500, + code: httpStatus.INTERNAL_SERVER_ERROR, success: false, - error: "Internal server error", + error: errorMessages.INTERNAL_SERVER_ERROR, }; - return Response.json(response, { status: 500 }); + return Response.json(response, { + status: httpStatus.INTERNAL_SERVER_ERROR, + }); } } diff --git a/src/routes/user/logout.ts b/src/routes/user/logout.ts index 558d2c9..08d16e7 100644 --- a/src/routes/user/logout.ts +++ b/src/routes/user/logout.ts @@ -1,9 +1,12 @@ import { echo } from "@atums/echo"; +import { + errorMessages, + httpStatus, + successMessages, +} from "#environment/constants"; import { cookieService, sessionManager } from "#lib/auth"; -import type { BaseResponse, ExtendedRequest, RouteDef } from "#types/server"; - -interface LogoutResponse extends BaseResponse {} +import type { ExtendedRequest, LogoutResponse, RouteDef } from "#types/server"; const routeDef: RouteDef = { method: ["POST", "DELETE"], @@ -17,24 +20,24 @@ async function handler(request: ExtendedRequest): Promise { if (!session) { const response: LogoutResponse = { - code: 401, + code: httpStatus.UNAUTHORIZED, success: false, - error: "Not authenticated", + error: errorMessages.NOT_AUTHENTICATED, }; - return Response.json(response, { status: 401 }); + return Response.json(response, { status: httpStatus.UNAUTHORIZED }); } await sessionManager.invalidateSession(request); const clearCookie = cookieService.clearCookie(); const response: LogoutResponse = { - code: 200, + code: httpStatus.OK, success: true, - message: "Logged out successfully", + message: successMessages.LOGOUT_SUCCESSFUL, }; return Response.json(response, { - status: 200, + status: httpStatus.OK, headers: { "Set-Cookie": clearCookie, }, @@ -46,11 +49,13 @@ async function handler(request: ExtendedRequest): Promise { }); const response: LogoutResponse = { - code: 500, + code: httpStatus.INTERNAL_SERVER_ERROR, success: false, - error: "Internal server error", + error: errorMessages.INTERNAL_SERVER_ERROR, }; - return Response.json(response, { status: 500 }); + return Response.json(response, { + status: httpStatus.INTERNAL_SERVER_ERROR, + }); } } diff --git a/src/routes/user/register.ts b/src/routes/user/register.ts index 39eedac..b4c4edb 100644 --- a/src/routes/user/register.ts +++ b/src/routes/user/register.ts @@ -1,6 +1,11 @@ import { echo } from "@atums/echo"; import { redis } from "bun"; import { environment } from "#environment/config"; +import { + cacheTTL, + generateCacheKey, + passwordHashing, +} from "#environment/constants"; import { extraValues } from "#environment/extra"; import { cassandra } from "#lib/database"; import { mailerService } from "#lib/mailer"; @@ -11,6 +16,7 @@ import { isValidPassword, isValidUsername, } from "#lib/validation"; + import type { ExtendedRequest, RegisterRequest, @@ -119,11 +125,7 @@ async function handler( const userId = pika.gen("user"); const verificationToken = Bun.randomUUIDv7(); - const hashedPassword = await Bun.password.hash(password, { - algorithm: "argon2id", - memoryCost: 4096, - timeCost: 3, - }); + const hashedPassword = await Bun.password.hash(password, passwordHashing); const now = new Date(); const insertUserQuery = ` @@ -155,13 +157,13 @@ async function handler( try { await redis.set( - `mail-verification:${verificationToken}`, + generateCacheKey.mailVerification(verificationToken), JSON.stringify({ userId: responseUser.id, email: responseUser.email, }), "EX", - 3 * 60 * 60, // 3h + cacheTTL.mailVerification, ); const emailVariables = { diff --git a/src/routes/user/reset/password.ts b/src/routes/user/reset/password.ts index 8bd5ccc..962853f 100644 --- a/src/routes/user/reset/password.ts +++ b/src/routes/user/reset/password.ts @@ -1,8 +1,15 @@ import { echo } from "@atums/echo"; import { redis } from "bun"; +import { + errorMessages, + generateCacheKey, + httpStatus, + passwordHashing, +} from "#environment/constants"; import { sessionManager } from "#lib/auth"; import { cassandra } from "#lib/database"; import { isValidPassword } from "#lib/validation"; + import type { ExtendedRequest, ResetData, @@ -32,33 +39,33 @@ async function handler( if (!token || !newPassword) { const response: ResetPasswordResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, error: "Token and new password are required", }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } const passwordValidation = isValidPassword(newPassword); if (!passwordValidation.valid) { const response: ResetPasswordResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, error: passwordValidation.error || "Invalid password", }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } - const resetKey = `password-reset:${token}`; + const resetKey = generateCacheKey.passwordReset(token); const resetDataRaw = await redis.get(resetKey); if (!resetDataRaw) { const response: ResetPasswordResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, - error: "Invalid or expired reset token", + error: errorMessages.INVALID_TOKEN, }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } let resetData: ResetData; @@ -67,21 +74,21 @@ async function handler( } catch { await redis.del(resetKey); const response: ResetPasswordResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, - error: "Invalid reset token format", + error: errorMessages.INVALID_TOKEN_FORMAT, }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } if (!resetData.userId || !resetData.email) { await redis.del(resetKey); const response: ResetPasswordResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, error: "Invalid reset data", }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } const userQuery = ` @@ -95,32 +102,32 @@ async function handler( if (!userResult?.rows || userResult.rows.length === 0) { await redis.del(resetKey); const response: ResetPasswordResponse = { - code: 404, + code: httpStatus.NOT_FOUND, success: false, - error: "User not found", + error: errorMessages.USER_NOT_FOUND, }; - return Response.json(response, { status: 404 }); + return Response.json(response, { status: httpStatus.NOT_FOUND }); } const user = userResult.rows[0]; if (!user) { await redis.del(resetKey); const response: ResetPasswordResponse = { - code: 404, + code: httpStatus.NOT_FOUND, success: false, - error: "User not found", + error: errorMessages.USER_NOT_FOUND, }; - return Response.json(response, { status: 404 }); + return Response.json(response, { status: httpStatus.NOT_FOUND }); } if (user.email !== resetData.email) { await redis.del(resetKey); const response: ResetPasswordResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, error: "Reset token does not match current email address", }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } const isSamePassword = await Bun.password.verify( @@ -129,18 +136,17 @@ async function handler( ); if (isSamePassword) { const response: ResetPasswordResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, - error: "New password must be different from current password", + error: errorMessages.PASSWORD_SAME_AS_CURRENT, }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } - const hashedNewPassword = await Bun.password.hash(newPassword, { - algorithm: "argon2id", - memoryCost: 4096, - timeCost: 3, - }); + const hashedNewPassword = await Bun.password.hash( + newPassword, + passwordHashing, + ); const updateQuery = ` UPDATE users @@ -162,16 +168,19 @@ async function handler( ); } + const baseMessage = "Password reset successfully"; + const sessionMessage = logoutAllSessions + ? `. Logged out from ${invalidatedCount} session(s).` + : "."; + const response: ResetPasswordResponse = { - code: 200, + code: httpStatus.OK, success: true, - message: logoutAllSessions - ? `Password reset successfully. Logged out from ${invalidatedCount} session(s).` - : "Password reset successfully.", + message: baseMessage + sessionMessage, loggedOutSessions: invalidatedCount, }; - return Response.json(response, { status: 200 }); + return Response.json(response, { status: httpStatus.OK }); } catch (error) { echo.error({ message: "Password reset failed", @@ -179,11 +188,13 @@ async function handler( }); const response: ResetPasswordResponse = { - code: 500, + code: httpStatus.INTERNAL_SERVER_ERROR, success: false, - error: "Internal server error", + error: errorMessages.INTERNAL_SERVER_ERROR, }; - return Response.json(response, { status: 500 }); + return Response.json(response, { + status: httpStatus.INTERNAL_SERVER_ERROR, + }); } } diff --git a/src/routes/user/update/email.ts b/src/routes/user/update/email.ts index 6120c2d..94664b2 100644 --- a/src/routes/user/update/email.ts +++ b/src/routes/user/update/email.ts @@ -1,11 +1,17 @@ import { echo } from "@atums/echo"; import { redis } from "bun"; import { environment } from "#environment/config"; -import { emailUpdateTimes } from "#environment/constants"; +import { + cacheTTL, + errorMessages, + generateCacheKey, + httpStatus, +} from "#environment/constants"; import { extraValues } from "#environment/extra"; import { sessionManager } from "#lib/auth"; import { cassandra } from "#lib/database"; import { mailerService } from "#lib/mailer"; +import { formatSecondsToHighestUnit } from "#lib/utils"; import { isValidEmail } from "#lib/validation"; import type { UserSession } from "#types/config"; @@ -35,11 +41,11 @@ async function handler( if (!session) { const response: EmailChangeResponse = { - code: 401, + code: httpStatus.UNAUTHORIZED, success: false, - error: "Not authenticated", + error: errorMessages.NOT_AUTHENTICATED, }; - return Response.json(response, { status: 401 }); + return Response.json(response, { status: httpStatus.UNAUTHORIZED }); } if (request.method === "GET") { @@ -54,11 +60,13 @@ async function handler( }); const response: EmailChangeResponse = { - code: 500, + code: httpStatus.INTERNAL_SERVER_ERROR, success: false, - error: "Internal server error", + error: errorMessages.INTERNAL_SERVER_ERROR, }; - return Response.json(response, { status: 500 }); + return Response.json(response, { + status: httpStatus.INTERNAL_SERVER_ERROR, + }); } } @@ -71,21 +79,21 @@ async function handleEmailChangeRequest( if (!newEmail) { const response: EmailChangeResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, error: "New email is required", }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } const emailValidation = isValidEmail(newEmail); if (!emailValidation.valid) { const response: EmailChangeResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, error: emailValidation.error || "Invalid email", }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } const normalizedEmail = newEmail.trim().toLowerCase(); @@ -102,51 +110,51 @@ async function handleEmailChangeRequest( await sessionManager.invalidateSession(request); const response: EmailChangeResponse = { - code: 404, + code: httpStatus.NOT_FOUND, success: false, - error: "User not found", + error: errorMessages.USER_NOT_FOUND, }; - return Response.json(response, { status: 404 }); + return Response.json(response, { status: httpStatus.NOT_FOUND }); } const currentUser = currentUserResult.rows[0]; if (!currentUser) { const response: EmailChangeResponse = { - code: 404, + code: httpStatus.NOT_FOUND, success: false, - error: "User not found", + error: errorMessages.USER_NOT_FOUND, }; - return Response.json(response, { status: 404 }); + return Response.json(response, { status: httpStatus.NOT_FOUND }); } if (normalizedEmail === currentUser.email) { const response: EmailChangeResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, error: "New email must be different from current email", }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } - const cooldownKey = `email-change-cooldown:${session.id}`; + const cooldownKey = generateCacheKey.emailChangeCooldown(session.id); const lastRequest = await redis.get(cooldownKey); if (lastRequest) { const lastRequestTime = Number.parseInt(lastRequest, 10); const timeSince = Date.now() - lastRequestTime; - const cooldownMs = emailUpdateTimes.coolDownMinutes * 60 * 1000; + const cooldownMs = cacheTTL.emailChangeCooldown * 1000; if (timeSince < cooldownMs) { const remainingMs = cooldownMs - timeSince; const remainingMinutes = Math.ceil(remainingMs / (60 * 1000)); const response: EmailChangeResponse = { - code: 429, + code: httpStatus.TOO_MANY_REQUESTS, success: false, error: `Please wait ${remainingMinutes} minute(s) before requesting another email change`, cooldownRemaining: remainingMinutes, }; - return Response.json(response, { status: 429 }); + return Response.json(response, { status: httpStatus.TOO_MANY_REQUESTS }); } } @@ -157,15 +165,15 @@ async function handleEmailChangeRequest( if (existingEmailResult.rows.length > 0) { const response: EmailChangeResponse = { - code: 409, + code: httpStatus.CONFLICT, success: false, - error: "Email already exists", + error: errorMessages.EMAIL_ALREADY_EXISTS, }; - return Response.json(response, { status: 409 }); + return Response.json(response, { status: httpStatus.CONFLICT }); } const verificationToken = Bun.randomUUIDv7(); - const emailChangeKey = `email-change:${verificationToken}`; + const emailChangeKey = generateCacheKey.emailChange(verificationToken); const now = Date.now(); try { @@ -180,14 +188,14 @@ async function handleEmailChangeRequest( emailChangeKey, JSON.stringify(emailChangeData), "EX", - emailUpdateTimes.tokenExpiryHours * 60 * 60, + cacheTTL.emailChange, ); await redis.set( cooldownKey, now.toString(), "EX", - emailUpdateTimes.coolDownMinutes * 60, + cacheTTL.emailChangeCooldown, ); // send verification email to NEW email @@ -198,7 +206,7 @@ async function handleEmailChangeRequest( displayName: currentUser.display_name || currentUser.username, currentEmail: currentUser.email, newEmail: normalizedEmail, - willExpire: `This link will expire in ${emailUpdateTimes.tokenExpiryHours} hours`, + willExpire: `This link will expire in ${formatSecondsToHighestUnit(cacheTTL.emailChange)}`, verificationUrl: `${environment.frontendFqdn}/user/email?token=${verificationToken}`, supportEmail: extraValues.supportEmail, }; @@ -219,7 +227,7 @@ async function handleEmailChangeRequest( currentEmail: currentUser.email, newEmail: normalizedEmail, requestTime: new Date().toLocaleString(), - willExpire: `This request will expire in ${emailUpdateTimes.tokenExpiryHours} hours`, + willExpire: `This request will expire in ${formatSecondsToHighestUnit(cacheTTL.emailChange)}`, supportEmail: extraValues.supportEmail, }; @@ -240,11 +248,11 @@ async function handleEmailChangeRequest( } const response: EmailChangeResponse = { - code: 200, + code: httpStatus.OK, success: true, message: `Email change verification sent to ${normalizedEmail}. Please check your new email to confirm the change. A notification has also been sent to your current email.`, }; - return Response.json(response, { status: 200 }); + return Response.json(response, { status: httpStatus.OK }); } catch (error) { await redis.del(emailChangeKey).catch(() => {}); await redis.del(cooldownKey).catch(() => {}); @@ -258,11 +266,13 @@ async function handleEmailChangeRequest( }); const response: EmailChangeResponse = { - code: 500, + code: httpStatus.INTERNAL_SERVER_ERROR, success: false, - error: "Failed to send verification email. Please try again.", + error: errorMessages.EMAIL_SEND_FAILED, }; - return Response.json(response, { status: 500 }); + return Response.json(response, { + status: httpStatus.INTERNAL_SERVER_ERROR, + }); } } @@ -274,23 +284,23 @@ async function handleEmailVerification( if (!token || typeof token !== "string" || token.trim() === "") { const response: EmailChangeResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, error: "Email change verification token is required", }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } - const emailChangeKey = `email-change:${token}`; + const emailChangeKey = generateCacheKey.emailChange(token); const emailChangeDataRaw = await redis.get(emailChangeKey); if (!emailChangeDataRaw) { const response: EmailChangeResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, - error: "Invalid or expired email change token", + error: errorMessages.INVALID_TOKEN, }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } let emailChangeData: EmailChangeData; @@ -300,11 +310,11 @@ async function handleEmailVerification( await redis.del(emailChangeKey); const response: EmailChangeResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, - error: "Invalid email change token format", + error: errorMessages.INVALID_TOKEN_FORMAT, }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } if ( @@ -315,11 +325,11 @@ async function handleEmailVerification( await redis.del(emailChangeKey); const response: EmailChangeResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, error: "Invalid email change data", }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } const userQuery = ` @@ -334,11 +344,11 @@ async function handleEmailVerification( await redis.del(emailChangeKey); const response: EmailChangeResponse = { - code: 404, + code: httpStatus.NOT_FOUND, success: false, - error: "User not found", + error: errorMessages.USER_NOT_FOUND, }; - return Response.json(response, { status: 404 }); + return Response.json(response, { status: httpStatus.NOT_FOUND }); } const user = userResult.rows[0]; @@ -346,23 +356,23 @@ async function handleEmailVerification( await redis.del(emailChangeKey); const response: EmailChangeResponse = { - code: 404, + code: httpStatus.NOT_FOUND, success: false, - error: "User not found", + error: errorMessages.USER_NOT_FOUND, }; - return Response.json(response, { status: 404 }); + return Response.json(response, { status: httpStatus.NOT_FOUND }); } if (user.email !== emailChangeData.currentEmail) { await redis.del(emailChangeKey); const response: EmailChangeResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, error: "Email change token is no longer valid - current email has changed", }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } const existingEmailQuery = "SELECT id FROM users WHERE email = ? LIMIT 1"; @@ -377,11 +387,11 @@ async function handleEmailVerification( await redis.del(emailChangeKey); const response: EmailChangeResponse = { - code: 409, + code: httpStatus.CONFLICT, success: false, error: "New email address is no longer available", }; - return Response.json(response, { status: 409 }); + return Response.json(response, { status: httpStatus.CONFLICT }); } const updateQuery = ` @@ -434,11 +444,13 @@ async function handleEmailVerification( const updatedUser = updatedUserResult.rows[0]; if (!updatedUser) { const response: EmailChangeResponse = { - code: 500, + code: httpStatus.INTERNAL_SERVER_ERROR, success: false, error: "Failed to fetch updated user data", }; - return Response.json(response, { status: 500 }); + return Response.json(response, { + status: httpStatus.INTERNAL_SERVER_ERROR, + }); } let sessionCookie: string | undefined; @@ -480,14 +492,14 @@ async function handleEmailVerification( }; const response: EmailChangeResponse = { - code: 200, + code: httpStatus.OK, success: true, message: `Email successfully changed to ${updatedUser.email} and verified.`, user: responseUser, }; return Response.json(response, { - status: 200, + status: httpStatus.OK, headers: sessionCookie ? { "Set-Cookie": sessionCookie } : {}, }); } diff --git a/src/routes/user/update/info.ts b/src/routes/user/update/info.ts index 207535a..82d6bdb 100644 --- a/src/routes/user/update/info.ts +++ b/src/routes/user/update/info.ts @@ -1,4 +1,9 @@ import { echo } from "@atums/echo"; +import { + errorMessages, + httpStatus, + successMessages, +} from "#environment/constants"; import { sessionManager } from "#lib/auth"; import { cassandra } from "#lib/database"; import { isValidDisplayName, isValidUsername } from "#lib/validation"; @@ -28,22 +33,22 @@ async function handler( if (!session) { const response: UpdateInfoResponse = { - code: 401, + code: httpStatus.UNAUTHORIZED, success: false, - error: "Not authenticated", + error: errorMessages.NOT_AUTHENTICATED, }; - return Response.json(response, { status: 401 }); + return Response.json(response, { status: httpStatus.UNAUTHORIZED }); } const { username, displayName } = requestBody as UpdateInfoRequest; if (username === undefined && displayName === undefined) { const response: UpdateInfoResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, error: "At least one field must be provided (username, displayName)", }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } const currentUserQuery = ` @@ -59,21 +64,21 @@ async function handler( await sessionManager.invalidateSession(request); const response: UpdateInfoResponse = { - code: 404, + code: httpStatus.NOT_FOUND, success: false, - error: "User not found", + error: errorMessages.USER_NOT_FOUND, }; - return Response.json(response, { status: 404 }); + return Response.json(response, { status: httpStatus.NOT_FOUND }); } const currentUser = currentUserResult.rows[0]; if (!currentUser) { const response: UpdateInfoResponse = { - code: 404, + code: httpStatus.NOT_FOUND, success: false, - error: "User not found", + error: errorMessages.USER_NOT_FOUND, }; - return Response.json(response, { status: 404 }); + return Response.json(response, { status: httpStatus.NOT_FOUND }); } const updates: { @@ -85,11 +90,11 @@ async function handler( const usernameValidation = isValidUsername(username); if (!usernameValidation.valid || !usernameValidation.username) { const response: UpdateInfoResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, error: usernameValidation.error || "Invalid username", }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } if (usernameValidation.username !== currentUser.username) { @@ -105,11 +110,11 @@ async function handler( existingUsernameResult.rows[0]?.id !== session.id ) { const response: UpdateInfoResponse = { - code: 409, + code: httpStatus.CONFLICT, success: false, - error: "Username already exists", + error: errorMessages.USERNAME_ALREADY_EXISTS, }; - return Response.json(response, { status: 409 }); + return Response.json(response, { status: httpStatus.CONFLICT }); } updates.username = usernameValidation.username; @@ -123,11 +128,11 @@ async function handler( const displayNameValidation = isValidDisplayName(displayName); if (!displayNameValidation.valid) { const response: UpdateInfoResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, error: displayNameValidation.error || "Invalid display name", }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } updates.displayName = displayNameValidation.name || null; } @@ -135,7 +140,7 @@ async function handler( if (Object.keys(updates).length === 0) { const response: UpdateInfoResponse = { - code: 200, + code: httpStatus.OK, success: true, message: "No changes required", user: { @@ -147,7 +152,7 @@ async function handler( createdAt: currentUser.created_at.toISOString(), }, }; - return Response.json(response, { status: 200 }); + return Response.json(response, { status: httpStatus.OK }); } const updateFields: string[] = []; @@ -182,11 +187,13 @@ async function handler( const updatedUser = updatedUserResult.rows[0]; if (!updatedUser) { const response: UpdateInfoResponse = { - code: 500, + code: httpStatus.INTERNAL_SERVER_ERROR, success: false, error: "Failed to fetch updated user data", }; - return Response.json(response, { status: 500 }); + return Response.json(response, { + status: httpStatus.INTERNAL_SERVER_ERROR, + }); } const userAgent = request.headers.get("User-Agent") || "Unknown"; @@ -216,14 +223,14 @@ async function handler( }; const response: UpdateInfoResponse = { - code: 200, + code: httpStatus.OK, success: true, - message: "User information updated successfully", + message: successMessages.USER_INFO_UPDATED, user: responseUser, }; return Response.json(response, { - status: 200, + status: httpStatus.OK, headers: { "Set-Cookie": sessionCookie, }, @@ -235,11 +242,13 @@ async function handler( }); const response: UpdateInfoResponse = { - code: 500, + code: httpStatus.INTERNAL_SERVER_ERROR, success: false, - error: "Internal server error", + error: errorMessages.INTERNAL_SERVER_ERROR, }; - return Response.json(response, { status: 500 }); + return Response.json(response, { + status: httpStatus.INTERNAL_SERVER_ERROR, + }); } } diff --git a/src/routes/user/update/password.ts b/src/routes/user/update/password.ts index e8f009e..a29b93c 100644 --- a/src/routes/user/update/password.ts +++ b/src/routes/user/update/password.ts @@ -1,4 +1,10 @@ import { echo } from "@atums/echo"; +import { + errorMessages, + httpStatus, + passwordHashing, + successMessages, +} from "#environment/constants"; import { sessionManager } from "#lib/auth"; import { cassandra } from "#lib/database"; import { isValidPassword } from "#lib/validation"; @@ -27,11 +33,11 @@ async function handler( if (!session) { const response: UpdatePasswordResponse = { - code: 401, + code: httpStatus.UNAUTHORIZED, success: false, - error: "Not authenticated", + error: errorMessages.NOT_AUTHENTICATED, }; - return Response.json(response, { status: 401 }); + return Response.json(response, { status: httpStatus.UNAUTHORIZED }); } const { currentPassword, newPassword, logoutAllSessions } = @@ -39,30 +45,30 @@ async function handler( if (!currentPassword || !newPassword) { const response: UpdatePasswordResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, error: "Both currentPassword and newPassword are required", }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } const passwordValidation = isValidPassword(newPassword); if (!passwordValidation.valid) { const response: UpdatePasswordResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, error: passwordValidation.error || "Invalid new password", }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } if (currentPassword === newPassword) { const response: UpdatePasswordResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, - error: "New password must be different from current password", + error: errorMessages.PASSWORD_SAME_AS_CURRENT, }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } const userQuery = ` @@ -78,21 +84,21 @@ async function handler( await sessionManager.invalidateSession(request); const response: UpdatePasswordResponse = { - code: 404, + code: httpStatus.NOT_FOUND, success: false, - error: "User not found", + error: errorMessages.USER_NOT_FOUND, }; - return Response.json(response, { status: 404 }); + return Response.json(response, { status: httpStatus.NOT_FOUND }); } const user = userResult.rows[0]; if (!user) { const response: UpdatePasswordResponse = { - code: 404, + code: httpStatus.NOT_FOUND, success: false, - error: "User not found", + error: errorMessages.USER_NOT_FOUND, }; - return Response.json(response, { status: 404 }); + return Response.json(response, { status: httpStatus.NOT_FOUND }); } const isCurrentPasswordValid = await Bun.password.verify( @@ -102,18 +108,17 @@ async function handler( if (!isCurrentPasswordValid) { const response: UpdatePasswordResponse = { - code: 401, + code: httpStatus.UNAUTHORIZED, success: false, - error: "Current password is incorrect", + error: errorMessages.CURRENT_PASSWORD_INCORRECT, }; - return Response.json(response, { status: 401 }); + return Response.json(response, { status: httpStatus.UNAUTHORIZED }); } - const hashedNewPassword = await Bun.password.hash(newPassword, { - algorithm: "argon2id", - memoryCost: 4096, - timeCost: 3, - }); + const hashedNewPassword = await Bun.password.hash( + newPassword, + passwordHashing, + ); const updateQuery = ` UPDATE users @@ -131,21 +136,25 @@ async function handler( const invalidatedCount = await sessionManager.invalidateAllSessionsForUser(session.id); + const baseMessage = successMessages.PASSWORD_UPDATED; + const sessionMessage = ` Logged out from ${invalidatedCount} session(s).`; + const response: UpdatePasswordResponse = { - code: 200, + code: httpStatus.OK, success: true, - message: `Password updated successfully. Logged out from ${invalidatedCount} session(s).`, + message: baseMessage + sessionMessage, loggedOutSessions: invalidatedCount, }; return Response.json(response, { - status: 200, + status: httpStatus.OK, headers: { "Content-Type": "application/json", "Set-Cookie": "session=; Path=/; Max-Age=0; HttpOnly", }, }); } + const allSessions = await sessionManager.getActiveSessionsForUser( session.id, ); @@ -180,18 +189,21 @@ async function handler( userAgent, ); + const baseMessage = successMessages.PASSWORD_UPDATED; + const sessionMessage = + invalidatedCount > 0 + ? ` Logged out from ${invalidatedCount} other session(s).` + : "."; + const response: UpdatePasswordResponse = { - code: 200, + code: httpStatus.OK, success: true, - message: - invalidatedCount > 0 - ? `Password updated successfully. Logged out from ${invalidatedCount} other session(s).` - : "Password updated successfully.", + message: baseMessage + sessionMessage, loggedOutSessions: invalidatedCount, }; return Response.json(response, { - status: 200, + status: httpStatus.OK, headers: { "Content-Type": "application/json", "Set-Cookie": sessionCookie, @@ -204,11 +216,13 @@ async function handler( }); const response: UpdatePasswordResponse = { - code: 500, + code: httpStatus.INTERNAL_SERVER_ERROR, success: false, - error: "Internal server error", + error: errorMessages.INTERNAL_SERVER_ERROR, }; - return Response.json(response, { status: 500 }); + return Response.json(response, { + status: httpStatus.INTERNAL_SERVER_ERROR, + }); } } diff --git a/src/routes/user/verify.ts b/src/routes/user/verify.ts index 5cf1b28..997e52a 100644 --- a/src/routes/user/verify.ts +++ b/src/routes/user/verify.ts @@ -1,5 +1,11 @@ import { echo } from "@atums/echo"; import { redis } from "bun"; +import { + errorMessages, + generateCacheKey, + httpStatus, + successMessages, +} from "#environment/constants"; import { sessionManager } from "#lib/auth"; import { cassandra } from "#lib/database"; @@ -23,23 +29,23 @@ async function handler(request: ExtendedRequest): Promise { if (!token || typeof token !== "string" || token.trim() === "") { const response: VerifyEmailResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, error: "Verification token is required", }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } - const verificationKey = `mail-verification:${token}`; + const verificationKey = generateCacheKey.mailVerification(token); const verificationDataRaw = await redis.get(verificationKey); if (!verificationDataRaw) { const response: VerifyEmailResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, - error: "Invalid or expired verification token", + error: errorMessages.INVALID_TOKEN, }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } let verificationData: VerificationData; @@ -49,22 +55,22 @@ async function handler(request: ExtendedRequest): Promise { await redis.del(verificationKey); const response: VerifyEmailResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, - error: "Invalid verification token format", + error: errorMessages.INVALID_TOKEN_FORMAT, }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } if (!verificationData.userId || !verificationData.email) { await redis.del(verificationKey); const response: VerifyEmailResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, error: "Invalid verification data", }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } const userQuery = ` @@ -90,11 +96,11 @@ async function handler(request: ExtendedRequest): Promise { await redis.del(verificationKey); const response: VerifyEmailResponse = { - code: 404, + code: httpStatus.NOT_FOUND, success: false, - error: "User not found", + error: errorMessages.USER_NOT_FOUND, }; - return Response.json(response, { status: 404 }); + return Response.json(response, { status: httpStatus.NOT_FOUND }); } const user = userResult.rows[0]; @@ -102,22 +108,22 @@ async function handler(request: ExtendedRequest): Promise { await redis.del(verificationKey); const response: VerifyEmailResponse = { - code: 404, + code: httpStatus.NOT_FOUND, success: false, - error: "User not found", + error: errorMessages.USER_NOT_FOUND, }; - return Response.json(response, { status: 404 }); + return Response.json(response, { status: httpStatus.NOT_FOUND }); } if (user.email !== verificationData.email) { await redis.del(verificationKey); const response: VerifyEmailResponse = { - code: 400, + code: httpStatus.BAD_REQUEST, success: false, error: "Verification token does not match current email address", }; - return Response.json(response, { status: 400 }); + return Response.json(response, { status: httpStatus.BAD_REQUEST }); } if (user.is_verified) { @@ -133,12 +139,12 @@ async function handler(request: ExtendedRequest): Promise { }; const response: VerifyEmailResponse = { - code: 200, + code: httpStatus.OK, success: true, - message: "Email is already verified", + message: errorMessages.EMAIL_ALREADY_VERIFIED, user: responseUser, }; - return Response.json(response, { status: 200 }); + return Response.json(response, { status: httpStatus.OK }); } const updateQuery = ` @@ -168,11 +174,13 @@ async function handler(request: ExtendedRequest): Promise { const updatedUser = updatedUserResult.rows[0]; if (!updatedUser) { const response: VerifyEmailResponse = { - code: 500, + code: httpStatus.INTERNAL_SERVER_ERROR, success: false, error: "Failed to fetch updated user data", }; - return Response.json(response, { status: 500 }); + return Response.json(response, { + status: httpStatus.INTERNAL_SERVER_ERROR, + }); } const { session } = request; @@ -215,14 +223,14 @@ async function handler(request: ExtendedRequest): Promise { }; const response: VerifyEmailResponse = { - code: 200, + code: httpStatus.OK, success: true, - message: "Email verified successfully", + message: successMessages.EMAIL_VERIFIED, user: responseUser, }; return Response.json(response, { - status: 200, + status: httpStatus.OK, headers: { "Set-Cookie": sessionCookie ? sessionCookie : "", }, @@ -235,11 +243,13 @@ async function handler(request: ExtendedRequest): Promise { }); const response: VerifyEmailResponse = { - code: 500, + code: httpStatus.INTERNAL_SERVER_ERROR, success: false, - error: "Internal server error", + error: errorMessages.INTERNAL_SERVER_ERROR, }; - return Response.json(response, { status: 500 }); + return Response.json(response, { + status: httpStatus.INTERNAL_SERVER_ERROR, + }); } } diff --git a/types/server/requests/user/index.ts b/types/server/requests/user/index.ts index a1ba3ad..57127f4 100644 --- a/types/server/requests/user/index.ts +++ b/types/server/requests/user/index.ts @@ -2,6 +2,7 @@ export * from "./base"; export * from "./responses"; export * from "./register"; export * from "./login"; +export * from "./logout"; export * from "./verify"; export * from "./update"; diff --git a/types/server/requests/user/logout.ts b/types/server/requests/user/logout.ts new file mode 100644 index 0000000..b5f2c62 --- /dev/null +++ b/types/server/requests/user/logout.ts @@ -0,0 +1,5 @@ +import type { BaseResponse } from "../base"; + +interface LogoutResponse extends BaseResponse {} + +export type { LogoutResponse };