move alot to constants, fix html
Some checks failed
Code quality checks / biome (push) Failing after 12s

This commit is contained in:
creations 2025-06-14 09:19:55 -04:00
parent 92172479f6
commit 33a602cdd0
Signed by: creations
GPG key ID: 8F553AA4320FC711
26 changed files with 603 additions and 296 deletions

View file

@ -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 };

View file

@ -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 };

View file

@ -32,3 +32,5 @@ export * from "./validation";
export * from "./database";
export * from "./mailer";
export * from "./user";
export * from "./cache";
export * from "./http";

View file

@ -1 +1,7 @@
export * from "./update";
const passwordHashing = {
algorithm: "argon2id" as const,
memoryCost: 4096,
timeCost: 3,
} as const;
export { passwordHashing };

View file

@ -1,6 +0,0 @@
const emailUpdateTimes = {
coolDownMinutes: 5,
tokenExpiryHours: 3,
};
export { emailUpdateTimes };

View file

@ -35,6 +35,6 @@
<p><strong>If this change was not authorized by you:</strong> Contact our support team immediately at {{supportEmail}}. Your account may have been compromised and we will help you recover it.</p>
<hr>
<p><small>User ID: {{id}} | {{companyName}}</small></p>
<p><small>User ID: {{id}}</small></p>
</body>
</html>

View file

@ -36,6 +36,6 @@
<p>Questions? Contact {{supportEmail}}</p>
<hr>
<p><small>User ID: {{id}} | {{companyName}}</small></p>
<p><small>User ID: {{id}}</small></p>
</body>
</html>

View file

@ -36,6 +36,6 @@
<p>If you did not request this email change, contact {{supportEmail}} immediately.</p>
<p>Questions? Contact {{supportEmail}}</p>
<hr>
<p><small>User ID: {{id}} | {{companyName}}</small></p>
<p><small>User ID: {{id}}</small></p>
</body>
</html>

View file

@ -34,6 +34,6 @@
<p>If you did not request this password reset, please contact {{supportEmail}} immediately. Your password will remain unchanged.</p>
<p>Questions? Contact {{supportEmail}}</p>
<hr>
<p><small>User ID: {{id}} | {{companyName}}</small></p>
<p><small>User ID: {{id}}</small></p>
</body>
</html>

View file

@ -32,9 +32,7 @@
<p>{{willExpire}} for security reasons.</p>
<p>Questions? Contact {{supportEmail}}</p>
<p>Best regards,<br>
The {{companyName}} team</p>
<hr>
<p><small>User ID: {{id}} | {{companyName}}</small></p>
<p><small>User ID: {{id}}</small></p>
</body>
</html>

View file

@ -1,2 +1,3 @@
export * from "./idGenerator";
export * from "./time";
export * from "./string";

133
src/lib/utils/string.ts Normal file
View file

@ -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 };

View file

@ -1,7 +1,7 @@
import {
displayNameRestrictions,
forbiddenDisplayNamePatterns,
nameRestrictions,
nameRestrictions
} from "#environment/constants";
import type { validationResult } from "#types/lib";

View file

@ -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<Response> {
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<Response> {
};
return Response.json(response, {
status: isHealthy ? 200 : 503,
status: isHealthy ? httpStatus.OK : httpStatus.SERVICE_UNAVAILABLE,
});
}

View file

@ -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<Response> {
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<Response> {
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<Response> {
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<Response> {
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<Response> {
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<Response> {
}
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<Response> {
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<Response> {
});
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,
});
}
}

View file

@ -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,
});
}
}

View file

@ -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,
});
}
}

View file

@ -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<Response> {
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<Response> {
});
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,
});
}
}

View file

@ -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 = {

View file

@ -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,
});
}
}

View file

@ -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 } : {},
});
}

View file

@ -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,
});
}
}

View file

@ -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,
});
}
}

View file

@ -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<Response> {
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<Response> {
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<Response> {
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<Response> {
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<Response> {
};
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<Response> {
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<Response> {
};
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<Response> {
});
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,
});
}
}

View file

@ -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";

View file

@ -0,0 +1,5 @@
import type { BaseResponse } from "../base";
interface LogoutResponse extends BaseResponse {}
export type { LogoutResponse };