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

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

View file

@ -0,0 +1,26 @@
const cacheKeys = {
session: "session",
mailVerification: "mail-verification",
passwordReset: "password-reset",
emailChange: "email-change",
emailChangeCooldown: "email-change-cooldown",
} as const;
const cacheTTL = {
passwordReset: 1 * 60 * 60, // 1h
mailVerification: 3 * 60 * 60, // 3h
emailChange: 3 * 60 * 60, // 3h
emailChangeCooldown: 5 * 60, // 5m
} as const;
const generateCacheKey = {
session: (userId: string, token: string) =>
`${cacheKeys.session}:${userId}:${token}`,
mailVerification: (token: string) => `${cacheKeys.mailVerification}:${token}`,
passwordReset: (token: string) => `${cacheKeys.passwordReset}:${token}`,
emailChange: (token: string) => `${cacheKeys.emailChange}:${token}`,
emailChangeCooldown: (userId: string) =>
`${cacheKeys.emailChangeCooldown}:${userId}`,
} as const;
export { cacheKeys, cacheTTL, generateCacheKey };

View file

@ -0,0 +1,54 @@
const httpStatus = {
OK: 200,
CREATED: 201,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
METHOD_NOT_ALLOWED: 405,
NOT_ACCEPTABLE: 406,
CONFLICT: 409,
TOO_MANY_REQUESTS: 429,
INTERNAL_SERVER_ERROR: 500,
SERVICE_UNAVAILABLE: 503,
};
const errorMessages = {
NOT_AUTHENTICATED: "Not authenticated",
INVALID_CREDENTIALS: "Invalid credentials",
USER_ALREADY_LOGGED_IN: "User already logged in",
USER_NOT_FOUND: "User not found",
USERNAME_ALREADY_EXISTS: "Username already exists",
EMAIL_ALREADY_EXISTS: "Email already exists",
MISSING_REQUIRED_FIELDS: "Missing required fields",
INVALID_TOKEN: "Invalid or expired token",
INVALID_TOKEN_FORMAT: "Invalid token format",
INTERNAL_SERVER_ERROR: "Internal server error",
DATABASE_QUERY_FAILED: "Database query failed",
NOT_FOUND: "Not Found",
METHOD_NOT_ALLOWED: "Method Not Allowed",
EMAIL_SEND_FAILED: "Failed to send email. Please try again.",
EMAIL_ALREADY_VERIFIED: "Email is already verified",
PASSWORD_SAME_AS_CURRENT:
"New password must be different from current password",
CURRENT_PASSWORD_INCORRECT: "Current password is incorrect",
};
const successMessages = {
LOGIN_SUCCESSFUL: "Login successful",
LOGOUT_SUCCESSFUL: "Logged out successfully",
EMAIL_VERIFIED: "Email verified successfully",
PASSWORD_UPDATED: "Password updated successfully",
USER_INFO_UPDATED: "User information updated successfully",
PASSWORD_RESET_SENT:
"If the email exists, a password reset link has been sent",
REGISTRATION_SUCCESSFUL:
"User registered successfully - please check your email to verify your account",
};
export { httpStatus, errorMessages, successMessages };

View file

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

View file

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

View file

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

View file

@ -35,6 +35,6 @@
<p><strong>If this change was not authorized by you:</strong> Contact our support team immediately at {{supportEmail}}. Your account may have been compromised and we will help you recover it.</p> <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>

View file

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

View file

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

View file

@ -34,6 +34,6 @@
<p>If you did not request this password reset, please contact {{supportEmail}} immediately. Your password will remain unchanged.</p> <p>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>

View file

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

View file

@ -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
View file

@ -0,0 +1,133 @@
function formatSecondsToHighestUnit(
seconds: number,
options: {
capitalize?: boolean;
pluralize?: boolean;
short?: boolean;
} = {},
): string {
const { capitalize = false, pluralize = true, short = false } = options;
const timeUnits = [
{
value: 365 * 24 * 60 * 60,
singular: "year",
plural: "years",
short: "y",
},
{
value: 30 * 24 * 60 * 60,
singular: "month",
plural: "months",
short: "mo",
},
{ value: 7 * 24 * 60 * 60, singular: "week", plural: "weeks", short: "w" },
{ value: 24 * 60 * 60, singular: "day", plural: "days", short: "d" },
{ value: 60 * 60, singular: "hour", plural: "hours", short: "h" },
{ value: 60, singular: "minute", plural: "minutes", short: "m" },
{ value: 1, singular: "second", plural: "seconds", short: "s" },
];
if (seconds === 0) {
const unit = short ? "s" : pluralize ? "seconds" : "second";
return `0 ${capitalize ? capitalizeFirst(unit) : unit}`;
}
if (seconds < 0) {
return formatSecondsToHighestUnit(Math.abs(seconds), options);
}
for (const unit of timeUnits) {
const count = Math.floor(seconds / unit.value);
if (count >= 1) {
let unitName: string;
if (short) {
unitName = unit.short;
} else if (pluralize && count !== 1) {
unitName = unit.plural;
} else {
unitName = unit.singular;
}
if (capitalize && !short) {
unitName = capitalizeFirst(unitName);
}
return `${count} ${unitName}`;
}
}
const unit = short ? "s" : pluralize ? "seconds" : "second";
return `${seconds} ${capitalize ? capitalizeFirst(unit) : unit}`;
}
function capitalizeFirst(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
function formatSecondsToMultipleUnits(
seconds: number,
maxUnits = 2,
options: {
capitalize?: boolean;
short?: boolean;
separator?: string;
} = {},
): string {
const { capitalize = true, short = false, separator = " " } = options;
const timeUnits = [
{
value: 365 * 24 * 60 * 60,
singular: "year",
plural: "years",
short: "y",
},
{
value: 30 * 24 * 60 * 60,
singular: "month",
plural: "months",
short: "mo",
},
{ value: 7 * 24 * 60 * 60, singular: "week", plural: "weeks", short: "w" },
{ value: 24 * 60 * 60, singular: "day", plural: "days", short: "d" },
{ value: 60 * 60, singular: "hour", plural: "hours", short: "h" },
{ value: 60, singular: "minute", plural: "minutes", short: "m" },
{ value: 1, singular: "second", plural: "seconds", short: "s" },
];
if (seconds === 0) {
const unit = short ? "s" : "seconds";
return `0 ${capitalize ? capitalizeFirst(unit) : unit}`;
}
const parts: string[] = [];
let remaining = Math.abs(seconds);
for (const unit of timeUnits) {
if (parts.length >= maxUnits) break;
const count = Math.floor(remaining / unit.value);
if (count > 0) {
let unitName: string;
if (short) {
unitName = unit.short;
} else {
unitName = count === 1 ? unit.singular : unit.plural;
}
if (capitalize && !short) {
unitName = capitalizeFirst(unitName);
}
parts.push(`${count} ${unitName}`);
remaining -= count * unit.value;
}
}
return parts.join(separator) || "0 seconds";
}
export { formatSecondsToHighestUnit, formatSecondsToMultipleUnits };

View file

@ -1,7 +1,7 @@
import { 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";

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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