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 "./database";
|
||||||
export * from "./mailer";
|
export * from "./mailer";
|
||||||
export * from "./user";
|
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>
|
<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>
|
<hr>
|
||||||
<p><small>User ID: {{id}} | {{companyName}}</small></p>
|
<p><small>User ID: {{id}}</small></p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -36,6 +36,6 @@
|
||||||
|
|
||||||
<p>Questions? Contact {{supportEmail}}</p>
|
<p>Questions? Contact {{supportEmail}}</p>
|
||||||
<hr>
|
<hr>
|
||||||
<p><small>User ID: {{id}} | {{companyName}}</small></p>
|
<p><small>User ID: {{id}}</small></p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -36,6 +36,6 @@
|
||||||
<p>If you did not request this email change, contact {{supportEmail}} immediately.</p>
|
<p>If you did not request this email change, contact {{supportEmail}} immediately.</p>
|
||||||
<p>Questions? Contact {{supportEmail}}</p>
|
<p>Questions? Contact {{supportEmail}}</p>
|
||||||
<hr>
|
<hr>
|
||||||
<p><small>User ID: {{id}} | {{companyName}}</small></p>
|
<p><small>User ID: {{id}}</small></p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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>If you did not request this password reset, please contact {{supportEmail}} immediately. Your password will remain unchanged.</p>
|
||||||
<p>Questions? Contact {{supportEmail}}</p>
|
<p>Questions? Contact {{supportEmail}}</p>
|
||||||
<hr>
|
<hr>
|
||||||
<p><small>User ID: {{id}} | {{companyName}}</small></p>
|
<p><small>User ID: {{id}}</small></p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -32,9 +32,7 @@
|
||||||
|
|
||||||
<p>{{willExpire}} for security reasons.</p>
|
<p>{{willExpire}} for security reasons.</p>
|
||||||
<p>Questions? Contact {{supportEmail}}</p>
|
<p>Questions? Contact {{supportEmail}}</p>
|
||||||
<p>Best regards,<br>
|
|
||||||
The {{companyName}} team</p>
|
|
||||||
<hr>
|
<hr>
|
||||||
<p><small>User ID: {{id}} | {{companyName}}</small></p>
|
<p><small>User ID: {{id}}</small></p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export * from "./idGenerator";
|
export * from "./idGenerator";
|
||||||
export * from "./time";
|
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 {
|
import {
|
||||||
displayNameRestrictions,
|
displayNameRestrictions,
|
||||||
forbiddenDisplayNamePatterns,
|
forbiddenDisplayNamePatterns,
|
||||||
nameRestrictions,
|
nameRestrictions
|
||||||
} from "#environment/constants";
|
} from "#environment/constants";
|
||||||
|
|
||||||
import type { validationResult } from "#types/lib";
|
import type { validationResult } from "#types/lib";
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { redis } from "bun";
|
import { redis } from "bun";
|
||||||
|
import { httpStatus } from "#environment/constants";
|
||||||
import { cassandra } from "#lib/database";
|
import { cassandra } from "#lib/database";
|
||||||
|
|
||||||
import type { ExtendedRequest, HealthResponse, RouteDef } from "#types/server";
|
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 isHealthy = cassandraHealth.connected && redisHealth === "healthy";
|
||||||
|
|
||||||
const response: HealthResponse = {
|
const response: HealthResponse = {
|
||||||
code: isHealthy ? 200 : 503,
|
code: isHealthy ? httpStatus.OK : httpStatus.SERVICE_UNAVAILABLE,
|
||||||
success: isHealthy,
|
success: isHealthy,
|
||||||
message: isHealthy
|
message: isHealthy
|
||||||
? "All services are healthy"
|
? "All services are healthy"
|
||||||
|
@ -44,7 +45,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
};
|
};
|
||||||
|
|
||||||
return Response.json(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 { echo } from "@atums/echo";
|
||||||
|
import { errorMessages, httpStatus } from "#environment/constants";
|
||||||
import { cassandra } from "#lib/database";
|
import { cassandra } from "#lib/database";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
@ -18,9 +19,7 @@ const routeDef: RouteDef = {
|
||||||
async function handler(request: ExtendedRequest): Promise<Response> {
|
async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const { id: identifier } = request.params;
|
const { id: identifier } = request.params;
|
||||||
|
|
||||||
const { session } = request;
|
const { session } = request;
|
||||||
|
|
||||||
let userQuery: string;
|
let userQuery: string;
|
||||||
let queryParams: string[];
|
let queryParams: string[];
|
||||||
let targetUser: UserRow | null = null;
|
let targetUser: UserRow | null = null;
|
||||||
|
@ -28,11 +27,11 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
if (!identifier) {
|
if (!identifier) {
|
||||||
if (!session) {
|
if (!session) {
|
||||||
const response: UserInfoResponse = {
|
const response: UserInfoResponse = {
|
||||||
code: 401,
|
code: httpStatus.UNAUTHORIZED,
|
||||||
success: false,
|
success: false,
|
||||||
error: "Not authenticated",
|
error: errorMessages.NOT_AUTHENTICATED,
|
||||||
};
|
};
|
||||||
return Response.json(response, { status: 401 });
|
return Response.json(response, { status: httpStatus.UNAUTHORIZED });
|
||||||
}
|
}
|
||||||
|
|
||||||
userQuery = `
|
userQuery = `
|
||||||
|
@ -64,11 +63,13 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
|
|
||||||
if (!userResult?.rows || !Array.isArray(userResult.rows)) {
|
if (!userResult?.rows || !Array.isArray(userResult.rows)) {
|
||||||
const response: UserInfoResponse = {
|
const response: UserInfoResponse = {
|
||||||
code: 500,
|
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||||
success: false,
|
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) {
|
if (userResult.rows.length === 0) {
|
||||||
|
@ -91,11 +92,11 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
|
|
||||||
if (!targetUser) {
|
if (!targetUser) {
|
||||||
const response: UserInfoResponse = {
|
const response: UserInfoResponse = {
|
||||||
code: 404,
|
code: httpStatus.NOT_FOUND,
|
||||||
success: false,
|
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 {
|
} else {
|
||||||
targetUser = userResult.rows[0] || null;
|
targetUser = userResult.rows[0] || null;
|
||||||
|
@ -103,11 +104,11 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
|
|
||||||
if (!targetUser) {
|
if (!targetUser) {
|
||||||
const response: UserInfoResponse = {
|
const response: UserInfoResponse = {
|
||||||
code: 404,
|
code: httpStatus.NOT_FOUND,
|
||||||
success: false,
|
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;
|
const isOwnProfile = session?.id === targetUser.id;
|
||||||
|
@ -135,7 +136,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const response: UserInfoResponse = {
|
const response: UserInfoResponse = {
|
||||||
code: 200,
|
code: httpStatus.OK,
|
||||||
success: true,
|
success: true,
|
||||||
message: isOwnProfile
|
message: isOwnProfile
|
||||||
? "User information retrieved successfully"
|
? "User information retrieved successfully"
|
||||||
|
@ -143,7 +144,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
user: responseUser,
|
user: responseUser,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Response.json(response, { status: 200 });
|
return Response.json(response, { status: httpStatus.OK });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
echo.error({
|
echo.error({
|
||||||
message: "Error retrieving user information",
|
message: "Error retrieving user information",
|
||||||
|
@ -151,11 +152,13 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: UserInfoResponse = {
|
const response: UserInfoResponse = {
|
||||||
code: 500,
|
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||||
success: false,
|
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 { echo } from "@atums/echo";
|
||||||
import { redis } from "bun";
|
import { redis } from "bun";
|
||||||
import { environment } from "#environment/config";
|
import { environment } from "#environment/config";
|
||||||
|
import {
|
||||||
|
cacheTTL,
|
||||||
|
errorMessages,
|
||||||
|
generateCacheKey,
|
||||||
|
httpStatus,
|
||||||
|
successMessages,
|
||||||
|
} from "#environment/constants";
|
||||||
import { extraValues } from "#environment/extra";
|
import { extraValues } from "#environment/extra";
|
||||||
import { cassandra } from "#lib/database";
|
import { cassandra } from "#lib/database";
|
||||||
import { mailerService } from "#lib/mailer";
|
import { mailerService } from "#lib/mailer";
|
||||||
|
import { formatSecondsToHighestUnit } from "#lib/utils";
|
||||||
import { isValidEmail } from "#lib/validation";
|
import { isValidEmail } from "#lib/validation";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
@ -29,21 +37,21 @@ async function handler(
|
||||||
|
|
||||||
if (!email) {
|
if (!email) {
|
||||||
const response: BaseResponse = {
|
const response: BaseResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
success: false,
|
||||||
error: "Email is required",
|
error: "Email is required",
|
||||||
};
|
};
|
||||||
return Response.json(response, { status: 400 });
|
return Response.json(response, { status: httpStatus.BAD_REQUEST });
|
||||||
}
|
}
|
||||||
|
|
||||||
const emailValidation = isValidEmail(email);
|
const emailValidation = isValidEmail(email);
|
||||||
if (!emailValidation.valid) {
|
if (!emailValidation.valid) {
|
||||||
const response: BaseResponse = {
|
const response: BaseResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
success: false,
|
||||||
error: emailValidation.error || "Invalid email",
|
error: emailValidation.error || "Invalid email",
|
||||||
};
|
};
|
||||||
return Response.json(response, { status: 400 });
|
return Response.json(response, { status: httpStatus.BAD_REQUEST });
|
||||||
}
|
}
|
||||||
|
|
||||||
const userQuery = `
|
const userQuery = `
|
||||||
|
@ -64,25 +72,25 @@ async function handler(
|
||||||
|
|
||||||
if (!userResult?.rows || userResult.rows.length === 0) {
|
if (!userResult?.rows || userResult.rows.length === 0) {
|
||||||
const response: BaseResponse = {
|
const response: BaseResponse = {
|
||||||
code: 200,
|
code: httpStatus.OK,
|
||||||
success: true,
|
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];
|
const user = userResult.rows[0];
|
||||||
if (!user) {
|
if (!user) {
|
||||||
const response: BaseResponse = {
|
const response: BaseResponse = {
|
||||||
code: 200,
|
code: httpStatus.OK,
|
||||||
success: true,
|
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 resetToken = Bun.randomUUIDv7();
|
||||||
const resetKey = `password-reset:${resetToken}`;
|
const resetKey = generateCacheKey.passwordReset(resetToken);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await redis.set(
|
await redis.set(
|
||||||
|
@ -92,7 +100,7 @@ async function handler(
|
||||||
email: user.email,
|
email: user.email,
|
||||||
}),
|
}),
|
||||||
"EX",
|
"EX",
|
||||||
1 * 60 * 60, // 1 hour
|
cacheTTL.passwordReset,
|
||||||
);
|
);
|
||||||
|
|
||||||
const emailVariables = {
|
const emailVariables = {
|
||||||
|
@ -100,7 +108,7 @@ async function handler(
|
||||||
companyName: extraValues.companyName,
|
companyName: extraValues.companyName,
|
||||||
id: user.id,
|
id: user.id,
|
||||||
displayName: user.display_name || user.username,
|
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}`,
|
resetUrl: `${environment.frontendFqdn}/user/reset-password?token=${resetToken}`,
|
||||||
supportEmail: extraValues.supportEmail,
|
supportEmail: extraValues.supportEmail,
|
||||||
};
|
};
|
||||||
|
@ -113,11 +121,11 @@ async function handler(
|
||||||
);
|
);
|
||||||
|
|
||||||
const response: BaseResponse = {
|
const response: BaseResponse = {
|
||||||
code: 200,
|
code: httpStatus.OK,
|
||||||
success: true,
|
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) {
|
} catch (error) {
|
||||||
await redis.del(resetKey).catch(() => {});
|
await redis.del(resetKey).catch(() => {});
|
||||||
|
|
||||||
|
@ -129,11 +137,13 @@ async function handler(
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: BaseResponse = {
|
const response: BaseResponse = {
|
||||||
code: 500,
|
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||||
success: false,
|
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) {
|
} catch (error) {
|
||||||
echo.error({
|
echo.error({
|
||||||
|
@ -142,11 +152,13 @@ async function handler(
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: BaseResponse = {
|
const response: BaseResponse = {
|
||||||
code: 500,
|
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||||
success: false,
|
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 { echo } from "@atums/echo";
|
||||||
|
import {
|
||||||
|
errorMessages,
|
||||||
|
httpStatus,
|
||||||
|
successMessages,
|
||||||
|
} from "#environment/constants";
|
||||||
import { sessionManager } from "#lib/auth";
|
import { sessionManager } from "#lib/auth";
|
||||||
import { cassandra } from "#lib/database";
|
import { cassandra } from "#lib/database";
|
||||||
import { isValidEmail, isValidUsername } from "#lib/validation";
|
import { isValidEmail, isValidUsername } from "#lib/validation";
|
||||||
|
@ -30,22 +35,21 @@ async function handler(
|
||||||
const existingSession = await sessionManager.getSession(request);
|
const existingSession = await sessionManager.getSession(request);
|
||||||
if (existingSession) {
|
if (existingSession) {
|
||||||
const response: LoginResponse = {
|
const response: LoginResponse = {
|
||||||
code: 409,
|
code: httpStatus.CONFLICT,
|
||||||
success: false,
|
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) {
|
if (!identifier || !password) {
|
||||||
const response: LoginResponse = {
|
const response: LoginResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
success: false,
|
||||||
error:
|
error: errorMessages.MISSING_REQUIRED_FIELDS,
|
||||||
"Missing required fields: identifier (username or email), password",
|
|
||||||
};
|
};
|
||||||
return Response.json(response, { status: 400 });
|
return Response.json(response, { status: httpStatus.BAD_REQUEST });
|
||||||
}
|
}
|
||||||
|
|
||||||
const isEmail = isValidEmail(identifier).valid;
|
const isEmail = isValidEmail(identifier).valid;
|
||||||
|
@ -53,11 +57,11 @@ async function handler(
|
||||||
|
|
||||||
if (!isEmail && !isUsername) {
|
if (!isEmail && !isUsername) {
|
||||||
const response: LoginResponse = {
|
const response: LoginResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
success: false,
|
||||||
error: "Invalid identifier format - must be a valid username or email",
|
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;
|
let userQuery: string;
|
||||||
|
@ -83,43 +87,45 @@ async function handler(
|
||||||
|
|
||||||
if (!userResult?.rows || !Array.isArray(userResult.rows)) {
|
if (!userResult?.rows || !Array.isArray(userResult.rows)) {
|
||||||
const response: LoginResponse = {
|
const response: LoginResponse = {
|
||||||
code: 500,
|
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||||
success: false,
|
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) {
|
if (userResult.rows.length === 0) {
|
||||||
const response: LoginResponse = {
|
const response: LoginResponse = {
|
||||||
code: 401,
|
code: httpStatus.UNAUTHORIZED,
|
||||||
success: false,
|
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];
|
const user = userResult.rows[0];
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
const response: LoginResponse = {
|
const response: LoginResponse = {
|
||||||
code: 401,
|
code: httpStatus.UNAUTHORIZED,
|
||||||
success: false,
|
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);
|
const isPasswordValid = await Bun.password.verify(password, user.password);
|
||||||
|
|
||||||
if (!isPasswordValid) {
|
if (!isPasswordValid) {
|
||||||
const response: LoginResponse = {
|
const response: LoginResponse = {
|
||||||
code: 401,
|
code: httpStatus.UNAUTHORIZED,
|
||||||
success: false,
|
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";
|
const userAgent = request.headers.get("User-Agent") || "Unknown";
|
||||||
|
@ -148,14 +154,14 @@ async function handler(
|
||||||
};
|
};
|
||||||
|
|
||||||
const response: LoginResponse = {
|
const response: LoginResponse = {
|
||||||
code: 200,
|
code: httpStatus.OK,
|
||||||
success: true,
|
success: true,
|
||||||
message: "Login successful",
|
message: successMessages.LOGIN_SUCCESSFUL,
|
||||||
user: responseUser,
|
user: responseUser,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Response.json(response, {
|
return Response.json(response, {
|
||||||
status: 200,
|
status: httpStatus.OK,
|
||||||
headers: {
|
headers: {
|
||||||
"Set-Cookie": sessionCookie,
|
"Set-Cookie": sessionCookie,
|
||||||
},
|
},
|
||||||
|
@ -167,11 +173,13 @@ async function handler(
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: LoginResponse = {
|
const response: LoginResponse = {
|
||||||
code: 500,
|
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||||
success: false,
|
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 { echo } from "@atums/echo";
|
||||||
|
import {
|
||||||
|
errorMessages,
|
||||||
|
httpStatus,
|
||||||
|
successMessages,
|
||||||
|
} from "#environment/constants";
|
||||||
import { cookieService, sessionManager } from "#lib/auth";
|
import { cookieService, sessionManager } from "#lib/auth";
|
||||||
|
|
||||||
import type { BaseResponse, ExtendedRequest, RouteDef } from "#types/server";
|
import type { ExtendedRequest, LogoutResponse, RouteDef } from "#types/server";
|
||||||
|
|
||||||
interface LogoutResponse extends BaseResponse {}
|
|
||||||
|
|
||||||
const routeDef: RouteDef = {
|
const routeDef: RouteDef = {
|
||||||
method: ["POST", "DELETE"],
|
method: ["POST", "DELETE"],
|
||||||
|
@ -17,24 +20,24 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
const response: LogoutResponse = {
|
const response: LogoutResponse = {
|
||||||
code: 401,
|
code: httpStatus.UNAUTHORIZED,
|
||||||
success: false,
|
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);
|
await sessionManager.invalidateSession(request);
|
||||||
const clearCookie = cookieService.clearCookie();
|
const clearCookie = cookieService.clearCookie();
|
||||||
|
|
||||||
const response: LogoutResponse = {
|
const response: LogoutResponse = {
|
||||||
code: 200,
|
code: httpStatus.OK,
|
||||||
success: true,
|
success: true,
|
||||||
message: "Logged out successfully",
|
message: successMessages.LOGOUT_SUCCESSFUL,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Response.json(response, {
|
return Response.json(response, {
|
||||||
status: 200,
|
status: httpStatus.OK,
|
||||||
headers: {
|
headers: {
|
||||||
"Set-Cookie": clearCookie,
|
"Set-Cookie": clearCookie,
|
||||||
},
|
},
|
||||||
|
@ -46,11 +49,13 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: LogoutResponse = {
|
const response: LogoutResponse = {
|
||||||
code: 500,
|
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||||
success: false,
|
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 { echo } from "@atums/echo";
|
||||||
import { redis } from "bun";
|
import { redis } from "bun";
|
||||||
import { environment } from "#environment/config";
|
import { environment } from "#environment/config";
|
||||||
|
import {
|
||||||
|
cacheTTL,
|
||||||
|
generateCacheKey,
|
||||||
|
passwordHashing,
|
||||||
|
} from "#environment/constants";
|
||||||
import { extraValues } from "#environment/extra";
|
import { extraValues } from "#environment/extra";
|
||||||
import { cassandra } from "#lib/database";
|
import { cassandra } from "#lib/database";
|
||||||
import { mailerService } from "#lib/mailer";
|
import { mailerService } from "#lib/mailer";
|
||||||
|
@ -11,6 +16,7 @@ import {
|
||||||
isValidPassword,
|
isValidPassword,
|
||||||
isValidUsername,
|
isValidUsername,
|
||||||
} from "#lib/validation";
|
} from "#lib/validation";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExtendedRequest,
|
ExtendedRequest,
|
||||||
RegisterRequest,
|
RegisterRequest,
|
||||||
|
@ -119,11 +125,7 @@ async function handler(
|
||||||
const userId = pika.gen("user");
|
const userId = pika.gen("user");
|
||||||
const verificationToken = Bun.randomUUIDv7();
|
const verificationToken = Bun.randomUUIDv7();
|
||||||
|
|
||||||
const hashedPassword = await Bun.password.hash(password, {
|
const hashedPassword = await Bun.password.hash(password, passwordHashing);
|
||||||
algorithm: "argon2id",
|
|
||||||
memoryCost: 4096,
|
|
||||||
timeCost: 3,
|
|
||||||
});
|
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const insertUserQuery = `
|
const insertUserQuery = `
|
||||||
|
@ -155,13 +157,13 @@ async function handler(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await redis.set(
|
await redis.set(
|
||||||
`mail-verification:${verificationToken}`,
|
generateCacheKey.mailVerification(verificationToken),
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
userId: responseUser.id,
|
userId: responseUser.id,
|
||||||
email: responseUser.email,
|
email: responseUser.email,
|
||||||
}),
|
}),
|
||||||
"EX",
|
"EX",
|
||||||
3 * 60 * 60, // 3h
|
cacheTTL.mailVerification,
|
||||||
);
|
);
|
||||||
|
|
||||||
const emailVariables = {
|
const emailVariables = {
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
import { echo } from "@atums/echo";
|
import { echo } from "@atums/echo";
|
||||||
import { redis } from "bun";
|
import { redis } from "bun";
|
||||||
|
import {
|
||||||
|
errorMessages,
|
||||||
|
generateCacheKey,
|
||||||
|
httpStatus,
|
||||||
|
passwordHashing,
|
||||||
|
} from "#environment/constants";
|
||||||
import { sessionManager } from "#lib/auth";
|
import { sessionManager } from "#lib/auth";
|
||||||
import { cassandra } from "#lib/database";
|
import { cassandra } from "#lib/database";
|
||||||
import { isValidPassword } from "#lib/validation";
|
import { isValidPassword } from "#lib/validation";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExtendedRequest,
|
ExtendedRequest,
|
||||||
ResetData,
|
ResetData,
|
||||||
|
@ -32,33 +39,33 @@ async function handler(
|
||||||
|
|
||||||
if (!token || !newPassword) {
|
if (!token || !newPassword) {
|
||||||
const response: ResetPasswordResponse = {
|
const response: ResetPasswordResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
success: false,
|
||||||
error: "Token and new password are required",
|
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);
|
const passwordValidation = isValidPassword(newPassword);
|
||||||
if (!passwordValidation.valid) {
|
if (!passwordValidation.valid) {
|
||||||
const response: ResetPasswordResponse = {
|
const response: ResetPasswordResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
success: false,
|
||||||
error: passwordValidation.error || "Invalid password",
|
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);
|
const resetDataRaw = await redis.get(resetKey);
|
||||||
|
|
||||||
if (!resetDataRaw) {
|
if (!resetDataRaw) {
|
||||||
const response: ResetPasswordResponse = {
|
const response: ResetPasswordResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
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;
|
let resetData: ResetData;
|
||||||
|
@ -67,21 +74,21 @@ async function handler(
|
||||||
} catch {
|
} catch {
|
||||||
await redis.del(resetKey);
|
await redis.del(resetKey);
|
||||||
const response: ResetPasswordResponse = {
|
const response: ResetPasswordResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
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) {
|
if (!resetData.userId || !resetData.email) {
|
||||||
await redis.del(resetKey);
|
await redis.del(resetKey);
|
||||||
const response: ResetPasswordResponse = {
|
const response: ResetPasswordResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
success: false,
|
||||||
error: "Invalid reset data",
|
error: "Invalid reset data",
|
||||||
};
|
};
|
||||||
return Response.json(response, { status: 400 });
|
return Response.json(response, { status: httpStatus.BAD_REQUEST });
|
||||||
}
|
}
|
||||||
|
|
||||||
const userQuery = `
|
const userQuery = `
|
||||||
|
@ -95,32 +102,32 @@ async function handler(
|
||||||
if (!userResult?.rows || userResult.rows.length === 0) {
|
if (!userResult?.rows || userResult.rows.length === 0) {
|
||||||
await redis.del(resetKey);
|
await redis.del(resetKey);
|
||||||
const response: ResetPasswordResponse = {
|
const response: ResetPasswordResponse = {
|
||||||
code: 404,
|
code: httpStatus.NOT_FOUND,
|
||||||
success: false,
|
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];
|
const user = userResult.rows[0];
|
||||||
if (!user) {
|
if (!user) {
|
||||||
await redis.del(resetKey);
|
await redis.del(resetKey);
|
||||||
const response: ResetPasswordResponse = {
|
const response: ResetPasswordResponse = {
|
||||||
code: 404,
|
code: httpStatus.NOT_FOUND,
|
||||||
success: false,
|
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) {
|
if (user.email !== resetData.email) {
|
||||||
await redis.del(resetKey);
|
await redis.del(resetKey);
|
||||||
const response: ResetPasswordResponse = {
|
const response: ResetPasswordResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
success: false,
|
||||||
error: "Reset token does not match current email address",
|
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(
|
const isSamePassword = await Bun.password.verify(
|
||||||
|
@ -129,18 +136,17 @@ async function handler(
|
||||||
);
|
);
|
||||||
if (isSamePassword) {
|
if (isSamePassword) {
|
||||||
const response: ResetPasswordResponse = {
|
const response: ResetPasswordResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
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, {
|
const hashedNewPassword = await Bun.password.hash(
|
||||||
algorithm: "argon2id",
|
newPassword,
|
||||||
memoryCost: 4096,
|
passwordHashing,
|
||||||
timeCost: 3,
|
);
|
||||||
});
|
|
||||||
|
|
||||||
const updateQuery = `
|
const updateQuery = `
|
||||||
UPDATE users
|
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 = {
|
const response: ResetPasswordResponse = {
|
||||||
code: 200,
|
code: httpStatus.OK,
|
||||||
success: true,
|
success: true,
|
||||||
message: logoutAllSessions
|
message: baseMessage + sessionMessage,
|
||||||
? `Password reset successfully. Logged out from ${invalidatedCount} session(s).`
|
|
||||||
: "Password reset successfully.",
|
|
||||||
loggedOutSessions: invalidatedCount,
|
loggedOutSessions: invalidatedCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Response.json(response, { status: 200 });
|
return Response.json(response, { status: httpStatus.OK });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
echo.error({
|
echo.error({
|
||||||
message: "Password reset failed",
|
message: "Password reset failed",
|
||||||
|
@ -179,11 +188,13 @@ async function handler(
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: ResetPasswordResponse = {
|
const response: ResetPasswordResponse = {
|
||||||
code: 500,
|
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||||
success: false,
|
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 { echo } from "@atums/echo";
|
||||||
import { redis } from "bun";
|
import { redis } from "bun";
|
||||||
import { environment } from "#environment/config";
|
import { environment } from "#environment/config";
|
||||||
import { emailUpdateTimes } from "#environment/constants";
|
import {
|
||||||
|
cacheTTL,
|
||||||
|
errorMessages,
|
||||||
|
generateCacheKey,
|
||||||
|
httpStatus,
|
||||||
|
} from "#environment/constants";
|
||||||
import { extraValues } from "#environment/extra";
|
import { extraValues } from "#environment/extra";
|
||||||
import { sessionManager } from "#lib/auth";
|
import { sessionManager } from "#lib/auth";
|
||||||
import { cassandra } from "#lib/database";
|
import { cassandra } from "#lib/database";
|
||||||
import { mailerService } from "#lib/mailer";
|
import { mailerService } from "#lib/mailer";
|
||||||
|
import { formatSecondsToHighestUnit } from "#lib/utils";
|
||||||
import { isValidEmail } from "#lib/validation";
|
import { isValidEmail } from "#lib/validation";
|
||||||
|
|
||||||
import type { UserSession } from "#types/config";
|
import type { UserSession } from "#types/config";
|
||||||
|
@ -35,11 +41,11 @@ async function handler(
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
const response: EmailChangeResponse = {
|
const response: EmailChangeResponse = {
|
||||||
code: 401,
|
code: httpStatus.UNAUTHORIZED,
|
||||||
success: false,
|
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") {
|
if (request.method === "GET") {
|
||||||
|
@ -54,11 +60,13 @@ async function handler(
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: EmailChangeResponse = {
|
const response: EmailChangeResponse = {
|
||||||
code: 500,
|
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||||
success: false,
|
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) {
|
if (!newEmail) {
|
||||||
const response: EmailChangeResponse = {
|
const response: EmailChangeResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
success: false,
|
||||||
error: "New email is required",
|
error: "New email is required",
|
||||||
};
|
};
|
||||||
return Response.json(response, { status: 400 });
|
return Response.json(response, { status: httpStatus.BAD_REQUEST });
|
||||||
}
|
}
|
||||||
|
|
||||||
const emailValidation = isValidEmail(newEmail);
|
const emailValidation = isValidEmail(newEmail);
|
||||||
if (!emailValidation.valid) {
|
if (!emailValidation.valid) {
|
||||||
const response: EmailChangeResponse = {
|
const response: EmailChangeResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
success: false,
|
||||||
error: emailValidation.error || "Invalid email",
|
error: emailValidation.error || "Invalid email",
|
||||||
};
|
};
|
||||||
return Response.json(response, { status: 400 });
|
return Response.json(response, { status: httpStatus.BAD_REQUEST });
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedEmail = newEmail.trim().toLowerCase();
|
const normalizedEmail = newEmail.trim().toLowerCase();
|
||||||
|
@ -102,51 +110,51 @@ async function handleEmailChangeRequest(
|
||||||
await sessionManager.invalidateSession(request);
|
await sessionManager.invalidateSession(request);
|
||||||
|
|
||||||
const response: EmailChangeResponse = {
|
const response: EmailChangeResponse = {
|
||||||
code: 404,
|
code: httpStatus.NOT_FOUND,
|
||||||
success: false,
|
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];
|
const currentUser = currentUserResult.rows[0];
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
const response: EmailChangeResponse = {
|
const response: EmailChangeResponse = {
|
||||||
code: 404,
|
code: httpStatus.NOT_FOUND,
|
||||||
success: false,
|
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) {
|
if (normalizedEmail === currentUser.email) {
|
||||||
const response: EmailChangeResponse = {
|
const response: EmailChangeResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
success: false,
|
||||||
error: "New email must be different from current email",
|
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);
|
const lastRequest = await redis.get(cooldownKey);
|
||||||
|
|
||||||
if (lastRequest) {
|
if (lastRequest) {
|
||||||
const lastRequestTime = Number.parseInt(lastRequest, 10);
|
const lastRequestTime = Number.parseInt(lastRequest, 10);
|
||||||
const timeSince = Date.now() - lastRequestTime;
|
const timeSince = Date.now() - lastRequestTime;
|
||||||
const cooldownMs = emailUpdateTimes.coolDownMinutes * 60 * 1000;
|
const cooldownMs = cacheTTL.emailChangeCooldown * 1000;
|
||||||
|
|
||||||
if (timeSince < cooldownMs) {
|
if (timeSince < cooldownMs) {
|
||||||
const remainingMs = cooldownMs - timeSince;
|
const remainingMs = cooldownMs - timeSince;
|
||||||
const remainingMinutes = Math.ceil(remainingMs / (60 * 1000));
|
const remainingMinutes = Math.ceil(remainingMs / (60 * 1000));
|
||||||
|
|
||||||
const response: EmailChangeResponse = {
|
const response: EmailChangeResponse = {
|
||||||
code: 429,
|
code: httpStatus.TOO_MANY_REQUESTS,
|
||||||
success: false,
|
success: false,
|
||||||
error: `Please wait ${remainingMinutes} minute(s) before requesting another email change`,
|
error: `Please wait ${remainingMinutes} minute(s) before requesting another email change`,
|
||||||
cooldownRemaining: remainingMinutes,
|
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) {
|
if (existingEmailResult.rows.length > 0) {
|
||||||
const response: EmailChangeResponse = {
|
const response: EmailChangeResponse = {
|
||||||
code: 409,
|
code: httpStatus.CONFLICT,
|
||||||
success: false,
|
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 verificationToken = Bun.randomUUIDv7();
|
||||||
const emailChangeKey = `email-change:${verificationToken}`;
|
const emailChangeKey = generateCacheKey.emailChange(verificationToken);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -180,14 +188,14 @@ async function handleEmailChangeRequest(
|
||||||
emailChangeKey,
|
emailChangeKey,
|
||||||
JSON.stringify(emailChangeData),
|
JSON.stringify(emailChangeData),
|
||||||
"EX",
|
"EX",
|
||||||
emailUpdateTimes.tokenExpiryHours * 60 * 60,
|
cacheTTL.emailChange,
|
||||||
);
|
);
|
||||||
|
|
||||||
await redis.set(
|
await redis.set(
|
||||||
cooldownKey,
|
cooldownKey,
|
||||||
now.toString(),
|
now.toString(),
|
||||||
"EX",
|
"EX",
|
||||||
emailUpdateTimes.coolDownMinutes * 60,
|
cacheTTL.emailChangeCooldown,
|
||||||
);
|
);
|
||||||
|
|
||||||
// send verification email to NEW email
|
// send verification email to NEW email
|
||||||
|
@ -198,7 +206,7 @@ async function handleEmailChangeRequest(
|
||||||
displayName: currentUser.display_name || currentUser.username,
|
displayName: currentUser.display_name || currentUser.username,
|
||||||
currentEmail: currentUser.email,
|
currentEmail: currentUser.email,
|
||||||
newEmail: normalizedEmail,
|
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}`,
|
verificationUrl: `${environment.frontendFqdn}/user/email?token=${verificationToken}`,
|
||||||
supportEmail: extraValues.supportEmail,
|
supportEmail: extraValues.supportEmail,
|
||||||
};
|
};
|
||||||
|
@ -219,7 +227,7 @@ async function handleEmailChangeRequest(
|
||||||
currentEmail: currentUser.email,
|
currentEmail: currentUser.email,
|
||||||
newEmail: normalizedEmail,
|
newEmail: normalizedEmail,
|
||||||
requestTime: new Date().toLocaleString(),
|
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,
|
supportEmail: extraValues.supportEmail,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -240,11 +248,11 @@ async function handleEmailChangeRequest(
|
||||||
}
|
}
|
||||||
|
|
||||||
const response: EmailChangeResponse = {
|
const response: EmailChangeResponse = {
|
||||||
code: 200,
|
code: httpStatus.OK,
|
||||||
success: true,
|
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.`,
|
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) {
|
} catch (error) {
|
||||||
await redis.del(emailChangeKey).catch(() => {});
|
await redis.del(emailChangeKey).catch(() => {});
|
||||||
await redis.del(cooldownKey).catch(() => {});
|
await redis.del(cooldownKey).catch(() => {});
|
||||||
|
@ -258,11 +266,13 @@ async function handleEmailChangeRequest(
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: EmailChangeResponse = {
|
const response: EmailChangeResponse = {
|
||||||
code: 500,
|
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||||
success: false,
|
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() === "") {
|
if (!token || typeof token !== "string" || token.trim() === "") {
|
||||||
const response: EmailChangeResponse = {
|
const response: EmailChangeResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
success: false,
|
||||||
error: "Email change verification token is required",
|
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);
|
const emailChangeDataRaw = await redis.get(emailChangeKey);
|
||||||
|
|
||||||
if (!emailChangeDataRaw) {
|
if (!emailChangeDataRaw) {
|
||||||
const response: EmailChangeResponse = {
|
const response: EmailChangeResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
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;
|
let emailChangeData: EmailChangeData;
|
||||||
|
@ -300,11 +310,11 @@ async function handleEmailVerification(
|
||||||
await redis.del(emailChangeKey);
|
await redis.del(emailChangeKey);
|
||||||
|
|
||||||
const response: EmailChangeResponse = {
|
const response: EmailChangeResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
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 (
|
if (
|
||||||
|
@ -315,11 +325,11 @@ async function handleEmailVerification(
|
||||||
await redis.del(emailChangeKey);
|
await redis.del(emailChangeKey);
|
||||||
|
|
||||||
const response: EmailChangeResponse = {
|
const response: EmailChangeResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
success: false,
|
||||||
error: "Invalid email change data",
|
error: "Invalid email change data",
|
||||||
};
|
};
|
||||||
return Response.json(response, { status: 400 });
|
return Response.json(response, { status: httpStatus.BAD_REQUEST });
|
||||||
}
|
}
|
||||||
|
|
||||||
const userQuery = `
|
const userQuery = `
|
||||||
|
@ -334,11 +344,11 @@ async function handleEmailVerification(
|
||||||
await redis.del(emailChangeKey);
|
await redis.del(emailChangeKey);
|
||||||
|
|
||||||
const response: EmailChangeResponse = {
|
const response: EmailChangeResponse = {
|
||||||
code: 404,
|
code: httpStatus.NOT_FOUND,
|
||||||
success: false,
|
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];
|
const user = userResult.rows[0];
|
||||||
|
@ -346,23 +356,23 @@ async function handleEmailVerification(
|
||||||
await redis.del(emailChangeKey);
|
await redis.del(emailChangeKey);
|
||||||
|
|
||||||
const response: EmailChangeResponse = {
|
const response: EmailChangeResponse = {
|
||||||
code: 404,
|
code: httpStatus.NOT_FOUND,
|
||||||
success: false,
|
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) {
|
if (user.email !== emailChangeData.currentEmail) {
|
||||||
await redis.del(emailChangeKey);
|
await redis.del(emailChangeKey);
|
||||||
|
|
||||||
const response: EmailChangeResponse = {
|
const response: EmailChangeResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
success: false,
|
||||||
error:
|
error:
|
||||||
"Email change token is no longer valid - current email has changed",
|
"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";
|
const existingEmailQuery = "SELECT id FROM users WHERE email = ? LIMIT 1";
|
||||||
|
@ -377,11 +387,11 @@ async function handleEmailVerification(
|
||||||
await redis.del(emailChangeKey);
|
await redis.del(emailChangeKey);
|
||||||
|
|
||||||
const response: EmailChangeResponse = {
|
const response: EmailChangeResponse = {
|
||||||
code: 409,
|
code: httpStatus.CONFLICT,
|
||||||
success: false,
|
success: false,
|
||||||
error: "New email address is no longer available",
|
error: "New email address is no longer available",
|
||||||
};
|
};
|
||||||
return Response.json(response, { status: 409 });
|
return Response.json(response, { status: httpStatus.CONFLICT });
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateQuery = `
|
const updateQuery = `
|
||||||
|
@ -434,11 +444,13 @@ async function handleEmailVerification(
|
||||||
const updatedUser = updatedUserResult.rows[0];
|
const updatedUser = updatedUserResult.rows[0];
|
||||||
if (!updatedUser) {
|
if (!updatedUser) {
|
||||||
const response: EmailChangeResponse = {
|
const response: EmailChangeResponse = {
|
||||||
code: 500,
|
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||||
success: false,
|
success: false,
|
||||||
error: "Failed to fetch updated user data",
|
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;
|
let sessionCookie: string | undefined;
|
||||||
|
@ -480,14 +492,14 @@ async function handleEmailVerification(
|
||||||
};
|
};
|
||||||
|
|
||||||
const response: EmailChangeResponse = {
|
const response: EmailChangeResponse = {
|
||||||
code: 200,
|
code: httpStatus.OK,
|
||||||
success: true,
|
success: true,
|
||||||
message: `Email successfully changed to ${updatedUser.email} and verified.`,
|
message: `Email successfully changed to ${updatedUser.email} and verified.`,
|
||||||
user: responseUser,
|
user: responseUser,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Response.json(response, {
|
return Response.json(response, {
|
||||||
status: 200,
|
status: httpStatus.OK,
|
||||||
headers: sessionCookie ? { "Set-Cookie": sessionCookie } : {},
|
headers: sessionCookie ? { "Set-Cookie": sessionCookie } : {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
import { echo } from "@atums/echo";
|
import { echo } from "@atums/echo";
|
||||||
|
import {
|
||||||
|
errorMessages,
|
||||||
|
httpStatus,
|
||||||
|
successMessages,
|
||||||
|
} from "#environment/constants";
|
||||||
import { sessionManager } from "#lib/auth";
|
import { sessionManager } from "#lib/auth";
|
||||||
import { cassandra } from "#lib/database";
|
import { cassandra } from "#lib/database";
|
||||||
import { isValidDisplayName, isValidUsername } from "#lib/validation";
|
import { isValidDisplayName, isValidUsername } from "#lib/validation";
|
||||||
|
@ -28,22 +33,22 @@ async function handler(
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
const response: UpdateInfoResponse = {
|
const response: UpdateInfoResponse = {
|
||||||
code: 401,
|
code: httpStatus.UNAUTHORIZED,
|
||||||
success: false,
|
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;
|
const { username, displayName } = requestBody as UpdateInfoRequest;
|
||||||
|
|
||||||
if (username === undefined && displayName === undefined) {
|
if (username === undefined && displayName === undefined) {
|
||||||
const response: UpdateInfoResponse = {
|
const response: UpdateInfoResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
success: false,
|
||||||
error: "At least one field must be provided (username, displayName)",
|
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 = `
|
const currentUserQuery = `
|
||||||
|
@ -59,21 +64,21 @@ async function handler(
|
||||||
await sessionManager.invalidateSession(request);
|
await sessionManager.invalidateSession(request);
|
||||||
|
|
||||||
const response: UpdateInfoResponse = {
|
const response: UpdateInfoResponse = {
|
||||||
code: 404,
|
code: httpStatus.NOT_FOUND,
|
||||||
success: false,
|
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];
|
const currentUser = currentUserResult.rows[0];
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
const response: UpdateInfoResponse = {
|
const response: UpdateInfoResponse = {
|
||||||
code: 404,
|
code: httpStatus.NOT_FOUND,
|
||||||
success: false,
|
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: {
|
const updates: {
|
||||||
|
@ -85,11 +90,11 @@ async function handler(
|
||||||
const usernameValidation = isValidUsername(username);
|
const usernameValidation = isValidUsername(username);
|
||||||
if (!usernameValidation.valid || !usernameValidation.username) {
|
if (!usernameValidation.valid || !usernameValidation.username) {
|
||||||
const response: UpdateInfoResponse = {
|
const response: UpdateInfoResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
success: false,
|
||||||
error: usernameValidation.error || "Invalid username",
|
error: usernameValidation.error || "Invalid username",
|
||||||
};
|
};
|
||||||
return Response.json(response, { status: 400 });
|
return Response.json(response, { status: httpStatus.BAD_REQUEST });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (usernameValidation.username !== currentUser.username) {
|
if (usernameValidation.username !== currentUser.username) {
|
||||||
|
@ -105,11 +110,11 @@ async function handler(
|
||||||
existingUsernameResult.rows[0]?.id !== session.id
|
existingUsernameResult.rows[0]?.id !== session.id
|
||||||
) {
|
) {
|
||||||
const response: UpdateInfoResponse = {
|
const response: UpdateInfoResponse = {
|
||||||
code: 409,
|
code: httpStatus.CONFLICT,
|
||||||
success: false,
|
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;
|
updates.username = usernameValidation.username;
|
||||||
|
@ -123,11 +128,11 @@ async function handler(
|
||||||
const displayNameValidation = isValidDisplayName(displayName);
|
const displayNameValidation = isValidDisplayName(displayName);
|
||||||
if (!displayNameValidation.valid) {
|
if (!displayNameValidation.valid) {
|
||||||
const response: UpdateInfoResponse = {
|
const response: UpdateInfoResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
success: false,
|
||||||
error: displayNameValidation.error || "Invalid display name",
|
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;
|
updates.displayName = displayNameValidation.name || null;
|
||||||
}
|
}
|
||||||
|
@ -135,7 +140,7 @@ async function handler(
|
||||||
|
|
||||||
if (Object.keys(updates).length === 0) {
|
if (Object.keys(updates).length === 0) {
|
||||||
const response: UpdateInfoResponse = {
|
const response: UpdateInfoResponse = {
|
||||||
code: 200,
|
code: httpStatus.OK,
|
||||||
success: true,
|
success: true,
|
||||||
message: "No changes required",
|
message: "No changes required",
|
||||||
user: {
|
user: {
|
||||||
|
@ -147,7 +152,7 @@ async function handler(
|
||||||
createdAt: currentUser.created_at.toISOString(),
|
createdAt: currentUser.created_at.toISOString(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return Response.json(response, { status: 200 });
|
return Response.json(response, { status: httpStatus.OK });
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateFields: string[] = [];
|
const updateFields: string[] = [];
|
||||||
|
@ -182,11 +187,13 @@ async function handler(
|
||||||
const updatedUser = updatedUserResult.rows[0];
|
const updatedUser = updatedUserResult.rows[0];
|
||||||
if (!updatedUser) {
|
if (!updatedUser) {
|
||||||
const response: UpdateInfoResponse = {
|
const response: UpdateInfoResponse = {
|
||||||
code: 500,
|
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||||
success: false,
|
success: false,
|
||||||
error: "Failed to fetch updated user data",
|
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";
|
const userAgent = request.headers.get("User-Agent") || "Unknown";
|
||||||
|
@ -216,14 +223,14 @@ async function handler(
|
||||||
};
|
};
|
||||||
|
|
||||||
const response: UpdateInfoResponse = {
|
const response: UpdateInfoResponse = {
|
||||||
code: 200,
|
code: httpStatus.OK,
|
||||||
success: true,
|
success: true,
|
||||||
message: "User information updated successfully",
|
message: successMessages.USER_INFO_UPDATED,
|
||||||
user: responseUser,
|
user: responseUser,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Response.json(response, {
|
return Response.json(response, {
|
||||||
status: 200,
|
status: httpStatus.OK,
|
||||||
headers: {
|
headers: {
|
||||||
"Set-Cookie": sessionCookie,
|
"Set-Cookie": sessionCookie,
|
||||||
},
|
},
|
||||||
|
@ -235,11 +242,13 @@ async function handler(
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: UpdateInfoResponse = {
|
const response: UpdateInfoResponse = {
|
||||||
code: 500,
|
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||||
success: false,
|
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 { echo } from "@atums/echo";
|
||||||
|
import {
|
||||||
|
errorMessages,
|
||||||
|
httpStatus,
|
||||||
|
passwordHashing,
|
||||||
|
successMessages,
|
||||||
|
} from "#environment/constants";
|
||||||
import { sessionManager } from "#lib/auth";
|
import { sessionManager } from "#lib/auth";
|
||||||
import { cassandra } from "#lib/database";
|
import { cassandra } from "#lib/database";
|
||||||
import { isValidPassword } from "#lib/validation";
|
import { isValidPassword } from "#lib/validation";
|
||||||
|
@ -27,11 +33,11 @@ async function handler(
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
const response: UpdatePasswordResponse = {
|
const response: UpdatePasswordResponse = {
|
||||||
code: 401,
|
code: httpStatus.UNAUTHORIZED,
|
||||||
success: false,
|
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 } =
|
const { currentPassword, newPassword, logoutAllSessions } =
|
||||||
|
@ -39,30 +45,30 @@ async function handler(
|
||||||
|
|
||||||
if (!currentPassword || !newPassword) {
|
if (!currentPassword || !newPassword) {
|
||||||
const response: UpdatePasswordResponse = {
|
const response: UpdatePasswordResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
success: false,
|
||||||
error: "Both currentPassword and newPassword are required",
|
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);
|
const passwordValidation = isValidPassword(newPassword);
|
||||||
if (!passwordValidation.valid) {
|
if (!passwordValidation.valid) {
|
||||||
const response: UpdatePasswordResponse = {
|
const response: UpdatePasswordResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
success: false,
|
||||||
error: passwordValidation.error || "Invalid new password",
|
error: passwordValidation.error || "Invalid new password",
|
||||||
};
|
};
|
||||||
return Response.json(response, { status: 400 });
|
return Response.json(response, { status: httpStatus.BAD_REQUEST });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentPassword === newPassword) {
|
if (currentPassword === newPassword) {
|
||||||
const response: UpdatePasswordResponse = {
|
const response: UpdatePasswordResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
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 = `
|
const userQuery = `
|
||||||
|
@ -78,21 +84,21 @@ async function handler(
|
||||||
await sessionManager.invalidateSession(request);
|
await sessionManager.invalidateSession(request);
|
||||||
|
|
||||||
const response: UpdatePasswordResponse = {
|
const response: UpdatePasswordResponse = {
|
||||||
code: 404,
|
code: httpStatus.NOT_FOUND,
|
||||||
success: false,
|
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];
|
const user = userResult.rows[0];
|
||||||
if (!user) {
|
if (!user) {
|
||||||
const response: UpdatePasswordResponse = {
|
const response: UpdatePasswordResponse = {
|
||||||
code: 404,
|
code: httpStatus.NOT_FOUND,
|
||||||
success: false,
|
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(
|
const isCurrentPasswordValid = await Bun.password.verify(
|
||||||
|
@ -102,18 +108,17 @@ async function handler(
|
||||||
|
|
||||||
if (!isCurrentPasswordValid) {
|
if (!isCurrentPasswordValid) {
|
||||||
const response: UpdatePasswordResponse = {
|
const response: UpdatePasswordResponse = {
|
||||||
code: 401,
|
code: httpStatus.UNAUTHORIZED,
|
||||||
success: false,
|
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, {
|
const hashedNewPassword = await Bun.password.hash(
|
||||||
algorithm: "argon2id",
|
newPassword,
|
||||||
memoryCost: 4096,
|
passwordHashing,
|
||||||
timeCost: 3,
|
);
|
||||||
});
|
|
||||||
|
|
||||||
const updateQuery = `
|
const updateQuery = `
|
||||||
UPDATE users
|
UPDATE users
|
||||||
|
@ -131,21 +136,25 @@ async function handler(
|
||||||
const invalidatedCount =
|
const invalidatedCount =
|
||||||
await sessionManager.invalidateAllSessionsForUser(session.id);
|
await sessionManager.invalidateAllSessionsForUser(session.id);
|
||||||
|
|
||||||
|
const baseMessage = successMessages.PASSWORD_UPDATED;
|
||||||
|
const sessionMessage = ` Logged out from ${invalidatedCount} session(s).`;
|
||||||
|
|
||||||
const response: UpdatePasswordResponse = {
|
const response: UpdatePasswordResponse = {
|
||||||
code: 200,
|
code: httpStatus.OK,
|
||||||
success: true,
|
success: true,
|
||||||
message: `Password updated successfully. Logged out from ${invalidatedCount} session(s).`,
|
message: baseMessage + sessionMessage,
|
||||||
loggedOutSessions: invalidatedCount,
|
loggedOutSessions: invalidatedCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Response.json(response, {
|
return Response.json(response, {
|
||||||
status: 200,
|
status: httpStatus.OK,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Set-Cookie": "session=; Path=/; Max-Age=0; HttpOnly",
|
"Set-Cookie": "session=; Path=/; Max-Age=0; HttpOnly",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const allSessions = await sessionManager.getActiveSessionsForUser(
|
const allSessions = await sessionManager.getActiveSessionsForUser(
|
||||||
session.id,
|
session.id,
|
||||||
);
|
);
|
||||||
|
@ -180,18 +189,21 @@ async function handler(
|
||||||
userAgent,
|
userAgent,
|
||||||
);
|
);
|
||||||
|
|
||||||
const response: UpdatePasswordResponse = {
|
const baseMessage = successMessages.PASSWORD_UPDATED;
|
||||||
code: 200,
|
const sessionMessage =
|
||||||
success: true,
|
|
||||||
message:
|
|
||||||
invalidatedCount > 0
|
invalidatedCount > 0
|
||||||
? `Password updated successfully. Logged out from ${invalidatedCount} other session(s).`
|
? ` Logged out from ${invalidatedCount} other session(s).`
|
||||||
: "Password updated successfully.",
|
: ".";
|
||||||
|
|
||||||
|
const response: UpdatePasswordResponse = {
|
||||||
|
code: httpStatus.OK,
|
||||||
|
success: true,
|
||||||
|
message: baseMessage + sessionMessage,
|
||||||
loggedOutSessions: invalidatedCount,
|
loggedOutSessions: invalidatedCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Response.json(response, {
|
return Response.json(response, {
|
||||||
status: 200,
|
status: httpStatus.OK,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Set-Cookie": sessionCookie,
|
"Set-Cookie": sessionCookie,
|
||||||
|
@ -204,11 +216,13 @@ async function handler(
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: UpdatePasswordResponse = {
|
const response: UpdatePasswordResponse = {
|
||||||
code: 500,
|
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||||
success: false,
|
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 { echo } from "@atums/echo";
|
||||||
import { redis } from "bun";
|
import { redis } from "bun";
|
||||||
|
import {
|
||||||
|
errorMessages,
|
||||||
|
generateCacheKey,
|
||||||
|
httpStatus,
|
||||||
|
successMessages,
|
||||||
|
} from "#environment/constants";
|
||||||
import { sessionManager } from "#lib/auth";
|
import { sessionManager } from "#lib/auth";
|
||||||
import { cassandra } from "#lib/database";
|
import { cassandra } from "#lib/database";
|
||||||
|
|
||||||
|
@ -23,23 +29,23 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
|
|
||||||
if (!token || typeof token !== "string" || token.trim() === "") {
|
if (!token || typeof token !== "string" || token.trim() === "") {
|
||||||
const response: VerifyEmailResponse = {
|
const response: VerifyEmailResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
success: false,
|
||||||
error: "Verification token is required",
|
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);
|
const verificationDataRaw = await redis.get(verificationKey);
|
||||||
|
|
||||||
if (!verificationDataRaw) {
|
if (!verificationDataRaw) {
|
||||||
const response: VerifyEmailResponse = {
|
const response: VerifyEmailResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
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;
|
let verificationData: VerificationData;
|
||||||
|
@ -49,22 +55,22 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
await redis.del(verificationKey);
|
await redis.del(verificationKey);
|
||||||
|
|
||||||
const response: VerifyEmailResponse = {
|
const response: VerifyEmailResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
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) {
|
if (!verificationData.userId || !verificationData.email) {
|
||||||
await redis.del(verificationKey);
|
await redis.del(verificationKey);
|
||||||
|
|
||||||
const response: VerifyEmailResponse = {
|
const response: VerifyEmailResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
success: false,
|
||||||
error: "Invalid verification data",
|
error: "Invalid verification data",
|
||||||
};
|
};
|
||||||
return Response.json(response, { status: 400 });
|
return Response.json(response, { status: httpStatus.BAD_REQUEST });
|
||||||
}
|
}
|
||||||
|
|
||||||
const userQuery = `
|
const userQuery = `
|
||||||
|
@ -90,11 +96,11 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
await redis.del(verificationKey);
|
await redis.del(verificationKey);
|
||||||
|
|
||||||
const response: VerifyEmailResponse = {
|
const response: VerifyEmailResponse = {
|
||||||
code: 404,
|
code: httpStatus.NOT_FOUND,
|
||||||
success: false,
|
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];
|
const user = userResult.rows[0];
|
||||||
|
@ -102,22 +108,22 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
await redis.del(verificationKey);
|
await redis.del(verificationKey);
|
||||||
|
|
||||||
const response: VerifyEmailResponse = {
|
const response: VerifyEmailResponse = {
|
||||||
code: 404,
|
code: httpStatus.NOT_FOUND,
|
||||||
success: false,
|
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) {
|
if (user.email !== verificationData.email) {
|
||||||
await redis.del(verificationKey);
|
await redis.del(verificationKey);
|
||||||
|
|
||||||
const response: VerifyEmailResponse = {
|
const response: VerifyEmailResponse = {
|
||||||
code: 400,
|
code: httpStatus.BAD_REQUEST,
|
||||||
success: false,
|
success: false,
|
||||||
error: "Verification token does not match current email address",
|
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) {
|
if (user.is_verified) {
|
||||||
|
@ -133,12 +139,12 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
};
|
};
|
||||||
|
|
||||||
const response: VerifyEmailResponse = {
|
const response: VerifyEmailResponse = {
|
||||||
code: 200,
|
code: httpStatus.OK,
|
||||||
success: true,
|
success: true,
|
||||||
message: "Email is already verified",
|
message: errorMessages.EMAIL_ALREADY_VERIFIED,
|
||||||
user: responseUser,
|
user: responseUser,
|
||||||
};
|
};
|
||||||
return Response.json(response, { status: 200 });
|
return Response.json(response, { status: httpStatus.OK });
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateQuery = `
|
const updateQuery = `
|
||||||
|
@ -168,11 +174,13 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
const updatedUser = updatedUserResult.rows[0];
|
const updatedUser = updatedUserResult.rows[0];
|
||||||
if (!updatedUser) {
|
if (!updatedUser) {
|
||||||
const response: VerifyEmailResponse = {
|
const response: VerifyEmailResponse = {
|
||||||
code: 500,
|
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||||
success: false,
|
success: false,
|
||||||
error: "Failed to fetch updated user data",
|
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;
|
const { session } = request;
|
||||||
|
@ -215,14 +223,14 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
};
|
};
|
||||||
|
|
||||||
const response: VerifyEmailResponse = {
|
const response: VerifyEmailResponse = {
|
||||||
code: 200,
|
code: httpStatus.OK,
|
||||||
success: true,
|
success: true,
|
||||||
message: "Email verified successfully",
|
message: successMessages.EMAIL_VERIFIED,
|
||||||
user: responseUser,
|
user: responseUser,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Response.json(response, {
|
return Response.json(response, {
|
||||||
status: 200,
|
status: httpStatus.OK,
|
||||||
headers: {
|
headers: {
|
||||||
"Set-Cookie": sessionCookie ? sessionCookie : "",
|
"Set-Cookie": sessionCookie ? sessionCookie : "",
|
||||||
},
|
},
|
||||||
|
@ -235,11 +243,13 @@ async function handler(request: ExtendedRequest): Promise<Response> {
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: VerifyEmailResponse = {
|
const response: VerifyEmailResponse = {
|
||||||
code: 500,
|
code: httpStatus.INTERNAL_SERVER_ERROR,
|
||||||
success: false,
|
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 "./responses";
|
||||||
export * from "./register";
|
export * from "./register";
|
||||||
export * from "./login";
|
export * from "./login";
|
||||||
|
export * from "./logout";
|
||||||
export * from "./verify";
|
export * from "./verify";
|
||||||
|
|
||||||
export * from "./update";
|
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