diff --git a/app/(auth)/reset-password/page.tsx b/app/(auth)/reset-password/page.tsx index e25bcb88..a804d27a 100644 --- a/app/(auth)/reset-password/page.tsx +++ b/app/(auth)/reset-password/page.tsx @@ -1,15 +1,59 @@ -import {PageParams} from "@/types/next"; -import {Metadata} from "next"; -import {ResetPasswordSection} from "@/components/wrappers/auth/reset-password/reset-password-section"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { ResetPasswordForm } from "@/components/wrappers/auth/login/reset-password-form/reset-password-form"; +import { redirect } from "next/navigation"; +import { auth } from "@/lib/auth/auth"; +import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar"; -export const metadata: Metadata = { - title: "Reset Password", -}; +export default async function RoutePage(props: { searchParams: Promise<{ token: string | undefined }> }) { + + const { token } = await props.searchParams; + + if (!token) { + return redirect(`/login?error=invalid_or_expired_token`); + } + + const verification = await (await auth.$context).internalAdapter.findVerificationValue(`reset-password:${token}`); + + if (!verification || verification.expiresAt < new Date()) { + return redirect(`/login?error=invalid_or_expired_token`); + } + + const user = await (await auth.$context).internalAdapter.findUserById(verification.value); -export default async function RoutePage(props: PageParams<{}>) { return ( -
- -
- ) -} \ No newline at end of file + + + +
+

Set a new password

+

Please enter your new password below.

