feature(auth): replace clerk.com with custom auth (#103)
* feature(auth): replace clerk.com with custom auth * minor fixes * remove notification preferences * decrease live events interval fix(api): cookies.. # Conflicts: # .gitignore # apps/api/src/index.ts # apps/dashboard/src/app/providers.tsx # packages/trpc/src/trpc.ts
This commit is contained in:
committed by
Carl-Gerhard Lindesvärd
parent
f28802b1c2
commit
d31d9924a5
88
packages/email/src/components/footer.tsx
Normal file
88
packages/email/src/components/footer.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
Column,
|
||||
Hr,
|
||||
Img,
|
||||
Link,
|
||||
Row,
|
||||
Section,
|
||||
Text,
|
||||
} from '@react-email/components';
|
||||
import React from 'react';
|
||||
|
||||
const baseUrl = 'https://openpanel.dev';
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<>
|
||||
<Hr />
|
||||
<Section className="w-full p-6">
|
||||
<Text className="text-[21px] font-regular" style={{ margin: 0 }}>
|
||||
An open-source alternative to Mixpanel
|
||||
</Text>
|
||||
|
||||
<br />
|
||||
|
||||
<Row>
|
||||
<Column className="align-middle w-[40px]">
|
||||
<Link href="https://git.new/openpanel">
|
||||
<Img
|
||||
src={`${baseUrl}/icons/github.png`}
|
||||
width="22"
|
||||
height="22"
|
||||
alt="OpenPanel on Github"
|
||||
/>
|
||||
</Link>
|
||||
</Column>
|
||||
<Column className="align-middle w-[40px]">
|
||||
<Link href="https://x.com/openpaneldev">
|
||||
<Img
|
||||
src={`${baseUrl}/icons/x.png`}
|
||||
width="22"
|
||||
height="22"
|
||||
alt="OpenPanel on X"
|
||||
/>
|
||||
</Link>
|
||||
</Column>
|
||||
|
||||
<Column className="align-middle">
|
||||
<Link href="https://go.openpanel.dev/discord">
|
||||
<Img
|
||||
src={`${baseUrl}/icons/discord.png`}
|
||||
width="22"
|
||||
height="22"
|
||||
alt="OpenPanel on Discord"
|
||||
/>
|
||||
</Link>
|
||||
</Column>
|
||||
|
||||
<Column className="align-middle">
|
||||
<Link href="mailto:hello@openpanel.dev">
|
||||
<Img
|
||||
src={`${baseUrl}/icons/email.png`}
|
||||
width="22"
|
||||
height="22"
|
||||
alt="Contact OpenPanel with email"
|
||||
/>
|
||||
</Link>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Text className="text-[#B8B8B8] text-xs">
|
||||
OpenPanel AB - Sankt Eriksgatan 100, 113 31, Stockholm, Sweden.
|
||||
</Text>
|
||||
</Row>
|
||||
|
||||
{/* <Row>
|
||||
<Link
|
||||
className="text-[#707070] text-[14px]"
|
||||
href="https://dashboard.openpanel.dev/settings/notifications"
|
||||
title="Unsubscribe"
|
||||
>
|
||||
Notification preferences
|
||||
</Link>
|
||||
</Row> */}
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
66
packages/email/src/components/layout.tsx
Normal file
66
packages/email/src/components/layout.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
Body,
|
||||
Container,
|
||||
Font,
|
||||
Html,
|
||||
Img,
|
||||
Section,
|
||||
Tailwind,
|
||||
} from '@react-email/components';
|
||||
// biome-ignore lint/style/useImportType: resend needs React
|
||||
import React from 'react';
|
||||
import { Footer } from './footer';
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function Layout({ children }: Props) {
|
||||
return (
|
||||
<Html>
|
||||
<Tailwind>
|
||||
<head>
|
||||
<Font
|
||||
fontFamily="Geist"
|
||||
fallbackFontFamily="Helvetica"
|
||||
webFont={{
|
||||
url: 'https://cdn.jsdelivr.net/npm/@fontsource/geist-sans@5.0.1/files/geist-sans-latin-400-normal.woff2',
|
||||
format: 'woff2',
|
||||
}}
|
||||
fontWeight={400}
|
||||
fontStyle="normal"
|
||||
/>
|
||||
|
||||
<Font
|
||||
fontFamily="Geist"
|
||||
fallbackFontFamily="Helvetica"
|
||||
webFont={{
|
||||
url: 'https://cdn.jsdelivr.net/npm/@fontsource/geist-sans@5.0.1/files/geist-sans-latin-500-normal.woff2',
|
||||
format: 'woff2',
|
||||
}}
|
||||
fontWeight={500}
|
||||
fontStyle="normal"
|
||||
/>
|
||||
</head>
|
||||
<Body className="bg-[#fff] my-auto mx-auto font-sans">
|
||||
<Container
|
||||
className="border-transparent md:border-[#E8E7E1] my-[40px] mx-auto max-w-[600px]"
|
||||
style={{ borderStyle: 'solid', borderWidth: 1 }}
|
||||
>
|
||||
<Section className="p-6">
|
||||
<Img
|
||||
src={'https://openpanel.dev/logo.png'}
|
||||
width="80"
|
||||
height="80"
|
||||
alt="OpenPanel Logo"
|
||||
style={{ borderRadius: 4 }}
|
||||
/>
|
||||
</Section>
|
||||
<Section className="p-6">{children}</Section>
|
||||
<Footer />
|
||||
</Container>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
45
packages/email/src/emails/email-invite.tsx
Normal file
45
packages/email/src/emails/email-invite.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import { Button, Link, Text } from '@react-email/components';
|
||||
import { z } from 'zod';
|
||||
import { Layout } from '../components/layout';
|
||||
|
||||
export const zEmailInvite = z.object({
|
||||
url: z.string(),
|
||||
organizationName: z.string(),
|
||||
});
|
||||
|
||||
export type Props = z.infer<typeof zEmailInvite>;
|
||||
export default EmailInvite;
|
||||
export function EmailInvite({
|
||||
organizationName = 'Acme Co',
|
||||
url = 'https://openpanel.dev',
|
||||
}: Props) {
|
||||
return (
|
||||
<Layout>
|
||||
<Text>You've been invited to join {organizationName}!</Text>
|
||||
<Text>
|
||||
If you don't have an account yet, click the button below to create one
|
||||
and join the organization:
|
||||
</Text>
|
||||
<Button
|
||||
href={url}
|
||||
style={{
|
||||
backgroundColor: '#000',
|
||||
borderRadius: '6px',
|
||||
color: '#fff',
|
||||
padding: '12px 20px',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
Join {organizationName}
|
||||
</Button>
|
||||
<Text>
|
||||
Join link: <Link href={url}>{url}</Link>
|
||||
</Text>
|
||||
<Text style={{ color: '#666' }}>
|
||||
Already have an account? No need to do anything - you'll have access
|
||||
automatically when you sign in.
|
||||
</Text>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
40
packages/email/src/emails/email-reset-password.tsx
Normal file
40
packages/email/src/emails/email-reset-password.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { Button, Link, Text } from '@react-email/components';
|
||||
import { z } from 'zod';
|
||||
import { Layout } from '../components/layout';
|
||||
|
||||
export const zEmailResetPassword = z.object({
|
||||
url: z.string(),
|
||||
});
|
||||
|
||||
export type Props = z.infer<typeof zEmailResetPassword>;
|
||||
export default EmailResetPassword;
|
||||
export function EmailResetPassword({ url = 'https://openpanel.dev' }: Props) {
|
||||
return (
|
||||
<Layout>
|
||||
<Text>
|
||||
You have requested to reset your password. Follow the link below to
|
||||
reset your password:
|
||||
</Text>
|
||||
<Button
|
||||
href={url}
|
||||
style={{
|
||||
backgroundColor: '#000',
|
||||
borderRadius: '6px',
|
||||
color: '#fff',
|
||||
padding: '12px 20px',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
Reset password
|
||||
</Button>
|
||||
<Text>
|
||||
Reset password link: <Link href={url}>{url}</Link>
|
||||
</Text>
|
||||
<Text style={{ color: '#666' }}>
|
||||
Have you not requested this? Please ignore this email and contact
|
||||
support if you believe this was a mistake.
|
||||
</Text>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
23
packages/email/src/emails/index.tsx
Normal file
23
packages/email/src/emails/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { z } from 'zod';
|
||||
import { EmailInvite, zEmailInvite } from './email-invite';
|
||||
import EmailResetPassword, {
|
||||
zEmailResetPassword,
|
||||
} from './email-reset-password';
|
||||
|
||||
export const templates = {
|
||||
invite: {
|
||||
subject: (data: z.infer<typeof zEmailInvite>) =>
|
||||
`Invite to join ${data.organizationName}`,
|
||||
Component: EmailInvite,
|
||||
schema: zEmailInvite,
|
||||
},
|
||||
'reset-password': {
|
||||
subject: (data: z.infer<typeof zEmailResetPassword>) =>
|
||||
'Reset your password',
|
||||
Component: EmailResetPassword,
|
||||
schema: zEmailResetPassword,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type Templates = typeof templates;
|
||||
export type TemplateKey = keyof Templates;
|
||||
49
packages/email/src/index.tsx
Normal file
49
packages/email/src/index.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { Resend } from 'resend';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { type TemplateKey, type Templates, templates } from './emails';
|
||||
|
||||
const FROM = 'hello@openpanel.dev';
|
||||
|
||||
export async function sendEmail<T extends TemplateKey>(
|
||||
template: T,
|
||||
options: {
|
||||
to: string | string[];
|
||||
data: z.infer<Templates[T]['schema']>;
|
||||
},
|
||||
) {
|
||||
if (!process.env.RESEND_API_KEY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
const { to, data } = options;
|
||||
const { subject, Component, schema } = templates[template];
|
||||
const props = schema.safeParse(data);
|
||||
|
||||
if (props.error) {
|
||||
console.error('Failed to parse data', props.error);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await resend.emails.send({
|
||||
from: FROM,
|
||||
to,
|
||||
// @ts-expect-error - TODO: fix this
|
||||
subject: subject(props.data),
|
||||
// @ts-expect-error - TODO: fix this
|
||||
react: <Component {...props.data} />,
|
||||
});
|
||||
|
||||
if (res.error) {
|
||||
throw new Error(res.error.message);
|
||||
}
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.error('Failed to send email', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user