Skip to content

Commit f93bee0

Browse files
committed
comitted reset password email
2 parents 45b73c5 + a15f999 commit f93bee0

9 files changed

Lines changed: 424 additions & 30 deletions

File tree

‎package-lock.json‎

Lines changed: 105 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-- CreateTable
2+
CREATE TABLE "PasswordResetToken" (
3+
"id" SERIAL NOT NULL,
4+
"userId" INTEGER NOT NULL,
5+
"token" TEXT NOT NULL,
6+
"expiresAt" TIMESTAMP(3) NOT NULL,
7+
"used" BOOLEAN NOT NULL DEFAULT false,
8+
9+
CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("id")
10+
);
11+
12+
-- CreateIndex
13+
CREATE UNIQUE INDEX "PasswordResetToken_token_key" ON "PasswordResetToken"("token");

‎prisma/schema.prisma‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ model UserInvite {
5959
expiration DateTime
6060
}
6161

62+
model PasswordResetToken {
63+
id Int @id @default(autoincrement())
64+
userId Int
65+
token String @unique
66+
expiresAt DateTime
67+
used Boolean @default(false)
68+
}
69+
6270
model Item {
6371
id Int @id @default(autoincrement())
6472
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { z } from "zod";
3+
import PasswordResetService from "@/services/passwordResetService";
4+
import { ArgumentError, errorResponse } from "@/util/errors";
5+
6+
const requestBodySchema = z.object({
7+
token: z.string().min(1, "Token is required"),
8+
password: z.string().min(6, "Password must be at least 6 characters long"),
9+
});
10+
11+
export async function POST(req: NextRequest) {
12+
try {
13+
const body = await req.json();
14+
const parsed = requestBodySchema.safeParse(body);
15+
16+
if (!parsed.success) {
17+
throw new ArgumentError(parsed.error.message);
18+
}
19+
20+
await PasswordResetService.changePassword(parsed.data);
21+
22+
return NextResponse.json(
23+
{ message: "Password successfully changed" },
24+
{ status: 200 }
25+
);
26+
} catch (error) {
27+
return errorResponse(error);
28+
}
29+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { z } from "zod";
3+
import PasswordResetService from "@/services/passwordResetService";
4+
import { ArgumentError, errorResponse } from "@/util/errors";
5+
6+
const requestBodySchema = z.object({
7+
email: z.string().email("Invalid email format"),
8+
});
9+
10+
export async function POST(req: NextRequest) {
11+
try {
12+
const body = await req.json();
13+
const parsed = requestBodySchema.safeParse(body);
14+
15+
if (!parsed.success) {
16+
throw new ArgumentError(parsed.error.message);
17+
}
18+
19+
await PasswordResetService.createPasswordResetToken(parsed.data);
20+
21+
return NextResponse.json(
22+
{ message: "Password reset email sent" },
23+
{ status: 200 }
24+
);
25+
} catch (error) {
26+
return errorResponse(error);
27+
}
28+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { z } from "zod";
3+
import PasswordResetService from "@/services/passwordResetService";
4+
import { ArgumentError, errorResponse } from "@/util/errors";
5+
6+
const searchParamsSchema = z.object({
7+
token: z.string().min(1, "Token is required"),
8+
});
9+
10+
export async function GET(request: NextRequest) {
11+
try {
12+
const token = request.nextUrl.searchParams.get("token");
13+
const parsed = searchParamsSchema.safeParse({ token });
14+
15+
if (!parsed.success) {
16+
throw new ArgumentError(parsed.error.message);
17+
}
18+
19+
const result = await PasswordResetService.verifyPasswordResetToken(
20+
parsed.data.token
21+
);
22+
23+
return NextResponse.json(result, { status: 200 });
24+
} catch (error) {
25+
return errorResponse(error);
26+
}
27+
}

‎src/email/index.ts‎

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,20 @@ import { render } from "@react-email/render";
33
import { writeFileSync, mkdirSync } from "fs";
44
import { join } from "path";
55
import { exec } from "child_process";
6-
import CreateAccountInvite, { CreateAccountInviteProps } from "./templates/CreateAccountInvite";
7-
import ItemsVisibleNotification, { ItemsVisibleNotificationProps } from "./templates/ItemsVisibleNotification";
8-
import DonorOfferCreated, { DonorOfferCreatedProps } from "./templates/DonorOfferCreated";
9-
import CreateAccountReminder, { CreateAccountReminderProps } from "./templates/CreateAccountReminder";
6+
import CreateAccountInvite, {
7+
CreateAccountInviteProps,
8+
} from "./templates/CreateAccountInvite";
9+
import ItemsVisibleNotification, {
10+
ItemsVisibleNotificationProps,
11+
} from "./templates/ItemsVisibleNotification";
12+
import DonorOfferCreated, {
13+
DonorOfferCreatedProps,
14+
} from "./templates/DonorOfferCreated";
15+
import CreateAccountReminder, {
16+
CreateAccountReminderProps,
17+
} from "./templates/CreateAccountReminder";
1018
import ExpiringItems, { ExpiringItemsProps } from "./templates/ExpiringItems";
19+
import ResetPassword, { ResetPasswordProps } from "./templates/ResetPassword";
1120

1221
const apiKey = process.env.SENDGRID_API_KEY as string;
1322
const fromEmail = process.env.SENDGRID_SENDER as string;
@@ -43,50 +52,50 @@ export async function sendEmail(
4352
html: string
4453
): Promise<[SendGrid.ClientResponse, object] | void> {
4554
if (openLocally) {
46-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
55+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
4756
const filename = `email-preview-${timestamp}.html`;
48-
const previewsDir = join(process.cwd(), 'email-previews');
57+
const previewsDir = join(process.cwd(), "email-previews");
4958
const filepath = join(previewsDir, filename);
50-
59+
5160
try {
5261
writeFileSync(filepath, html);
5362
} catch {
5463
mkdirSync(previewsDir, { recursive: true });
5564
writeFileSync(filepath, html);
5665
}
57-
58-
const relativePath = `email-previews/${filename}`;
59-
console.log(`📧 Email preview saved: ${relativePath}`);
60-
61-
const fileUrl = `file://${filepath}`;
62-
const platform = process.platform;
63-
66+
67+
const relativePath = `email-previews/${filename}`;
68+
console.log(`📧 Email preview saved: ${relativePath}`);
69+
70+
const fileUrl = `file://${filepath}`;
71+
const platform = process.platform;
72+
6473
let command: string;
65-
if (platform === 'darwin') {
74+
if (platform === "darwin") {
6675
command = `open -n "${fileUrl}"`;
67-
} else if (platform === 'win32') {
76+
} else if (platform === "win32") {
6877
command = `start "" "${fileUrl}"`;
6978
} else {
7079
command = `xdg-open "${fileUrl}"`;
7180
}
72-
81+
7382
exec(command, (error) => {
7483
if (error) {
75-
console.log(`📧 Could not auto-open browser. Please open manually: ${fileUrl}`);
84+
console.log(
85+
`📧 Could not auto-open browser. Please open manually: ${fileUrl}`
86+
);
7687
} else {
7788
console.log(`📧 Email preview opened in browser`);
7889
}
7990
});
80-
91+
8192
return;
8293
}
8394

84-
return SendGrid.send({
85-
to,
86-
from: fromEmail,
87-
subject,
88-
html,
89-
}, Array.isArray(to));
95+
return SendGrid.send(
96+
{ to, from: fromEmail, subject, html },
97+
Array.isArray(to)
98+
);
9099
}
91100

92101
export class EmailClient {
@@ -95,15 +104,15 @@ export class EmailClient {
95104
return sendEmail(to, "Your Invite Link", html);
96105
}
97106

98-
static async sendUserInviteReminder(to: string, props: CreateAccountReminderProps) {
107+
static async sendUserInviteReminder(
108+
to: string,
109+
props: CreateAccountReminderProps
110+
) {
99111
const html = await render(CreateAccountReminder(props));
100112
return sendEmail(to, "Reminder: Complete Your Account", html);
101113
}
102114

103-
static async sendItemsExpiring(
104-
to: string,
105-
props: ExpiringItemsProps
106-
) {
115+
static async sendItemsExpiring(to: string, props: ExpiringItemsProps) {
107116
const html = await render(ExpiringItems(props));
108117
return sendEmail(to, `Items Expiring in ${props.month}`, html);
109118
}
@@ -123,4 +132,9 @@ export class EmailClient {
123132
const html = await render(DonorOfferCreated(props));
124133
return sendEmail(to, "Donor Offer Created", html);
125134
}
135+
136+
static async sendPasswordReset(to: string, props: ResetPasswordProps) {
137+
const html = await render(ResetPassword(props));
138+
return sendEmail(to, "Reset Your Password", html);
139+
}
126140
}

0 commit comments

Comments
 (0)