move alot to constants, fix html
Some checks failed
Code quality checks / biome (push) Failing after 12s
Some checks failed
Code quality checks / biome (push) Failing after 12s
This commit is contained in:
parent
92172479f6
commit
33a602cdd0
26 changed files with 603 additions and 296 deletions
26
src/environment/constants/cache.ts
Normal file
26
src/environment/constants/cache.ts
Normal 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 };
|
54
src/environment/constants/http.ts
Normal file
54
src/environment/constants/http.ts
Normal 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 };
|
|
@ -32,3 +32,5 @@ export * from "./validation";
|
|||
export * from "./database";
|
||||
export * from "./mailer";
|
||||
export * from "./user";
|
||||
export * from "./cache";
|
||||
export * from "./http";
|
||||
|
|
|
@ -1 +1,7 @@
|
|||
export * from "./update";
|
||||
const passwordHashing = {
|
||||
algorithm: "argon2id" as const,
|
||||
memoryCost: 4096,
|
||||
timeCost: 3,
|
||||
} as const;
|
||||
|
||||
export { passwordHashing };
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
const emailUpdateTimes = {
|
||||
coolDownMinutes: 5,
|
||||
tokenExpiryHours: 3,
|
||||
};
|
||||
|
||||
export { emailUpdateTimes };
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./idGenerator";
|
||||
export * from "./time";
|
||||
export * from "./string";
|
||||
|
|
133
src/lib/utils/string.ts
Normal file
133
src/lib/utils/string.ts
Normal 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 };
|
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
displayNameRestrictions,
|
||||
forbiddenDisplayNamePatterns,
|
||||
nameRestrictions,
|
||||
nameRestrictions
|
||||
} from "#environment/constants";
|
||||
|
||||
import type { validationResult } from "#types/lib";
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 } : {},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 response: UpdatePasswordResponse = {
|
||||
code: 200,
|
||||
success: true,
|
||||
message:
|
||||
const baseMessage = successMessages.PASSWORD_UPDATED;
|
||||
const sessionMessage =
|
||||
invalidatedCount > 0
|
||||
? `Password updated successfully. Logged out from ${invalidatedCount} other session(s).`
|
||||
: "Password updated successfully.",
|
||||
? ` Logged out from ${invalidatedCount} other session(s).`
|
||||
: ".";
|
||||
|
||||
const response: UpdatePasswordResponse = {
|
||||
code: httpStatus.OK,
|
||||
success: true,
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
5
types/server/requests/user/logout.ts
Normal file
5
types/server/requests/user/logout.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import type { BaseResponse } from "../base";
|
||||
|
||||
interface LogoutResponse extends BaseResponse {}
|
||||
|
||||
export type { LogoutResponse };
|
Loading…
Add table
Add a link
Reference in a new issue