Signed-off-by: OpenClaw Bot <ai-agent@topdoglabs.com>
This commit is contained in:
parent
bd2261c82f
commit
ee8158993f
75
package-lock.json
generated
75
package-lock.json
generated
@ -19,7 +19,8 @@
|
||||
"@supabase/supabase-js": "^2.97.0",
|
||||
"better-sqlite3": "^12.6.2",
|
||||
"dotenv": "^16.6.1",
|
||||
"firebase": "^12.9.0"
|
||||
"firebase": "^12.9.0",
|
||||
"resend": "^6.9.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
@ -3361,6 +3362,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@stablelib/base64": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
|
||||
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
@ -6294,6 +6301,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-sha256": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
|
||||
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
|
||||
@ -8443,6 +8456,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/postal-mime": {
|
||||
"version": "2.7.3",
|
||||
"resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.3.tgz",
|
||||
"integrity": "sha512-MjhXadAJaWgYzevi46+3kLak8y6gbg0ku14O1gO/LNOuay8dO+1PtcSGvAdgDR0DoIsSaiIA8y/Ddw6MnrO0Tw==",
|
||||
"license": "MIT-0"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
@ -8868,6 +8887,27 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resend": {
|
||||
"version": "6.9.2",
|
||||
"resolved": "https://registry.npmjs.org/resend/-/resend-6.9.2.tgz",
|
||||
"integrity": "sha512-uIM6CQ08tS+hTCRuKBFbOBvHIGaEhqZe8s4FOgqsVXSbQLAhmNWpmUhG3UAtRnmcwTWFUqnHa/+Vux8YGPyDBA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"postal-mime": "2.7.3",
|
||||
"svix": "1.84.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-email/render": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@react-email/render": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.11",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||
@ -9305,6 +9345,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/standardwebhooks": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
|
||||
"integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stablelib/base64": "^1.0.0",
|
||||
"fast-sha256": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stop-iteration-iterator": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
||||
@ -9546,6 +9596,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/svix": {
|
||||
"version": "1.84.1",
|
||||
"resolved": "https://registry.npmjs.org/svix/-/svix-1.84.1.tgz",
|
||||
"integrity": "sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"standardwebhooks": "1.0.0",
|
||||
"uuid": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.1.tgz",
|
||||
@ -10040,6 +10100,19 @@
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/victory-vendor": {
|
||||
"version": "37.3.6",
|
||||
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
||||
|
||||
@ -19,7 +19,8 @@
|
||||
"@supabase/supabase-js": "^2.97.0",
|
||||
"better-sqlite3": "^12.6.2",
|
||||
"dotenv": "^16.6.1",
|
||||
"firebase": "^12.9.0"
|
||||
"firebase": "^12.9.0",
|
||||
"resend": "^6.9.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
|
||||
@ -8,6 +8,63 @@ function hashToken(token: string): string {
|
||||
return createHash("sha256").update(token).digest("hex");
|
||||
}
|
||||
|
||||
function getAppOrigin(request: Request): string {
|
||||
const appUrl = process.env.NEXT_PUBLIC_APP_URL?.trim();
|
||||
if (appUrl) return appUrl.replace(/\/$/, "");
|
||||
|
||||
const vercelUrl = process.env.VERCEL_URL?.trim();
|
||||
if (vercelUrl) return `https://${vercelUrl}`;
|
||||
|
||||
return new URL(request.url).origin;
|
||||
}
|
||||
|
||||
async function sendResetEmail(params: {
|
||||
to: string;
|
||||
resetUrl: string;
|
||||
expiresAt: string;
|
||||
}): Promise<void> {
|
||||
const resendApiKey = process.env.RESEND_API_KEY?.trim();
|
||||
const resendFromEmail = (
|
||||
process.env.RESEND_FROM_EMAIL ||
|
||||
process.env.EMAIL_FROM
|
||||
)?.trim();
|
||||
|
||||
if (!resendApiKey || !resendFromEmail) {
|
||||
throw new Error("Resend is not configured. Missing RESEND_API_KEY or RESEND_FROM_EMAIL.");
|
||||
}
|
||||
|
||||
const response = await fetch("https://api.resend.com/emails", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${resendApiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
from: resendFromEmail,
|
||||
to: [params.to],
|
||||
subject: "Reset your password",
|
||||
html: `
|
||||
<div style="font-family:Arial,sans-serif;line-height:1.5;color:#111827;">
|
||||
<p>Password reset requested for your account.</p>
|
||||
<p>
|
||||
<a href="${params.resetUrl}" style="display:inline-block;padding:10px 14px;background:#111827;color:#ffffff;text-decoration:none;border-radius:6px;">
|
||||
Reset Password
|
||||
</a>
|
||||
</p>
|
||||
<p>This link expires at ${params.expiresAt}.</p>
|
||||
<p>If you did not request this, you can ignore this email.</p>
|
||||
</div>
|
||||
`,
|
||||
text: `Password reset requested.\n\nReset link: ${params.resetUrl}\n\nThis link expires at ${params.expiresAt}.\n\nIf you did not request this, ignore this email.`,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const details = await response.text().catch(() => "");
|
||||
throw new Error(`Resend API error (${response.status}): ${details}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = (await request.json()) as { email?: string };
|
||||
@ -53,16 +110,32 @@ export async function POST(request: Request) {
|
||||
used: false,
|
||||
});
|
||||
|
||||
// In production, you would send an email here
|
||||
// For now, log to console and return the reset link
|
||||
const resetUrl = `/reset-password?token=${token}&email=${encodeURIComponent(email)}`;
|
||||
// Build an absolute reset URL for email delivery.
|
||||
const resetPath = `/reset-password?token=${token}&email=${encodeURIComponent(email)}`;
|
||||
const resetUrl = `${getAppOrigin(request)}${resetPath}`;
|
||||
|
||||
try {
|
||||
await sendResetEmail({ to: email, resetUrl, expiresAt });
|
||||
console.log(`Password reset email sent to ${email}`);
|
||||
} catch (sendError) {
|
||||
console.error("Failed to send password reset email:", sendError);
|
||||
|
||||
// Keep local dev workflow usable even if email config is missing.
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
console.log(`\n🔐 PASSWORD RESET REQUEST for ${email}`);
|
||||
console.log(` Reset URL: ${resetUrl}`);
|
||||
console.log(` Token expires: ${expiresAt}\n`);
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ error: "Unable to send reset email. Please try again later." },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "Password reset link generated. Check server logs for the link.",
|
||||
message: "If an account exists with that email, a reset link has been sent.",
|
||||
// In dev, include the reset URL
|
||||
...(process.env.NODE_ENV !== "production" && { resetUrl }),
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user