mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Merge remote-tracking branch 'origin/main'
# Conflicts: # package.json
This commit is contained in:
@@ -14,13 +14,11 @@ export default async function Layout({children}: { children: React.ReactNode })
|
|||||||
email: user?.email
|
email: user?.email
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if(userInfo && userInfo.authMethod){
|
||||||
if(userInfo){
|
|
||||||
redirect('/dashboard')
|
redirect('/dashboard')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LayoutAdmin>
|
<LayoutAdmin>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
|
import {redirect} from "next/navigation";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-1 flex-col gap-4 p-4">
|
// return (
|
||||||
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
|
// <div className="flex flex-1 flex-col gap-4 p-4">
|
||||||
<div className="aspect-video rounded-xl bg-muted/50"/>
|
// <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 className="aspect-video rounded-xl bg-muted/50"/>
|
||||||
</div>
|
// <div className="aspect-video rounded-xl bg-muted/50"/>
|
||||||
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min"/>
|
// </div>
|
||||||
</div>
|
// <div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min"/>
|
||||||
)
|
// </div>
|
||||||
|
// )
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
|
import {redirect} from "next/navigation";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
redirect("/dashboard/agents")
|
||||||
|
|
||||||
return(
|
// return(
|
||||||
<div>
|
// <div>
|
||||||
<h1>Ok</h1>
|
// <h1>Ok</h1>
|
||||||
<Button>Button</Button>
|
// <Button>Button</Button>
|
||||||
</div>
|
// </div>
|
||||||
)
|
// )
|
||||||
}
|
}
|
||||||
@@ -2,15 +2,12 @@ import {PageParams} from "@/types/next";
|
|||||||
import {Page, PageContent, PageDescription, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageContent, PageDescription, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
import {prisma} from "@/prisma";
|
import {prisma} from "@/prisma";
|
||||||
import {requiredCurrentUser} from "@/auth/current-user";
|
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";
|
import {SettingsTabs} from "@/components/wrappers/Dashboard/Settings/SettingsTabs/SettingsTabs";
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
const user = await requiredCurrentUser()
|
const user = await requiredCurrentUser()
|
||||||
|
|
||||||
|
|
||||||
const users = await prisma.user.findMany({
|
const users = await prisma.user.findMany({
|
||||||
where:{
|
where:{
|
||||||
id: {
|
id: {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
- /app/node_modules
|
- /app/node_modules
|
||||||
|
container_name: portabase-app
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
|
|||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ function checkRouteExists(pathname) {
|
|||||||
// /^\/api\/auth\/\w+$/, // Dynamic route with a number as a parameter (e.g., /api/dynamic/123)
|
// /^\/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\/healthcheck\/\w+$/, // Dynamic route with an alphanumeric parameter (e.g., /api/user/username)
|
||||||
/^\/api\/agent\/[^/]+\/status\/?$/, // Dynamic route for /api/agent/[id]/status
|
/^\/api\/agent\/[^/]+\/status\/?$/, // Dynamic route for /api/agent/[id]/status
|
||||||
|
/^\/api\/logs\$/,
|
||||||
];
|
];
|
||||||
return routePatterns.some(pattern => pattern.test(pathname));
|
return routePatterns.some(pattern => pattern.test(pathname));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.0",
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.3",
|
"@radix-ui/react-tooltip": "^1.1.3",
|
||||||
|
"@react-email/components": "^0.0.28",
|
||||||
"@t3-oss/env-nextjs": "^0.11.1",
|
"@t3-oss/env-nextjs": "^0.11.1",
|
||||||
"@tanstack/react-query": "^5.59.18",
|
"@tanstack/react-query": "^5.59.18",
|
||||||
"@tanstack/react-table": "^8.20.5",
|
"@tanstack/react-table": "^8.20.5",
|
||||||
@@ -49,6 +50,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"dockerode": "^4.0.2",
|
||||||
"embla-carousel-react": "^8.3.1",
|
"embla-carousel-react": "^8.3.1",
|
||||||
"input-otp": "^1.4.0",
|
"input-otp": "^1.4.0",
|
||||||
"lucide-react": "^0.454.0",
|
"lucide-react": "^0.454.0",
|
||||||
@@ -58,9 +60,11 @@
|
|||||||
"next-intl": "^3.24.0",
|
"next-intl": "^3.24.0",
|
||||||
"next-safe-action": "^7.4.3",
|
"next-safe-action": "^7.4.3",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
|
"nodemailer": "^6.9.16",
|
||||||
"react": "19.0.0-rc-66855b96-20241106",
|
"react": "19.0.0-rc-66855b96-20241106",
|
||||||
"react-day-picker": "8.10.1",
|
"react-day-picker": "8.10.1",
|
||||||
"react-dom": "19.0.0-rc-66855b96-20241106",
|
"react-dom": "19.0.0-rc-66855b96-20241106",
|
||||||
|
"react-email": "^3.0.2",
|
||||||
"react-hook-form": "^7.53.1",
|
"react-hook-form": "^7.53.1",
|
||||||
"react-resizable-panels": "^2.1.6",
|
"react-resizable-panels": "^2.1.6",
|
||||||
"react-twc": "^1.4.2",
|
"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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+101
-4
@@ -14,26 +14,50 @@ import {
|
|||||||
EmailFormSchema,
|
EmailFormSchema,
|
||||||
EmailFormType
|
EmailFormType
|
||||||
} from "@/components/wrappers/Dashboard/Settings/SettingsEmailTab/EmailForm/email-form.schema";
|
} 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 = {
|
export type EmailFormProps = {
|
||||||
defaultValues?: EmailFormType;
|
defaultValues?: EmailFormType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EmailForm = (props: EmailFormProps) => {
|
export const EmailForm = (props: EmailFormProps) => {
|
||||||
|
|
||||||
|
const isCreate = !Boolean(props.defaultValues)
|
||||||
|
|
||||||
const form = useZodForm({
|
const form = useZodForm({
|
||||||
schema: EmailFormSchema,
|
schema: EmailFormSchema,
|
||||||
|
defaultValues: props.defaultValues,
|
||||||
});
|
});
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: async (values: EmailFormType) => {
|
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 (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|
||||||
<Form form={form}
|
<Form form={form}
|
||||||
|
|
||||||
className="flex flex-col gap-4 mt-3"
|
className="flex flex-col gap-4 mt-3"
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
await mutation.mutateAsync(values);
|
await mutation.mutateAsync(values);
|
||||||
@@ -42,6 +66,8 @@ export const EmailForm = (props: EmailFormProps) => {
|
|||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="smtpFrom"
|
name="smtpFrom"
|
||||||
|
defaultValue=""
|
||||||
|
|
||||||
render={({field}) => (
|
render={({field}) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>From Email *</FormLabel>
|
<FormLabel>From Email *</FormLabel>
|
||||||
@@ -54,10 +80,81 @@ export const EmailForm = (props: EmailFormProps) => {
|
|||||||
</FormItem>
|
</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>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
+31
@@ -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 {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) => {
|
export type SettingsEmailTabProps = {
|
||||||
return(
|
settings: Settings
|
||||||
<div>
|
}
|
||||||
<EmailForm/>
|
|
||||||
|
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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+1
-1
@@ -40,7 +40,7 @@ export const SettingsStorageTab = (props: SettingsStorageTabProps) => {
|
|||||||
</div>
|
</div>
|
||||||
{isSwitched && (
|
{isSwitched && (
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<StorageS3Form/>
|
<StorageS3Form defaultValues={props.settings.s3EndPointUrl ? props.settings : null}/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+7
-3
@@ -22,6 +22,7 @@ export type S3FormProps = {
|
|||||||
export const StorageS3Form = (props: S3FormProps) => {
|
export const StorageS3Form = (props: S3FormProps) => {
|
||||||
const form = useZodForm({
|
const form = useZodForm({
|
||||||
schema: S3FormSchema,
|
schema: S3FormSchema,
|
||||||
|
defaultValues: props.defaultValues,
|
||||||
});
|
});
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: async (values: S3FormType) => {
|
mutationFn: async (values: S3FormType) => {
|
||||||
@@ -99,9 +100,12 @@ export const StorageS3Form = (props: S3FormProps) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Button>
|
<div className="flex justify-end gap-4">
|
||||||
Save
|
|
||||||
</Button>
|
<Button>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const SettingsTabs = (props: SettingsTabsProps) => {
|
|||||||
<DataTableWithPagination columns={usersColumns} data={props.users}/>
|
<DataTableWithPagination columns={usersColumns} data={props.users}/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="email">
|
<TabsContent value="email">
|
||||||
<SettingsEmailTab/>
|
<SettingsEmailTab settings={props.settings}/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="storage">
|
<TabsContent value="storage">
|
||||||
<SettingsStorageTab settings={props.settings}/>
|
<SettingsStorageTab settings={props.settings}/>
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ export const SidebarMenuCustom = (props: SidebarMenuCustomProps) => {
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
// Menu items.
|
// Menu items.
|
||||||
const items = [
|
const items = [
|
||||||
{
|
// {
|
||||||
title: "Home",
|
// title: "Home",
|
||||||
url: "/",
|
// url: "/",
|
||||||
icon: Home,
|
// icon: Home,
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
title: "Agents",
|
title: "Agents",
|
||||||
url: "agents",
|
url: "agents",
|
||||||
|
|||||||
+15
-5
@@ -9,21 +9,26 @@ export const env = createEnv({
|
|||||||
server: {
|
server: {
|
||||||
NODE_ENV: z.enum(['development', 'production']),
|
NODE_ENV: z.enum(['development', 'production']),
|
||||||
DATABASE_URL: z.string().url(),
|
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(),
|
NEXT_PUBLIC_SECRET: z.string(),
|
||||||
NEXTAUTH_URL: 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_ID: z.string().optional(),
|
||||||
AUTH_GOOGLE_SECRET: z.string().optional(),
|
AUTH_GOOGLE_SECRET: z.string().optional(),
|
||||||
|
|
||||||
S3_ENDPOINT: z.string().optional(),
|
S3_ENDPOINT: z.string().optional(),
|
||||||
S3_ACCESS_KEY: z.string().optional(),
|
S3_ACCESS_KEY: z.string().optional(),
|
||||||
S3_SECRET_KEY: z.string().optional(),
|
S3_SECRET_KEY: z.string().optional(),
|
||||||
S3_BUCKET_NAME: z.string().optional(),
|
S3_BUCKET_NAME: z.string().optional(),
|
||||||
S3_PORT: z.string().optional(),
|
S3_PORT: z.string().optional(),
|
||||||
S3_USE_SSL: z.string().optional(),
|
S3_USE_SSL: z.string().optional(),
|
||||||
|
|
||||||
|
STORAGE_TYPE: z.string().optional(),
|
||||||
},
|
},
|
||||||
/*
|
/*
|
||||||
* Environment variables available on the client (and server).
|
* Environment variables available on the client (and server).
|
||||||
@@ -45,18 +50,23 @@ export const env = createEnv({
|
|||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
NEXT_PUBLIC_DOMAIN_NAME: process.env.NEXT_PUBLIC_DOMAIN_NAME,
|
NEXT_PUBLIC_DOMAIN_NAME: process.env.NEXT_PUBLIC_DOMAIN_NAME,
|
||||||
DATABASE_URL: process.env.DATABASE_URL,
|
DATABASE_URL: process.env.DATABASE_URL,
|
||||||
|
|
||||||
SMTP_PASSWORD: process.env.SMTP_PASSWORD,
|
SMTP_PASSWORD: process.env.SMTP_PASSWORD,
|
||||||
SMTP_FROM: process.env.SMTP_FROM,
|
SMTP_FROM: process.env.SMTP_FROM,
|
||||||
SMTP_HOST: process.env.SMTP_HOST,
|
SMTP_HOST: process.env.SMTP_HOST,
|
||||||
SMTP_PORT: process.env.SMTP_PORT,
|
SMTP_PORT: process.env.SMTP_PORT,
|
||||||
SMTP_USER: process.env.SMTP_USER,
|
SMTP_USER: process.env.SMTP_USER,
|
||||||
|
|
||||||
AUTH_GOOGLE_ID: process.env.AUTH_GOOGLE_ID,
|
AUTH_GOOGLE_ID: process.env.AUTH_GOOGLE_ID,
|
||||||
AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET,
|
AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET,
|
||||||
|
|
||||||
S3_ENDPOINT: process.env.S3_ENDPOINT,
|
S3_ENDPOINT: process.env.S3_ENDPOINT,
|
||||||
S3_ACCESS_KEY: process.env.S3_ACCESS_KEY,
|
S3_ACCESS_KEY: process.env.S3_ACCESS_KEY,
|
||||||
S3_SECRET_KEY: process.env.S3_SECRET_KEY,
|
S3_SECRET_KEY: process.env.S3_SECRET_KEY,
|
||||||
S3_BUCKET_NAME: process.env.S3_BUCKET_NAME,
|
S3_BUCKET_NAME: process.env.S3_BUCKET_NAME,
|
||||||
S3_PORT: process.env.S3_PORT,
|
S3_PORT: process.env.S3_PORT,
|
||||||
S3_USE_SSL: process.env.S3_USE_SSL,
|
S3_USE_SSL: process.env.S3_USE_SSL,
|
||||||
|
|
||||||
|
STORAGE_TYPE: process.env.STORAGE_TYPE,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
@@ -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
@@ -1,4 +1,6 @@
|
|||||||
import {prisma} from "@/prisma";
|
import {prisma} from "@/prisma";
|
||||||
|
import {Settings} from "@prisma/client";
|
||||||
|
import {env} from "@/env.mjs";
|
||||||
|
|
||||||
|
|
||||||
export function init() {
|
export function init() {
|
||||||
@@ -15,19 +17,45 @@ export function init() {
|
|||||||
|
|
||||||
async function createSettingsIfNotExist() {
|
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({
|
const settings = await prisma.settings.findUnique({
|
||||||
where: {
|
where: {
|
||||||
name: "system",
|
name: "system",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if(!settings){
|
if(!settings){
|
||||||
console.log("====Init Setting====")
|
console.log("====Init Setting : Create ====")
|
||||||
await prisma.settings.create({
|
await prisma.settings.create({
|
||||||
data: {
|
data: {
|
||||||
name: "system",
|
name: "system",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}else{
|
||||||
|
console.log("====Init Setting : Update ====")
|
||||||
|
await prisma.settings.update({
|
||||||
|
where: {
|
||||||
|
name: "system",
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
...configSettings,
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function consoleAscii(){
|
function consoleAscii(){
|
||||||
|
|||||||
Reference in New Issue
Block a user