+
+ +
+ + + + {user!.name + .split(" ") + .map((n) => n[0]) + .join("") + .toUpperCase() + .slice(0, 2)} + + + +
+
{user!.name}
+
{user!.email}
+
+
+
+ + + + +
+
+ ); +} diff --git a/src/components/emails/auth/email-forgot-password.tsx b/src/components/emails/auth/email-forgot-password.tsx new file mode 100644 index 00000000..abecc5fe --- /dev/null +++ b/src/components/emails/auth/email-forgot-password.tsx @@ -0,0 +1,38 @@ +import * as React from "react"; +import EmailLayout from "../email-layout"; +import {Heading, Text, Section, Button} from "@react-email/components"; +import {getServerUrl} from "@/utils/get-server-url"; + +interface EmailCreateUserProps { + firstname?: string; + token: string; +} + +export const EmailForgotPassword = ({firstname, token}: EmailCreateUserProps) => { + const baseUrl = getServerUrl(); + + return ( + + + Hello {firstname}, + + + We received a request to reset the password for your account associated with this email. + + + If you did not request this password reset, please ignore this email. Your current password + will remain unchanged. If this seems suspicious, please contact your administrator. + +
+ +
+
+ ); +}; + +export default EmailForgotPassword; diff --git a/src/components/emails/auth/email-new-login.tsx b/src/components/emails/auth/email-new-login.tsx new file mode 100644 index 00000000..65b75958 --- /dev/null +++ b/src/components/emails/auth/email-new-login.tsx @@ -0,0 +1,32 @@ +import * as React from "react"; +import EmailLayout from "../email-layout"; +import {Heading, Text} from "@react-email/components"; + +interface EmailCreateUserProps { + firstname?: string; + os: string; + browser: string; + ipAddress?: string; +} + +export const EmailNewLogin = ({firstname, ipAddress, os, browser}: EmailCreateUserProps) => { + return ( + + + Hello {firstname}, + + + We detected a new login to your account. + + + Login details: +
+ Device: {os} - {browser} +
+ IP Address: {ipAddress} +
+
+ ); +}; + +export default EmailNewLogin; diff --git a/src/components/emails/email-verification.tsx b/src/components/emails/auth/email-verification.tsx similarity index 100% rename from src/components/emails/email-verification.tsx rename to src/components/emails/auth/email-verification.tsx diff --git a/src/components/emails/email-reset-password.tsx b/src/components/emails/email-reset-password.tsx deleted file mode 100644 index 876901a9..00000000 --- a/src/components/emails/email-reset-password.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from "react"; -import EmailLayout from "./email-layout"; -import {Text, Section, Button} from "@react-email/components"; - -export interface EmailResetPasswordProps { - url: string; -} - -export const EmailResetPassword = ({url}: EmailResetPasswordProps) => { - return ( - - Hello ! - You are receiving this email because we - received a password reset request for your account.{" "} -
- -
- If you did not request a password reset, no - further action is required. - Regards,
Portabase
-
- ); -}; - -export default EmailResetPassword; diff --git a/src/components/wrappers/auth/login/forgot-password-form/forgot-password-form.tsx b/src/components/wrappers/auth/login/forgot-password-form/forgot-password-form.tsx index f30d5fc0..51be6eb6 100644 --- a/src/components/wrappers/auth/login/forgot-password-form/forgot-password-form.tsx +++ b/src/components/wrappers/auth/login/forgot-password-form/forgot-password-form.tsx @@ -30,7 +30,7 @@ export const ForgotPasswordForm = (props: ForgotPasswordFormProps) => { }, { onSuccess: () => { - toast.success("Password reset successfully."); + toast.success("If an account with this email address exists, you will receive an email with instructions to reset your password."); }, onError: (error) => { toast.error(error.error.message); diff --git a/src/components/wrappers/auth/login/login-form/login-form.tsx b/src/components/wrappers/auth/login/login-form/login-form.tsx index cb94fb21..9b19b6b3 100644 --- a/src/components/wrappers/auth/login/login-form/login-form.tsx +++ b/src/components/wrappers/auth/login/login-form/login-form.tsx @@ -63,6 +63,7 @@ export const LoginForm = (props: loginFormProps) => { toast.success("Login success"); }, onError: (error) => { + console.log(error); toast.error(error.error.message); }, } diff --git a/src/features/notifications/providers/smtp.ts b/src/features/notifications/providers/smtp.ts index 41c8ba99..5cea2205 100644 --- a/src/features/notifications/providers/smtp.ts +++ b/src/features/notifications/providers/smtp.ts @@ -1,70 +1,8 @@ -// "use server" -// import type {EventPayload, DispatchResult} from '../types'; -// import nodemailer from 'nodemailer'; -// import TestEmailSettings from "@/components/emails/email-settings-test" -// -// export async function sendSmtp( -// config: { -// host: string; -// port: number; -// secure: boolean; -// user: string; -// password: string; -// from: string; -// to: string | string[]; -// }, -// payload: EventPayload -// ): Promise { -// console.log(config) -// const transporter = nodemailer.createTransport({ -// pool: true, -// host: config.host, -// port: config.port, -// // secure: config.secure, -// secure: true, -// auth: {user: config.user, pass: config.password}, -// }); -// -// -// -// const result = await transporter.verify(); -// console.log(result); -// -// const html = ` -//

${payload.title}

-//

Level: ${payload.level}

-//

${payload.message.replace(/\n/g, '
')}

-// ${payload.data ? `
${JSON.stringify(payload.data, null, 2)}
` : ''} -// `; -// -// const info = await transporter.sendMail({ -// from: config.from, -// to: Array.isArray(config.to) ? config.to.join(', ') : config.to, -// // to: config.from, -// subject: `[${payload.level.toUpperCase()}] ${payload.title}`, -// html, -// // subject: "Portabase", -// // html: await render(TestEmailSettings(), {}), -// }); -// -// console.log(info); -// -// return { -// success: true, -// provider: 'smtp', -// // message: `Email sent: ${info.messageId}`, -// message: `Email sent: ${config.to}`, -// response: info, -// }; -// } - - "use server"; import { render } from "@react-email/render"; import nodemailer from "nodemailer"; import type { EventPayload, DispatchResult } from '../types'; import EmailNotification from "@/components/emails/email-notification"; -import EmailResetPassword from "@/components/emails/email-reset-password"; export async function sendSmtp( config: { diff --git a/src/lib/auth/auth.ts b/src/lib/auth/auth.ts index ea066832..6a393eb7 100644 --- a/src/lib/auth/auth.ts +++ b/src/lib/auth/auth.ts @@ -11,10 +11,12 @@ import {count, eq} from "drizzle-orm"; import {MemberWithUser, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization"; import {sendEmail} from "@/lib/email/email-helper"; import {render} from "@react-email/render"; -import EmailResetPassword from "@/components/emails/email-reset-password"; import {SUPPORTED_PROVIDERS} from "../../../portabase.config"; import {withUpdatedAt} from "@/db/utils"; -import EmailVerification from "@/components/emails/email-verification"; +import EmailVerification from "@/components/emails/auth/email-verification"; +import EmailForgotPassword from "@/components/emails/auth/email-forgot-password"; +import {getDeviceDetails} from "@/utils/detection"; +import EmailNewLogin from "@/components/emails/auth/email-new-login"; export const auth = betterAuth({ database: drizzleAdapter(db, { @@ -27,7 +29,7 @@ export const auth = betterAuth({ emailAndPassword: { enabled: true, requireEmailVerification: false, - sendResetPassword: async ({user, url, token}, request) => { + sendResetPassword: async ({user, token}, request) => { const [updatedUser] = await db.update(drizzleDb.schemas.user).set(withUpdatedAt({ emailVerified: true, @@ -36,7 +38,13 @@ export const auth = betterAuth({ await sendEmail({ to: user.email, subject: "Reset your password", - html: await render(EmailResetPassword({url: url})), + html: await render( + EmailForgotPassword({ + firstname: user.name!, + token, + }), + {} + ), }); }, @@ -71,8 +79,6 @@ export const auth = betterAuth({ }); }, }, - - socialProviders: SUPPORTED_PROVIDERS.reduce((acc: any, provider: any) => { if (provider.id === "credential") return acc; if (provider.id === "google") { @@ -89,7 +95,6 @@ export const auth = betterAuth({ }, }, - plugins: [ openAPI(), nextCookies(), @@ -176,7 +181,6 @@ export const auth = betterAuth({ } else { console.warn("Default organization not found. Cannot assign member."); } - }, }, }, @@ -185,38 +189,42 @@ export const auth = betterAuth({ before: async (session, context) => { const userId = session.userId; - let memberships = await db.query.member.findMany({ where: eq(drizzleDb.schemas.member.userId, userId), }); - // if (!memberships.length) { - // const defaultOrgSlug = "default"; - // const defaultOrg = await db.query.organization.findFirst({ - // where: eq(drizzleDb.schemas.organization.slug, defaultOrgSlug), - // }); - // - // if (!defaultOrg) { - // throw new Error("No organization found. Cannot assign member."); - // } - // - // await db.insert(drizzleDb.schemas.member).values({ - // userId, - // organizationId: defaultOrg.id, - // role: "member", - // }); - // - // memberships = await db.query.member.findMany({ - // where: eq(drizzleDb.schemas.member.userId, userId), - // }); - // } - return { data: { activeOrganizationId: memberships[0].organizationId, }, }; }, + after: async (session) => { + const user = await db.query.user.findFirst({ + where: eq(drizzleDb.schemas.user.id, session.userId), + }); + + if (user && user.role != "pending") { + const deviceInfo = getDeviceDetails(session.userAgent); + await sendEmail({ + to: user.email, + subject: "New login to your account", + html: await render( + EmailNewLogin({ + firstname: user.name!, + os: deviceInfo.os, + browser: deviceInfo.browser, + ipAddress: session.ipAddress!, + }), + {} + ), + }); + + (await auth.$context).internalAdapter.updateUser(user.id, { + lastConnectedAt: new Date(), + }); + } + }, }, },