Working on settings. Adding .env value take in count when they are present in order to prefill the Settings on startup. Adding the SendEMail Feature and the form in settings.

This commit is contained in:
charles-gauthereau
2024-11-17 16:31:34 +01:00
parent ff9e3ffa67
commit 0de1c4daa2
24 changed files with 1701 additions and 66 deletions
+1 -3
View File
@@ -12,13 +12,11 @@ export default async function Layout({children}: { children: React.ReactNode })
email: user?.email
}
})
if(userInfo){
if(userInfo && userInfo.authMethod){
redirect('/dashboard')
}
}
return (
<LayoutAdmin>
<div
+12 -10
View File
@@ -1,15 +1,17 @@
import {PageParams} from "@/types/next";
import {redirect} from "next/navigation";
export default async function RoutePage(props: PageParams<{}>) {
return (
<div className="flex flex-1 flex-col gap-4 p-4">
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
<div className="aspect-video rounded-xl bg-muted/50"/>
<div className="aspect-video rounded-xl bg-muted/50"/>
<div className="aspect-video rounded-xl bg-muted/50"/>
</div>
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min"/>
</div>
)
// return (
// <div className="flex flex-1 flex-col gap-4 p-4">
// <div className="grid auto-rows-min gap-4 md:grid-cols-3">
// <div className="aspect-video rounded-xl bg-muted/50"/>
// <div className="aspect-video rounded-xl bg-muted/50"/>
// <div className="aspect-video rounded-xl bg-muted/50"/>
// </div>
// <div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min"/>
// </div>
// )
}
+8 -6
View File
@@ -1,12 +1,14 @@
import {PageParams} from "@/types/next";
import {Button} from "@/components/ui/button";
import {redirect} from "next/navigation";
export default async function RoutePage(props: PageParams<{}>) {
redirect("/dashboard/agents")
return(
<div>
<h1>Ok</h1>
<Button>Button</Button>
</div>
)
// return(
// <div>
// <h1>Ok</h1>
// <Button>Button</Button>
// </div>
// )
}
@@ -2,15 +2,12 @@ import {PageParams} from "@/types/next";
import {Page, PageContent, PageDescription, PageHeader, PageTitle} from "@/features/layout/page";
import {prisma} from "@/prisma";
import {requiredCurrentUser} from "@/auth/current-user";
import {DataTableWithPagination} from "@/components/wrappers/table/data-table-with-pagination";
import {usersColumns} from "@/components/wrappers/Dashboard/Settings/SettingsTabs/columns-users";
import {SettingsTabs} from "@/components/wrappers/Dashboard/Settings/SettingsTabs/SettingsTabs";
export default async function RoutePage(props: PageParams<{}>) {
const user = await requiredCurrentUser()
const users = await prisma.user.findMany({
where:{
id: {
+1
View File
@@ -18,6 +18,7 @@ services:
volumes:
- .:/app
- /app/node_modules
container_name: portabase-app
db:
image: postgres:16-alpine
+13
View File
@@ -0,0 +1,13 @@
import {
Button as ReactEmailButton
} from "@react-email/components";
import {ComponentPropsWithoutRef} from "react";
export const Button = (props: ComponentPropsWithoutRef<typeof ReactEmailButton>) => {
return (
<ReactEmailButton
className="block w-52 rounded bg-blue-600 py-3.5 text-center text-sm font-normal text-white no-underline "
{...props}
/>
)
}
+50
View File
@@ -0,0 +1,50 @@
import {
Body,
Button,
Container,
Head,
Html,
Img,
Link,
Preview,
Section,
Tailwind,
Text,
} from "@react-email/components";
import * as React from "react";
import { PropsWithChildren } from "react";
import {getServerUrl} from "@/utils/get-server-url";
const baseUrl = getServerUrl();
export const EmailLayout = ({ children, preview }: PropsWithChildren<{ preview?: string }>) => {
return (
<Tailwind>
<Html>
<Head />
{preview ? <Preview>{preview}</Preview> : <Preview>Please check your mails</Preview>}
<Body className="bg-gray-100 py-4"
style={{fontFamily: "Arial, sans-serif"}}>
<Container className="bg-white border border-gray-200 p-12">
<Img
src={`${baseUrl}/logo-title-black.png`}
width="400"
height="auto"
alt="Logo"
/>
<Section>
{children}
</Section>
</Container>
</Body>
</Html>
</Tailwind>
);
};
export default EmailLayout;
+28
View File
@@ -0,0 +1,28 @@
import {
Text,
} from "@react-email/components";
import * as React from "react";
import EmailLayout from "./EmailLayout";
export const TestEmailSettings = () => {
return (
<EmailLayout
preview="Test Email"
>
<Text
className="text-base font-light leading-8 text-green-800 "
>
Hi, this is a test email for settings and alerts !
</Text>
<Text
className="text-base font-light leading-8 text-green-800 ">
Soluce Technologies | Portabase
</Text>
</EmailLayout>
);
};
export default TestEmailSettings;
+13
View File
@@ -0,0 +1,13 @@
import {
Text as ReactEmailText
} from "@react-email/components";
import {ComponentPropsWithoutRef} from "react";
export const Text = (props: ComponentPropsWithoutRef<typeof ReactEmailText>) => {
return (
<ReactEmailText
className="text-base font-light leading-8 text-green-800 "
{...props}
/>
)
}
+1
View File
@@ -31,6 +31,7 @@ function checkRouteExists(pathname) {
// /^\/api\/auth\/\w+$/, // Dynamic route with a number as a parameter (e.g., /api/dynamic/123)
// /^\/api\/agent\/healthcheck\/\w+$/, // Dynamic route with an alphanumeric parameter (e.g., /api/user/username)
/^\/api\/agent\/[^/]+\/status\/?$/, // Dynamic route for /api/agent/[id]/status
/^\/api\/logs\$/,
];
return routePatterns.some(pattern => pattern.test(pathname));
}
+4
View File
@@ -40,6 +40,7 @@
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.3",
"@react-email/components": "^0.0.28",
"@t3-oss/env-nextjs": "^0.11.1",
"@tanstack/react-query": "^5.59.18",
"@tanstack/react-table": "^8.20.5",
@@ -49,6 +50,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"dockerode": "^4.0.2",
"embla-carousel-react": "^8.3.1",
"input-otp": "^1.4.0",
"lucide-react": "^0.454.0",
@@ -58,9 +60,11 @@
"next-intl": "^3.24.0",
"next-safe-action": "^7.4.3",
"next-themes": "^0.3.0",
"nodemailer": "^6.9.16",
"react": "19.0.0-rc-66855b96-20241106",
"react-day-picker": "^8.10.1",
"react-dom": "19.0.0-rc-66855b96-20241106",
"react-email": "^3.0.2",
"react-hook-form": "^7.53.1",
"react-resizable-panels": "^2.1.6",
"react-twc": "^1.4.2",
@@ -0,0 +1,40 @@
"use client"
import {Button} from "@/components/ui/button";
import {useState} from "react";
import {Loader2} from "lucide-react";
export type VariantButton = {
secondary: string
default: string
outline: string
ghost: string
link: string
destructive: string
}
export type ButtonWithConfirmProps = {
icon?: any,
text: string,
variant?: keyof VariantButton ,
className?: string,
onClick: () => void,
isPending? : boolean
};
export const ButtonWithLoading = (props: ButtonWithConfirmProps) => {
return(
<Button
onClick={() => {
props.onClick()
}}
variant={props.variant ? props.variant : "default"}
className={props.className}
>
{props.isPending && <Loader2 className="animate-spin mr-4" size={16}/>}
{props.text}
<>
{props.icon ? props.icon : null}
</>
</Button>
)
}
@@ -14,26 +14,50 @@ import {
EmailFormSchema,
EmailFormType
} from "@/components/wrappers/Dashboard/Settings/SettingsEmailTab/EmailForm/email-form.schema";
import Link from "next/link";
import {PasswordInput} from "@/components/wrappers/Auth/PaswordInput/password-input";
import {
updateEmailSettingsAction
} from "@/components/wrappers/Dashboard/Settings/SettingsEmailTab/EmailForm/email-form.action";
import {toast} from "sonner";
import {useRouter} from "next/navigation";
export type EmailFormProps = {
defaultValues?: EmailFormType;
}
export const EmailForm = (props: EmailFormProps) => {
const isCreate = !Boolean(props.defaultValues)
const form = useZodForm({
schema: EmailFormSchema,
defaultValues: props.defaultValues,
});
const router = useRouter();
const mutation = useMutation({
mutationFn: async (values: EmailFormType) => {
const updateEmailSettings = await updateEmailSettingsAction({name: "system", data: values})
const data = updateEmailSettings?.data?.data
if (updateEmailSettings?.serverError || !data) {
console.log(updateEmailSettings?.serverError);
toast.error(updateEmailSettings?.serverError);
return;
}
toast.success(`Success updating email informations`);
router.refresh()
}
})
return (
<TooltipProvider>
<Card>
<CardContent>
<Form form={form}
className="flex flex-col gap-4 mt-3"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
@@ -42,6 +66,8 @@ export const EmailForm = (props: EmailFormProps) => {
<FormField
control={form.control}
name="smtpFrom"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>From Email *</FormLabel>
@@ -54,10 +80,81 @@ export const EmailForm = (props: EmailFormProps) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="smtpHost"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>Server Host *</FormLabel>
<FormControl>
<Input
placeholder={"ssl0.ovh.net"} {...field} />
</FormControl>
<FormDescription>{"Your email server host"}</FormDescription>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="smtpPort"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>Server Port *</FormLabel>
<FormControl>
<Input
placeholder={"465"} {...field} />
</FormControl>
<FormDescription>{"Your email server port (send)"}</FormDescription>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="smtpPassword"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<PasswordInput placeholder="Password" {...field}/>
</FormControl>
<FormDescription>{"Your email server password"}</FormDescription>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="smtpUser"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>User Email *</FormLabel>
<FormControl>
<Input
placeholder={"exemple@portabase.com"} {...field} />
</FormControl>
<FormDescription>{"The email server user"}</FormDescription>
<FormMessage/>
</FormItem>
)}
/>
<div className="flex justify-end gap-4">
<Button>
Save
</Button>
</div>
<Button>
Save
</Button>
</Form>
</CardContent>
</Card>
@@ -0,0 +1,31 @@
"use server"
import {userAction} from "@/safe-actions";
import {z} from "zod";
import {prisma} from "@/prisma";
import {EmailFormSchema} from "@/components/wrappers/Dashboard/Settings/SettingsEmailTab/EmailForm/email-form.schema";
export const updateEmailSettingsAction = userAction
.schema(
z.object({
name: z.string(),
data: EmailFormSchema,
}
)
)
.action(async ({parsedInput, ctx}) => {
console.log("parsedInput", parsedInput.data)
const updatedSettings = await prisma.settings.update({
where: {
name: parsedInput.name,
},
data: parsedInput.data,
})
return {
data: updatedSettings,
}
})
@@ -1,12 +1,60 @@
import {EmailForm} from "@/components/wrappers/Dashboard/Settings/SettingsEmailTab/EmailForm/EmailForm";
import {Settings} from "@prisma/client";
import {Send} from "lucide-react";
import {ButtonWithLoading} from "@/components/wrappers/Button/ButtonWithLoading/ButtonWithLoading";
import {useMutation} from "@tanstack/react-query";
import {sendEmail} from "@/utils/email-helper";
import TestEmailSettings from "../../../../../../emails/TestEmailSettings";
import {render} from "@react-email/render";
import {toast} from "sonner";
export type SettingsEmailTabProps = {}
export const SettingsEmailTab = (props:SettingsEmailTabProps) => {
return(
<div>
<EmailForm/>
export type SettingsEmailTabProps = {
settings: Settings
}
export const SettingsEmailTab = (props: SettingsEmailTabProps) => {
const mutation = useMutation({
mutationFn: async () => {
const email = await sendEmail({
to: props.settings.smtpUser,
subject: "Portabase email test !",
html: await render(TestEmailSettings(), {})
});
if(email.response){
toast.success("Test Email Successfully sent !");
}
}
})
const handleSendMailTest = async () => {
console.log("Sending email test...");
await mutation.mutateAsync()
}
return (
<div className="flex flex-col h-full py-4">
<div className="flex gap-4 h-fit justify-between">
<h1>Settings for Portabase storage</h1>
{props.settings.smtpFrom && (
<ButtonWithLoading
isPending={mutation.isPending}
onClick={async () => {
await handleSendMailTest()
}}
icon={<Send/>}
text="Send email test"
/>
)}
</div>
<div className="mt-5">
<EmailForm defaultValues={props.settings.smtpFrom ? props.settings : null}/>
</div>
</div>
)
}
@@ -40,7 +40,7 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
</div>
{isSwitched && (
<div className="mt-5">
<StorageS3Form/>
<StorageS3Form defaultValues={props.settings.s3EndPointUrl ? props.settings : null}/>
</div>
)}
</div>
@@ -22,6 +22,7 @@ export type S3FormProps = {
export const StorageS3Form = (props: S3FormProps) => {
const form = useZodForm({
schema: S3FormSchema,
defaultValues: props.defaultValues,
});
const mutation = useMutation({
mutationFn: async (values: S3FormType) => {
@@ -99,9 +100,12 @@ export const StorageS3Form = (props: S3FormProps) => {
</FormItem>
)}
/>
<Button>
Save
</Button>
<div className="flex justify-end gap-4">
<Button>
Save
</Button>
</div>
</Form>
</CardContent>
</Card>
@@ -37,7 +37,7 @@ export const SettingsTabs = (props: SettingsTabsProps) => {
<DataTableWithPagination columns={usersColumns} data={props.users}/>
</TabsContent>
<TabsContent value="email">
<SettingsEmailTab/>
<SettingsEmailTab settings={props.settings}/>
</TabsContent>
<TabsContent value="storage">
<SettingsStorageTab settings={props.settings}/>
@@ -15,11 +15,11 @@ export const SidebarMenuCustom = (props: SidebarMenuCustomProps) => {
const pathname = usePathname();
// Menu items.
const items = [
{
title: "Home",
url: "/",
icon: Home,
},
// {
// title: "Home",
// url: "/",
// icon: Home,
// },
{
title: "Agents",
url: "agents",
+15 -5
View File
@@ -9,21 +9,26 @@ export const env = createEnv({
server: {
NODE_ENV: z.enum(['development', 'production']),
DATABASE_URL: z.string().url(),
SMTP_PASSWORD: z.string(),
SMTP_FROM: z.string(),
SMTP_HOST: z.string(),
SMTP_PORT: z.string(),
SMTP_USER: z.string(),
NEXT_PUBLIC_SECRET: z.string(),
NEXTAUTH_URL: z.string(),
SMTP_PASSWORD: z.string().optional(),
SMTP_FROM: z.string().optional(),
SMTP_HOST: z.string().optional(),
SMTP_PORT: z.string().optional(),
SMTP_USER: z.string().optional(),
AUTH_GOOGLE_ID: z.string().optional(),
AUTH_GOOGLE_SECRET: z.string().optional(),
S3_ENDPOINT: z.string().optional(),
S3_ACCESS_KEY: z.string().optional(),
S3_SECRET_KEY: z.string().optional(),
S3_BUCKET_NAME: z.string().optional(),
S3_PORT: z.string().optional(),
S3_USE_SSL: z.string().optional(),
STORAGE_TYPE: z.string().optional(),
},
/*
* Environment variables available on the client (and server).
@@ -45,18 +50,23 @@ export const env = createEnv({
NODE_ENV: process.env.NODE_ENV,
NEXT_PUBLIC_DOMAIN_NAME: process.env.NEXT_PUBLIC_DOMAIN_NAME,
DATABASE_URL: process.env.DATABASE_URL,
SMTP_PASSWORD: process.env.SMTP_PASSWORD,
SMTP_FROM: process.env.SMTP_FROM,
SMTP_HOST: process.env.SMTP_HOST,
SMTP_PORT: process.env.SMTP_PORT,
SMTP_USER: process.env.SMTP_USER,
AUTH_GOOGLE_ID: process.env.AUTH_GOOGLE_ID,
AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET,
S3_ENDPOINT: process.env.S3_ENDPOINT,
S3_ACCESS_KEY: process.env.S3_ACCESS_KEY,
S3_SECRET_KEY: process.env.S3_SECRET_KEY,
S3_BUCKET_NAME: process.env.S3_BUCKET_NAME,
S3_PORT: process.env.S3_PORT,
S3_USE_SSL: process.env.S3_USE_SSL,
STORAGE_TYPE: process.env.STORAGE_TYPE,
},
});
+50
View File
@@ -0,0 +1,50 @@
"use server"
import {prisma} from "@/prisma";
import nodemailer from "nodemailer";
type Payload = {
to: string;
subject: string;
html: any
};
export const sendEmail = async (data: Payload) => {
const settings = await prisma.settings.findUnique({
where: {
name: "system"
}
})
const smtpSettings = {
host: settings.smtpHost,
port: parseInt(settings.smtpPort),
auth: {
user: settings.smtpUser,
pass: settings.smtpPassword,
},
};
// Create a transporter object using nodemailer
const transporter = nodemailer.createTransport({
...smtpSettings
});
// Render the React component to HTML
// const emailHtml = await render(HelloEmail(), {});
// Set up email options
const mailOptions = {
...data
};
return await transporter.sendMail({
from: settings.smtpFrom,
...mailOptions,
});
};
+8
View File
@@ -0,0 +1,8 @@
import {env} from "@/env.mjs";
export const getServerUrl = () => {
if (typeof window !== 'undefined') {
return window.location.origin;
}
return `https://${env.NEXT_PUBLIC_DOMAIN_NAME}`;
}
+29 -1
View File
@@ -1,4 +1,6 @@
import {prisma} from "@/prisma";
import {Settings} from "@prisma/client";
import {env} from "@/env.mjs";
export function init() {
@@ -15,19 +17,45 @@ export function init() {
async function createSettingsIfNotExist() {
const configSettings = {
name: "system",
smtpPassword: env.SMTP_PASSWORD ?? null,
smtpFrom: env.SMTP_FROM ?? null,
smtpHost: env.SMTP_HOST ?? null,
smtpPort: env.SMTP_PORT ?? null,
smtpUser: env.SMTP_USER ?? null,
s3EndPointUrl: env.S3_ENDPOINT ?? null,
s3AccessKeyId: env.S3_ACCESS_KEY ?? null,
s3SecretAccessKey: env.S3_SECRET_KEY ?? null,
S3BucketName: env.S3_BUCKET_NAME ?? null,
}
const settings = await prisma.settings.findUnique({
where: {
name: "system",
}
})
if(!settings){
console.log("====Init Setting====")
console.log("====Init Setting : Create ====")
await prisma.settings.create({
data: {
name: "system",
}
})
}else{
console.log("====Init Setting : Update ====")
await prisma.settings.update({
where: {
name: "system",
},
data: {
...configSettings,
}
})
}
}
function consoleAscii(){
+1229 -19
View File
File diff suppressed because it is too large Load Diff