mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Working and testing the profile module.
This commit is contained in:
@@ -1,15 +1,59 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||||
import {Metadata} from "next";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import {ResetPasswordSection} from "@/components/wrappers/auth/reset-password/reset-password-section";
|
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 = {
|
export default async function RoutePage(props: { searchParams: Promise<{ token: string | undefined }> }) {
|
||||||
title: "Reset Password",
|
|
||||||
};
|
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 (
|
return (
|
||||||
<div className="mx-auto grid w-full gap-6">
|
<TooltipProvider>
|
||||||
<ResetPasswordSection/>
|
<Card className="w-full max-w-md shadow-lg">
|
||||||
</div>
|
<CardHeader className="space-y-4">
|
||||||
)
|
<div className="space-y-1 text-center">
|
||||||
|
<h1 className="text-2xl font-bold tracking-tight">Set a new password</h1>
|
||||||
|
<p className="text-sm text-muted-foreground text-balance">Please enter your new password below.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center space-y-2 text-center">
|
||||||
|
<Avatar className="relative flex h-16 w-16 shrink-0 overflow-hidden rounded-full border">
|
||||||
|
<AvatarImage src={user!.image ?? ""} alt={user!.name} className="aspect-square h-full w-full object-cover" />
|
||||||
|
<AvatarFallback className="flex h-full w-full items-center justify-center rounded-full bg-muted text-2xl font-medium text-muted-foreground">
|
||||||
|
{user!.name
|
||||||
|
.split(" ")
|
||||||
|
.map((n) => n[0])
|
||||||
|
.join("")
|
||||||
|
.toUpperCase()
|
||||||
|
.slice(0, 2)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="font-semibold text-lg">{user!.name}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">{user!.email}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<ResetPasswordForm />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -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 (
|
||||||
|
<EmailLayout preview="Portabase - Forgot Password">
|
||||||
|
<Heading className="mx-0 my-[30px] p-0 text-center font-normal text-[24px] text-black">
|
||||||
|
Hello <strong>{firstname}</strong>,
|
||||||
|
</Heading>
|
||||||
|
<Text className="text-[14px] text-black leading-[24px]">
|
||||||
|
We received a request to reset the password for your account associated with this email.
|
||||||
|
</Text>
|
||||||
|
<Text className="text-[14px] text-black leading-[24px]">
|
||||||
|
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.
|
||||||
|
</Text>
|
||||||
|
<Section className="mt-[32px] mb-[32px] text-center">
|
||||||
|
<Button
|
||||||
|
className="rounded bg-[#000000] px-5 py-3 text-center font-semibold text-[12px] text-white no-underline"
|
||||||
|
href={`${baseUrl}/reset-password?token=${token}`}
|
||||||
|
>
|
||||||
|
Reset my password
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
</EmailLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmailForgotPassword;
|
||||||
@@ -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 (
|
||||||
|
<EmailLayout preview="Portabase - New Login">
|
||||||
|
<Heading className="mx-0 my-[30px] p-0 text-center font-normal text-[24px] text-black">
|
||||||
|
Hello <strong>{firstname}</strong>,
|
||||||
|
</Heading>
|
||||||
|
<Text className="text-[14px] text-black leading-[24px]">
|
||||||
|
We detected a new login to your account.
|
||||||
|
</Text>
|
||||||
|
<Text className="text-[14px] text-black leading-[24px]">
|
||||||
|
<strong>Login details:</strong>
|
||||||
|
<br/>
|
||||||
|
Device: {os} - {browser}
|
||||||
|
<br/>
|
||||||
|
IP Address: {ipAddress}
|
||||||
|
</Text>
|
||||||
|
</EmailLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmailNewLogin;
|
||||||
@@ -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 (
|
|
||||||
<EmailLayout preview="Email for password reset of your Portabase account">
|
|
||||||
<Text className="text-base font-bold ">Hello !</Text>
|
|
||||||
<Text className="text-base font-light ">You are receiving this email because we
|
|
||||||
received a password reset request for your account.</Text>{" "}
|
|
||||||
<Section className="mt-[32px] mb-[32px] text-center">
|
|
||||||
<Button
|
|
||||||
className="rounded bg-[#000000] px-5 py-3 text-center font-semibold text-[12px] text-white no-underline"
|
|
||||||
href={url}
|
|
||||||
>
|
|
||||||
Reset Password
|
|
||||||
</Button>
|
|
||||||
</Section>
|
|
||||||
<Text className="text-base font-light ">If you did not request a password reset, no
|
|
||||||
further action is required.</Text>
|
|
||||||
<Text className="text-base font-light ">Regards,<br/>Portabase</Text>
|
|
||||||
</EmailLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EmailResetPassword;
|
|
||||||
@@ -30,7 +30,7 @@ export const ForgotPasswordForm = (props: ForgotPasswordFormProps) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
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) => {
|
onError: (error) => {
|
||||||
toast.error(error.error.message);
|
toast.error(error.error.message);
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export const LoginForm = (props: loginFormProps) => {
|
|||||||
toast.success("Login success");
|
toast.success("Login success");
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
|
console.log(error);
|
||||||
toast.error(error.error.message);
|
toast.error(error.error.message);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<DispatchResult> {
|
|
||||||
// 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 = `
|
|
||||||
// <h2>${payload.title}</h2>
|
|
||||||
// <p><strong>Level:</strong> ${payload.level}</p>
|
|
||||||
// <p>${payload.message.replace(/\n/g, '<br>')}</p>
|
|
||||||
// ${payload.data ? `<pre>${JSON.stringify(payload.data, null, 2)}</pre>` : ''}
|
|
||||||
// `;
|
|
||||||
//
|
|
||||||
// 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";
|
"use server";
|
||||||
import { render } from "@react-email/render";
|
import { render } from "@react-email/render";
|
||||||
import nodemailer from "nodemailer";
|
import nodemailer from "nodemailer";
|
||||||
import type { EventPayload, DispatchResult } from '../types';
|
import type { EventPayload, DispatchResult } from '../types';
|
||||||
import EmailNotification from "@/components/emails/email-notification";
|
import EmailNotification from "@/components/emails/email-notification";
|
||||||
import EmailResetPassword from "@/components/emails/email-reset-password";
|
|
||||||
|
|
||||||
export async function sendSmtp(
|
export async function sendSmtp(
|
||||||
config: {
|
config: {
|
||||||
|
|||||||
+38
-30
@@ -11,10 +11,12 @@ import {count, eq} from "drizzle-orm";
|
|||||||
import {MemberWithUser, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
import {MemberWithUser, OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
||||||
import {sendEmail} from "@/lib/email/email-helper";
|
import {sendEmail} from "@/lib/email/email-helper";
|
||||||
import {render} from "@react-email/render";
|
import {render} from "@react-email/render";
|
||||||
import EmailResetPassword from "@/components/emails/email-reset-password";
|
|
||||||
import {SUPPORTED_PROVIDERS} from "../../../portabase.config";
|
import {SUPPORTED_PROVIDERS} from "../../../portabase.config";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
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({
|
export const auth = betterAuth({
|
||||||
database: drizzleAdapter(db, {
|
database: drizzleAdapter(db, {
|
||||||
@@ -27,7 +29,7 @@ export const auth = betterAuth({
|
|||||||
emailAndPassword: {
|
emailAndPassword: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
requireEmailVerification: false,
|
requireEmailVerification: false,
|
||||||
sendResetPassword: async ({user, url, token}, request) => {
|
sendResetPassword: async ({user, token}, request) => {
|
||||||
|
|
||||||
const [updatedUser] = await db.update(drizzleDb.schemas.user).set(withUpdatedAt({
|
const [updatedUser] = await db.update(drizzleDb.schemas.user).set(withUpdatedAt({
|
||||||
emailVerified: true,
|
emailVerified: true,
|
||||||
@@ -36,7 +38,13 @@ export const auth = betterAuth({
|
|||||||
await sendEmail({
|
await sendEmail({
|
||||||
to: user.email,
|
to: user.email,
|
||||||
subject: "Reset your password",
|
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) => {
|
socialProviders: SUPPORTED_PROVIDERS.reduce((acc: any, provider: any) => {
|
||||||
if (provider.id === "credential") return acc;
|
if (provider.id === "credential") return acc;
|
||||||
if (provider.id === "google") {
|
if (provider.id === "google") {
|
||||||
@@ -89,7 +95,6 @@ export const auth = betterAuth({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
openAPI(),
|
openAPI(),
|
||||||
nextCookies(),
|
nextCookies(),
|
||||||
@@ -176,7 +181,6 @@ export const auth = betterAuth({
|
|||||||
} else {
|
} else {
|
||||||
console.warn("Default organization not found. Cannot assign member.");
|
console.warn("Default organization not found. Cannot assign member.");
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -185,38 +189,42 @@ export const auth = betterAuth({
|
|||||||
before: async (session, context) => {
|
before: async (session, context) => {
|
||||||
const userId = session.userId;
|
const userId = session.userId;
|
||||||
|
|
||||||
|
|
||||||
let memberships = await db.query.member.findMany({
|
let memberships = await db.query.member.findMany({
|
||||||
where: eq(drizzleDb.schemas.member.userId, userId),
|
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 {
|
return {
|
||||||
data: {
|
data: {
|
||||||
activeOrganizationId: memberships[0].organizationId,
|
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(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user