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:
Carl-Gerhard Lindesvärd
2024-12-18 21:30:39 +01:00
committed by Carl-Gerhard Lindesvärd
parent f28802b1c2
commit d31d9924a5
151 changed files with 18484 additions and 12853 deletions

View 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>
</>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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;

View 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;
}
}