add reset password email and route
This commit is contained in:
parent
fff3c3ca50
commit
d36c34dd17
12 changed files with 433 additions and 68 deletions
2
bun.lock
2
bun.lock
|
@ -21,7 +21,7 @@
|
||||||
"@biomejs/biome",
|
"@biomejs/biome",
|
||||||
],
|
],
|
||||||
"packages": {
|
"packages": {
|
||||||
"@atums/echo": ["@atums/echo@1.0.3", "", { "dependencies": { "date-fns-tz": "^3.2.0" } }, "sha512-WQ2d4oWTaE+6VeLIu2FepmZipdwUrM+SiiO5moHhSsP4P+MaQCjq5qp34nwB/vOHv2jd9UcBzy27iUziTffCjg=="],
|
"@atums/echo": ["@atums/echo@1.0.6", "", { "dependencies": { "date-fns-tz": "^3.2.0" } }, "sha512-2v0coX0Ptau6pjh4aTJDXMMJ2z/Q+0r8tvLokjeyUnLWGOPMwg+i4saBrkvDtHvQbNiq/NiEwMFLCxeIlxEyLQ=="],
|
||||||
|
|
||||||
"@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="],
|
"@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="],
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
"rotate": true,
|
"rotate": true,
|
||||||
"maxFiles": 3,
|
"maxFiles": 3,
|
||||||
|
"fileNameFormat": "yyyy-MM-dd",
|
||||||
|
|
||||||
"console": true,
|
"console": true,
|
||||||
"consoleColor": true,
|
"consoleColor": true,
|
||||||
|
|
47
src/environment/mailer/templates/forgot-password.html
Normal file
47
src/environment/mailer/templates/forgot-password.html
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{subject}}</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #0066cc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>Password Reset - {{companyName}}</h1>
|
||||||
|
|
||||||
|
<p>Hi {{displayName}},</p>
|
||||||
|
|
||||||
|
<p>You requested a password reset for your account. Click the link below to reset your password:</p>
|
||||||
|
|
||||||
|
<p><a href="{{resetUrl}}">{{resetUrl}}</a></p>
|
||||||
|
|
||||||
|
<p>{{willExpire}} for security reasons.</p>
|
||||||
|
|
||||||
|
<p>If you did not request this password reset, please ignore this email. Your password will remain unchanged.</p>
|
||||||
|
|
||||||
|
<p>Questions? Contact {{supportEmail}}</p>
|
||||||
|
|
||||||
|
<p>Best regards,<br>
|
||||||
|
The {{companyName}} team</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<p><small>User ID: {{id}} | © {{currentYear}} {{companyName}}</small></p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -11,89 +11,35 @@
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
line-height: 1.6;
|
line-height: 1.5;
|
||||||
background-color: #1a1a1a;
|
|
||||||
color: #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
color: #4a9eff;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #4a9eff;
|
color: #0066cc;
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 12px 24px;
|
|
||||||
background-color: #2d7a2d;
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expiry-notice {
|
|
||||||
background-color: #2a2a2a;
|
|
||||||
border: 1px solid #444;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin: 15px 0;
|
|
||||||
color: #ffa500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fallback-url {
|
|
||||||
word-break: break-all;
|
|
||||||
background-color: #2a2a2a;
|
|
||||||
border: 1px solid #444;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
margin-top: 30px;
|
|
||||||
padding-top: 20px;
|
|
||||||
border-top: 1px solid #444;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h2>Welcome to {{companyName}}!</h2>
|
<h1>Welcome to {{companyName}}!</h1>
|
||||||
|
|
||||||
<p>Hi {{displayName}},</p>
|
<p>Hi {{displayName}},</p>
|
||||||
|
|
||||||
<p>Thanks for signing up! Please verify your email address to activate your account:</p>
|
<p>Please verify your email address:</p>
|
||||||
|
|
||||||
<p><a href="{{verificationUrl}}" class="button">Verify Email Address</a></p>
|
<p><a href="{{verificationUrl}}">{{verificationUrl}}</a></p>
|
||||||
|
|
||||||
<div class="expiry-notice">
|
<p>{{willExpire}} for security reasons.</p>
|
||||||
<strong>⏰ Important:</strong> {{willExpire}} for security reasons.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p><strong>If the button doesn't work:</strong></p>
|
<p>Questions? Contact {{supportEmail}}</p>
|
||||||
<p>Copy and paste this link into your browser:</p>
|
|
||||||
<div class="fallback-url">{{verificationUrl}}</div>
|
|
||||||
|
|
||||||
<p>Once verified, you'll have full access to your {{companyName}} account!</p>
|
<p>Best regards,<br>
|
||||||
|
The {{companyName}} team</p>
|
||||||
<p>Questions? Reply to this email or contact us at {{supportEmail}}</p>
|
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<p>Best regards,<br>The {{companyName}} team</p>
|
<p><small>User ID: {{id}} | © {{currentYear}} {{companyName}}</small></p>
|
||||||
|
|
||||||
<div class="footer">
|
|
||||||
<p>User ID: {{id}}</p>
|
|
||||||
<p>© {{currentYear}} {{companyName}}. All rights reserved.</p>
|
|
||||||
<p><small>This is an automated message. Please do not reply directly to this email.</small></p>
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
154
src/routes/user/forgot/password.ts
Normal file
154
src/routes/user/forgot/password.ts
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
import { echo } from "@atums/echo";
|
||||||
|
import { redis } from "bun";
|
||||||
|
import { environment } from "#environment/config";
|
||||||
|
import { extraValues } from "#environment/extra";
|
||||||
|
import { cassandra } from "#lib/database";
|
||||||
|
import { mailerService } from "#lib/mailer";
|
||||||
|
import { isValidEmail } from "#lib/validation";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
BaseResponse,
|
||||||
|
ExtendedRequest,
|
||||||
|
ForgotPasswordRequest,
|
||||||
|
RouteDef,
|
||||||
|
} from "#types/server";
|
||||||
|
|
||||||
|
const routeDef: RouteDef = {
|
||||||
|
method: "POST",
|
||||||
|
accepts: "application/json",
|
||||||
|
returns: "application/json",
|
||||||
|
needsBody: "json",
|
||||||
|
};
|
||||||
|
|
||||||
|
async function handler(
|
||||||
|
_request: ExtendedRequest,
|
||||||
|
requestBody: unknown,
|
||||||
|
): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const { email } = requestBody as ForgotPasswordRequest;
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
const response: BaseResponse = {
|
||||||
|
code: 400,
|
||||||
|
success: false,
|
||||||
|
error: "Email is required",
|
||||||
|
};
|
||||||
|
return Response.json(response, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailValidation = isValidEmail(email);
|
||||||
|
if (!emailValidation.valid) {
|
||||||
|
const response: BaseResponse = {
|
||||||
|
code: 400,
|
||||||
|
success: false,
|
||||||
|
error: emailValidation.error || "Invalid email",
|
||||||
|
};
|
||||||
|
return Response.json(response, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userQuery = `
|
||||||
|
SELECT id, username, display_name, email, is_verified
|
||||||
|
FROM users WHERE email = ? LIMIT 1
|
||||||
|
`;
|
||||||
|
const userResult = (await cassandra.execute(userQuery, [
|
||||||
|
email.trim().toLowerCase(),
|
||||||
|
])) as {
|
||||||
|
rows: Array<{
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
display_name: string | null;
|
||||||
|
email: string;
|
||||||
|
is_verified: boolean;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!userResult?.rows || userResult.rows.length === 0) {
|
||||||
|
const response: BaseResponse = {
|
||||||
|
code: 200,
|
||||||
|
success: true,
|
||||||
|
message: "If the email exists, a password reset link has been sent",
|
||||||
|
};
|
||||||
|
return Response.json(response, { status: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = userResult.rows[0];
|
||||||
|
if (!user) {
|
||||||
|
const response: BaseResponse = {
|
||||||
|
code: 200,
|
||||||
|
success: true,
|
||||||
|
message: "If the email exists, a password reset link has been sent",
|
||||||
|
};
|
||||||
|
return Response.json(response, { status: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetToken = Bun.randomUUIDv7();
|
||||||
|
const resetKey = `password-reset:${resetToken}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await redis.set(
|
||||||
|
resetKey,
|
||||||
|
JSON.stringify({
|
||||||
|
userId: user.id,
|
||||||
|
email: user.email,
|
||||||
|
}),
|
||||||
|
"EX",
|
||||||
|
1 * 60 * 60, // 1 hour
|
||||||
|
);
|
||||||
|
|
||||||
|
const emailVariables = {
|
||||||
|
subject: `Password Reset - ${extraValues.companyName}`,
|
||||||
|
companyName: extraValues.companyName,
|
||||||
|
id: user.id,
|
||||||
|
displayName: user.display_name || user.username,
|
||||||
|
willExpire: "This link will expire in 1 hour",
|
||||||
|
resetUrl: `${environment.frontendFqdn}/user/reset-password?token=${resetToken}`,
|
||||||
|
supportEmail: extraValues.supportEmail,
|
||||||
|
currentYear: new Date().getFullYear(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await mailerService.sendTemplateEmail(
|
||||||
|
user.email,
|
||||||
|
`Password Reset - ${extraValues.companyName}`,
|
||||||
|
"forgot-password",
|
||||||
|
emailVariables,
|
||||||
|
);
|
||||||
|
|
||||||
|
const response: BaseResponse = {
|
||||||
|
code: 200,
|
||||||
|
success: true,
|
||||||
|
message: "If the email exists, a password reset link has been sent",
|
||||||
|
};
|
||||||
|
return Response.json(response, { status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
await redis.del(resetKey).catch(() => {});
|
||||||
|
|
||||||
|
echo.error({
|
||||||
|
message: "Failed to send password reset email",
|
||||||
|
error,
|
||||||
|
userId: user.id,
|
||||||
|
email: user.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response: BaseResponse = {
|
||||||
|
code: 500,
|
||||||
|
success: false,
|
||||||
|
error: "Failed to send password reset email. Please try again.",
|
||||||
|
};
|
||||||
|
return Response.json(response, { status: 500 });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
echo.error({
|
||||||
|
message: "Password reset request failed",
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response: BaseResponse = {
|
||||||
|
code: 500,
|
||||||
|
success: false,
|
||||||
|
error: "Internal server error",
|
||||||
|
};
|
||||||
|
return Response.json(response, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { handler, routeDef };
|
190
src/routes/user/reset/password.ts
Normal file
190
src/routes/user/reset/password.ts
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
import { echo } from "@atums/echo";
|
||||||
|
import { redis } from "bun";
|
||||||
|
import { sessionManager } from "#lib/auth";
|
||||||
|
import { cassandra } from "#lib/database";
|
||||||
|
import { isValidPassword } from "#lib/validation";
|
||||||
|
import type {
|
||||||
|
ExtendedRequest,
|
||||||
|
ResetData,
|
||||||
|
ResetPasswordRequest,
|
||||||
|
ResetPasswordResponse,
|
||||||
|
RouteDef,
|
||||||
|
UserRow,
|
||||||
|
} from "#types/server";
|
||||||
|
|
||||||
|
const routeDef: RouteDef = {
|
||||||
|
method: "POST",
|
||||||
|
accepts: "application/json",
|
||||||
|
returns: "application/json",
|
||||||
|
needsBody: "json",
|
||||||
|
};
|
||||||
|
|
||||||
|
async function handler(
|
||||||
|
_request: ExtendedRequest,
|
||||||
|
requestBody: unknown,
|
||||||
|
): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
token,
|
||||||
|
newPassword,
|
||||||
|
logoutAllSessions = true,
|
||||||
|
} = requestBody as ResetPasswordRequest;
|
||||||
|
|
||||||
|
if (!token || !newPassword) {
|
||||||
|
const response: ResetPasswordResponse = {
|
||||||
|
code: 400,
|
||||||
|
success: false,
|
||||||
|
error: "Token and new password are required",
|
||||||
|
};
|
||||||
|
return Response.json(response, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordValidation = isValidPassword(newPassword);
|
||||||
|
if (!passwordValidation.valid) {
|
||||||
|
const response: ResetPasswordResponse = {
|
||||||
|
code: 400,
|
||||||
|
success: false,
|
||||||
|
error: passwordValidation.error || "Invalid password",
|
||||||
|
};
|
||||||
|
return Response.json(response, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetKey = `password-reset:${token}`;
|
||||||
|
const resetDataRaw = await redis.get(resetKey);
|
||||||
|
|
||||||
|
if (!resetDataRaw) {
|
||||||
|
const response: ResetPasswordResponse = {
|
||||||
|
code: 400,
|
||||||
|
success: false,
|
||||||
|
error: "Invalid or expired reset token",
|
||||||
|
};
|
||||||
|
return Response.json(response, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
let resetData: ResetData;
|
||||||
|
try {
|
||||||
|
resetData = JSON.parse(resetDataRaw);
|
||||||
|
} catch {
|
||||||
|
await redis.del(resetKey);
|
||||||
|
const response: ResetPasswordResponse = {
|
||||||
|
code: 400,
|
||||||
|
success: false,
|
||||||
|
error: "Invalid reset token format",
|
||||||
|
};
|
||||||
|
return Response.json(response, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resetData.userId || !resetData.email) {
|
||||||
|
await redis.del(resetKey);
|
||||||
|
const response: ResetPasswordResponse = {
|
||||||
|
code: 400,
|
||||||
|
success: false,
|
||||||
|
error: "Invalid reset data",
|
||||||
|
};
|
||||||
|
return Response.json(response, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userQuery = `
|
||||||
|
SELECT id, username, display_name, email, password, is_verified, created_at, updated_at
|
||||||
|
FROM users WHERE id = ? LIMIT 1
|
||||||
|
`;
|
||||||
|
const userResult = (await cassandra.execute(userQuery, [
|
||||||
|
resetData.userId,
|
||||||
|
])) as { rows: UserRow[] };
|
||||||
|
|
||||||
|
if (!userResult?.rows || userResult.rows.length === 0) {
|
||||||
|
await redis.del(resetKey);
|
||||||
|
const response: ResetPasswordResponse = {
|
||||||
|
code: 404,
|
||||||
|
success: false,
|
||||||
|
error: "User not found",
|
||||||
|
};
|
||||||
|
return Response.json(response, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = userResult.rows[0];
|
||||||
|
if (!user) {
|
||||||
|
await redis.del(resetKey);
|
||||||
|
const response: ResetPasswordResponse = {
|
||||||
|
code: 404,
|
||||||
|
success: false,
|
||||||
|
error: "User not found",
|
||||||
|
};
|
||||||
|
return Response.json(response, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.email !== resetData.email) {
|
||||||
|
await redis.del(resetKey);
|
||||||
|
const response: ResetPasswordResponse = {
|
||||||
|
code: 400,
|
||||||
|
success: false,
|
||||||
|
error: "Reset token does not match current email address",
|
||||||
|
};
|
||||||
|
return Response.json(response, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSamePassword = await Bun.password.verify(
|
||||||
|
newPassword,
|
||||||
|
user.password,
|
||||||
|
);
|
||||||
|
if (isSamePassword) {
|
||||||
|
const response: ResetPasswordResponse = {
|
||||||
|
code: 400,
|
||||||
|
success: false,
|
||||||
|
error: "New password must be different from current password",
|
||||||
|
};
|
||||||
|
return Response.json(response, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedNewPassword = await Bun.password.hash(newPassword, {
|
||||||
|
algorithm: "argon2id",
|
||||||
|
memoryCost: 4096,
|
||||||
|
timeCost: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateQuery = `
|
||||||
|
UPDATE users
|
||||||
|
SET password = ?, updated_at = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`;
|
||||||
|
await cassandra.execute(updateQuery, [
|
||||||
|
hashedNewPassword,
|
||||||
|
new Date(),
|
||||||
|
user.id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
await redis.del(resetKey);
|
||||||
|
|
||||||
|
let invalidatedCount = 0;
|
||||||
|
if (logoutAllSessions) {
|
||||||
|
invalidatedCount = await sessionManager.invalidateAllSessionsForUser(
|
||||||
|
user.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response: ResetPasswordResponse = {
|
||||||
|
code: 200,
|
||||||
|
success: true,
|
||||||
|
message: logoutAllSessions
|
||||||
|
? `Password reset successfully. Logged out from ${invalidatedCount} session(s).`
|
||||||
|
: "Password reset successfully.",
|
||||||
|
loggedOutSessions: invalidatedCount,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Response.json(response, { status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
echo.error({
|
||||||
|
message: "Password reset failed",
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response: ResetPasswordResponse = {
|
||||||
|
code: 500,
|
||||||
|
success: false,
|
||||||
|
error: "Internal server error",
|
||||||
|
};
|
||||||
|
return Response.json(response, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { handler, routeDef };
|
|
@ -134,7 +134,7 @@ async function handler(
|
||||||
const response: UpdatePasswordResponse = {
|
const response: UpdatePasswordResponse = {
|
||||||
code: 200,
|
code: 200,
|
||||||
success: true,
|
success: true,
|
||||||
message: `Password updated successfully. Logged out from ${invalidatedCount} sessions.`,
|
message: `Password updated successfully. Logged out from ${invalidatedCount} session(s).`,
|
||||||
loggedOutSessions: invalidatedCount,
|
loggedOutSessions: invalidatedCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -185,7 +185,7 @@ async function handler(
|
||||||
success: true,
|
success: true,
|
||||||
message:
|
message:
|
||||||
invalidatedCount > 0
|
invalidatedCount > 0
|
||||||
? `Password updated successfully. Logged out from ${invalidatedCount} other sessions.`
|
? `Password updated successfully. Logged out from ${invalidatedCount} other session(s).`
|
||||||
: "Password updated successfully.",
|
: "Password updated successfully.",
|
||||||
loggedOutSessions: invalidatedCount,
|
loggedOutSessions: invalidatedCount,
|
||||||
};
|
};
|
||||||
|
|
1
types/server/requests/user/forgot/index.ts
Normal file
1
types/server/requests/user/forgot/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from "./password";
|
5
types/server/requests/user/forgot/password.ts
Normal file
5
types/server/requests/user/forgot/password.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
interface ForgotPasswordRequest {
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { ForgotPasswordRequest };
|
|
@ -5,3 +5,5 @@ export * from "./login";
|
||||||
export * from "./verify";
|
export * from "./verify";
|
||||||
|
|
||||||
export * from "./update";
|
export * from "./update";
|
||||||
|
export * from "./forgot";
|
||||||
|
export * from "./reset";
|
||||||
|
|
1
types/server/requests/user/reset/index.ts
Normal file
1
types/server/requests/user/reset/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from "./password";
|
18
types/server/requests/user/reset/password.ts
Normal file
18
types/server/requests/user/reset/password.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import type { BaseResponse } from "../../base";
|
||||||
|
|
||||||
|
interface ResetPasswordRequest {
|
||||||
|
token: string;
|
||||||
|
newPassword: string;
|
||||||
|
logoutAllSessions?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResetPasswordResponse extends BaseResponse {
|
||||||
|
loggedOutSessions?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResetData {
|
||||||
|
userId: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { ResetPasswordRequest, ResetPasswordResponse, ResetData };
|
Loading…
Add table
Add a link
Reference in a new issue