diff --git a/.env.example b/.env.example index 3b9f12d..509bb84 100644 --- a/.env.example +++ b/.env.example @@ -5,16 +5,17 @@ REDIS_URL="redis://localhost:6379" NEXTAUTH_URL="http://localhost:3000" -GITHUB_ID="" -GITHUB_SECRET="" AWS_DEFAULT_REGION="us-east-1" -AWS_SECRET_KEY="" -AWS_ACCESS_KEY="" +AWS_SECRET_KEY="some-secret-key" +AWS_ACCESS_KEY="some-access-key" +AWS_SES_ENDPOINT="http://localhost:3003/api/ses" +AWS_SNS_ENDPOINT="http://localhost:3003/api/sns" NEXTAUTH_SECRET="" +FROM_EMAIL="hello@unsend.dev" API_RATE_LIMIT=2 -NEXT_PUBLIC_IS_CLOUD=false +NEXT_PUBLIC_IS_CLOUD=true diff --git a/.env.selfhost.example b/.env.selfhost.example index 0cc2e98..0ee4dcb 100644 --- a/.env.selfhost.example +++ b/.env.selfhost.example @@ -12,10 +12,15 @@ DATABASE_URL="postgresql://postgres:postgres@postgres:5432/unsend" NEXTAUTH_URL="http://localhost:3000" NEXTAUTH_SECRET= +## Auth providers any one is required # Github login - required GITHUB_ID="" GITHUB_SECRET="" +# Google login - required +GOOGLE_CLIENT_ID="" +GOOGLE_CLIENT_SECRET="" + # AWS details - required AWS_DEFAULT_REGION="us-east-1" AWS_SECRET_KEY="" diff --git a/apps/docs/get-started/local.mdx b/apps/docs/get-started/local.mdx index 18487eb..520cb23 100644 --- a/apps/docs/get-started/local.mdx +++ b/apps/docs/get-started/local.mdx @@ -10,38 +10,38 @@ Unsend's codebase is fully [open-source on github](https://github.com/unsend-dev Here is the codebase structure - ``` +``` apps - ├── docs - ├── marketing - ├── web +├── docs +├── marketing +├── web packages - ├── eslint-config - ├── sdk - ├── tailwind-config - ├── typescript-config - ├── ui - ``` +├── eslint-config +├── sdk +├── tailwind-config +├── typescript-config +├── ui +``` -The ```apps``` directory contains the code for: +The `apps` directory contains the code for: -- ```web```: Code for our dashboard and email infra +- `web`: Code for our dashboard and email infra -- ```marketing```: The code for the landing page of Unsend +- `marketing`: The code for the landing page of Unsend -- ```docs```: The documentation that you are currently reading. +- `docs`: The documentation that you are currently reading. -The ```packages``` directory contains the code for: +The `packages` directory contains the code for: -- ```eslint-config``` package contains shared ESLint configuration settings +- `eslint-config` package contains shared ESLint configuration settings -- ```sdk``` package contains typescript sdk for unsend rest api +- `sdk` package contains typescript sdk for unsend rest api -- ```tailwind-config``` This package contains a shared Tailwind CSS configuration. +- `tailwind-config` This package contains a shared Tailwind CSS configuration. -- ```typescript-config``` This package contains a shared typescript configuration +- `typescript-config` This package contains a shared typescript configuration -- ```ui``` This package is a collection of reusable UI components like buttons, badges, etc +- `ui` This package is a collection of reusable UI components like buttons, badges, etc ## Running Unsend locally @@ -62,9 +62,10 @@ To run Unsend, locally you will need to setup the following: Once the repo is forked you can clone it on your local machine using: - ```bash - git clone https://github.com/your-username/unsend.git - ``` +```bash +git clone https://github.com/your-username/unsend.git +``` + @@ -95,42 +96,57 @@ pnpm install Use the following command to generate a key and add it under ```NEXTAUTH_SECRET``` - ```bash - openssl rand -base64 32 - ``` +```bash +openssl rand -base64 32 +``` + - + + + You don't need this setup if you have `FROM_EMAIL` set in your environment + variables. for development email link will logged in the console. + - Next, [create a new GitHub App](https://github.com/settings/applications/new). This will allow you to sign in to Unsend with your GitHub account.4 +Next, [create a new GitHub App](https://github.com/settings/applications/new). This will allow you to sign in to Unsend with your GitHub account.4 - Add the homepage as: - - ``` - http://localhost:3000/login - ``` +Add the homepage as: - and callback URL as: - ``` - http://localhost:3000/api/auth/callback/github - ``` +``` +http://localhost:3000/login +``` + +and callback URL as: + +``` +http://localhost:3000/api/auth/callback/github +``` + +Once the app is added you can add the Client ID under `GITHUB_ID`and CLIENT SECRET under `GITHUB_SECRET` - Once the app is added you can add the Client ID under ``GITHUB_ID``and CLIENT SECRET under ```GITHUB_SECRET``` - + + + + You don't need this setup if you are using the local-sen-sns image. But email + will not be sent out. + + +Next, we need to add in the [AWS credentials](https://docs.unsend.dev/get-started/create-aws-credentials). Follow the detailed guide to get the AWS credentials with accurate permissions and add them in: + +``` +AWS_ACCESS_KEY= +AWS_SECRET_KEY= +``` - Next, we need to add in the [AWS credentials](https://docs.unsend.dev/get-started/create-aws-credentials). Follow the detailed guide to get the AWS credentials with accurate permissions and add them in: - - ``` - AWS_ACCESS_KEY= - AWS_SECRET_KEY= - ``` ## Running Unsend locally + We are using a local Postgresql server and a local Redis server. But if you don't have docker you can also manually set these up. ### Option 1: Using Docker Recommended + ```bash @@ -147,6 +163,7 @@ Landing page will be started on ```bash http://localhost:3001 ``` + Run the following command to get the URL. Here is the more detailed guide by [cloudflare](https://developers.cloudflare.com/pages/how-to/preview-with-cloudflare-tunnel/) @@ -160,6 +177,7 @@ You can paste the URL provided by cloudflare in the Callback URL section ### Option 2: Using your own database or hosted database + ```bash @@ -188,7 +206,6 @@ You can paste the URL provided by cloudflare in the Callback URL section - ## Run documentation To run the documentation run the following command: diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma index 839bf1c..3b46bb1 100644 --- a/apps/web/prisma/schema.prisma +++ b/apps/web/prisma/schema.prisma @@ -289,16 +289,17 @@ model Campaign { } model Template { - id String @id @default(cuid()) - name String - teamId Int - subject String - html String? - content String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + name String + teamId Int + subject String + html String? + content String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + @@index([createdAt(sort: Desc)]) } diff --git a/apps/web/src/app/api/ses_callback/route.ts b/apps/web/src/app/api/ses_callback/route.ts index 7ea0623..f0b76c1 100644 --- a/apps/web/src/app/api/ses_callback/route.ts +++ b/apps/web/src/app/api/ses_callback/route.ts @@ -1,3 +1,4 @@ +import { env } from "~/env"; import { db } from "~/server/db"; import { parseSesHook, SesHookParser } from "~/server/service/ses-hook-parser"; import { SesSettingsService } from "~/server/service/ses-settings-service"; @@ -83,6 +84,10 @@ async function handleSubscription(message: any) { * A simple check to ensure that the event is from the correct topic */ async function checkEventValidity(message: SnsNotificationMessage) { + if (env.NODE_ENV === "development") { + return true; + } + const { TopicArn } = message; const configuredTopicArn = await SesSettingsService.getTopicArns(); diff --git a/apps/web/src/components/settings/AddSesSettings.tsx b/apps/web/src/components/settings/AddSesSettings.tsx index 7eb05b0..38d176f 100644 --- a/apps/web/src/components/settings/AddSesSettings.tsx +++ b/apps/web/src/components/settings/AddSesSettings.tsx @@ -17,6 +17,7 @@ import { Input } from "@unsend/ui/src/input"; import { Button } from "@unsend/ui/src/button"; import Spinner from "@unsend/ui/src/spinner"; import { toast } from "@unsend/ui/src/toaster"; +import { isLocalhost } from "~/utils/client"; const FormSchema = z.object({ region: z.string(), @@ -65,14 +66,16 @@ export const AddSesSettingsForm: React.FC = ({ }); function onSubmit(data: z.infer) { - if (!data.unsendUrl.startsWith("https://")) { + const localhost = isLocalhost(); + + if (!data.unsendUrl.startsWith("https://") && !localhost) { form.setError("unsendUrl", { message: "URL must start with https://", }); return; } - if (data.unsendUrl.includes("localhost")) { + if (data.unsendUrl.includes("localhost") && !localhost) { form.setError("unsendUrl", { message: "URL must be a valid url", }); diff --git a/apps/web/src/env.js b/apps/web/src/env.js index 4d0abda..5c9f475 100644 --- a/apps/web/src/env.js +++ b/apps/web/src/env.js @@ -29,14 +29,15 @@ export const env = createEnv({ // VERCEL_URL doesn't include `https` so it cant be validated as a URL process.env.VERCEL ? z.string() : z.string().url() ), - GITHUB_ID: z.string(), - GITHUB_SECRET: z.string(), + GITHUB_ID: z.string().optional(), + GITHUB_SECRET: z.string().optional(), AWS_ACCESS_KEY: z.string(), AWS_SECRET_KEY: z.string(), UNSEND_API_KEY: z.string().optional(), GOOGLE_CLIENT_ID: z.string().optional(), GOOGLE_CLIENT_SECRET: z.string().optional(), AWS_SES_ENDPOINT: z.string().optional(), + AWS_SNS_ENDPOINT: z.string().optional(), AWS_DEFAULT_REGION: z.string().default("us-east-1"), API_RATE_LIMIT: z .string() @@ -83,6 +84,7 @@ export const env = createEnv({ GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, AWS_DEFAULT_REGION: process.env.AWS_DEFAULT_REGION, AWS_SES_ENDPOINT: process.env.AWS_SES_ENDPOINT, + AWS_SNS_ENDPOINT: process.env.AWS_SNS_ENDPOINT, API_RATE_LIMIT: process.env.API_RATE_LIMIT, NEXT_PUBLIC_IS_CLOUD: process.env.NEXT_PUBLIC_IS_CLOUD, ADMIN_EMAIL: process.env.ADMIN_EMAIL, diff --git a/apps/web/src/server/auth.ts b/apps/web/src/server/auth.ts index 46a9459..b23ec74 100644 --- a/apps/web/src/server/auth.ts +++ b/apps/web/src/server/auth.ts @@ -44,13 +44,17 @@ declare module "next-auth" { * Auth providers */ -const providers: Provider[] = [ - GitHubProvider({ - clientId: env.GITHUB_ID, - clientSecret: env.GITHUB_SECRET, - allowDangerousEmailAccountLinking: true, - }), -]; +const providers: Provider[] = []; + +if (env.GITHUB_ID && env.GITHUB_SECRET) { + providers.push( + GitHubProvider({ + clientId: env.GITHUB_ID, + clientSecret: env.GITHUB_SECRET, + allowDangerousEmailAccountLinking: true, + }) + ); +} if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) { providers.push( @@ -76,6 +80,10 @@ if (env.FROM_EMAIL) { ); } +if (providers.length === 0) { + throw new Error("No auth providers found, need atleast one"); +} + /** * Options for NextAuth.js used to configure adapters, providers, callbacks, etc. * @@ -100,7 +108,7 @@ export const authOptions: NextAuthOptions = { events: { createUser: async ({ user }) => { // No waitlist for self hosting - if (!env.NEXT_PUBLIC_IS_CLOUD) { + if (!env.NEXT_PUBLIC_IS_CLOUD || env.NODE_ENV === "development") { await db.user.update({ where: { id: user.id }, data: { isBetaUser: true }, diff --git a/apps/web/src/server/aws/sns.ts b/apps/web/src/server/aws/sns.ts index b02fd49..0b99b13 100644 --- a/apps/web/src/server/aws/sns.ts +++ b/apps/web/src/server/aws/sns.ts @@ -8,6 +8,7 @@ import { env } from "~/env"; function getSnsClient(region: string) { return new SNSClient({ + endpoint: env.AWS_SNS_ENDPOINT, region: region, credentials: { accessKeyId: env.AWS_ACCESS_KEY, @@ -44,6 +45,5 @@ export async function subscribeEndpoint( const client = getSnsClient(region); const data = await client.send(subscribeCommand); - console.log(data.SubscriptionArn); return data.SubscriptionArn; } diff --git a/apps/web/src/server/mailer.ts b/apps/web/src/server/mailer.ts index d991ce7..bc639e7 100644 --- a/apps/web/src/server/mailer.ts +++ b/apps/web/src/server/mailer.ts @@ -18,7 +18,7 @@ export async function sendSignUpEmail( const { host } = new URL(url); if (env.NODE_ENV === "development") { - console.log("Sending sign in email", email, url, token); + console.log("Sending sign in email", { email, url, token }); return; } diff --git a/apps/web/src/server/service/ses-settings-service.ts b/apps/web/src/server/service/ses-settings-service.ts index 8a75a68..d60a555 100644 --- a/apps/web/src/server/service/ses-settings-service.ts +++ b/apps/web/src/server/service/ses-settings-service.ts @@ -131,9 +131,10 @@ export class SesSettingsService { try { await sns.deleteTopic(topicArn, region); } catch (deleteError) { - console.error('Failed to delete SNS topic after error:', deleteError); + console.error("Failed to delete SNS topic after error:", deleteError); } } + console.error("Failed to create SES setting", error); throw error; } } diff --git a/apps/web/src/utils/client.ts b/apps/web/src/utils/client.ts new file mode 100644 index 0000000..bd563ad --- /dev/null +++ b/apps/web/src/utils/client.ts @@ -0,0 +1,3 @@ +export const isLocalhost = () => { + return location.hostname === "localhost"; +}; diff --git a/docker/dev/compose.yml b/docker/dev/compose.yml index 241cb21..5d729d8 100644 --- a/docker/dev/compose.yml +++ b/docker/dev/compose.yml @@ -24,6 +24,15 @@ services: - redis:/data command: ["redis-server", "--maxmemory-policy", "noeviction"] + local-sen-sns: + image: unsend/local-ses-sns:latest + container_name: local-ses-sns + restart: always + ports: + - "5350:3000" + environment: + WEBHOOK_URL: http://localhost:3000/api/ses_callback + minio: image: minio/minio container_name: unsend-storage-dev