make dev setup better (#116)

* make dev setup better

* chage docs

* remove the need of github login
This commit is contained in:
KM Koushik
2025-03-17 21:33:43 +11:00
committed by GitHub
parent 8b9d81ab2a
commit 0a1d93ac60
13 changed files with 129 additions and 74 deletions

View File

@@ -5,16 +5,17 @@ REDIS_URL="redis://localhost:6379"
NEXTAUTH_URL="http://localhost:3000" NEXTAUTH_URL="http://localhost:3000"
GITHUB_ID=""
GITHUB_SECRET=""
AWS_DEFAULT_REGION="us-east-1" AWS_DEFAULT_REGION="us-east-1"
AWS_SECRET_KEY="" AWS_SECRET_KEY="some-secret-key"
AWS_ACCESS_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="" NEXTAUTH_SECRET=""
FROM_EMAIL="hello@unsend.dev"
API_RATE_LIMIT=2 API_RATE_LIMIT=2
NEXT_PUBLIC_IS_CLOUD=false NEXT_PUBLIC_IS_CLOUD=true

View File

@@ -12,10 +12,15 @@ DATABASE_URL="postgresql://postgres:postgres@postgres:5432/unsend"
NEXTAUTH_URL="http://localhost:3000" NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET= NEXTAUTH_SECRET=
## Auth providers any one is required
# Github login - required # Github login - required
GITHUB_ID="<your-github-client-id>" GITHUB_ID="<your-github-client-id>"
GITHUB_SECRET="<your-github-client-secret>" GITHUB_SECRET="<your-github-client-secret>"
# Google login - required
GOOGLE_CLIENT_ID="<your-google-client-id>"
GOOGLE_CLIENT_SECRET="<your-google-client-secret>"
# AWS details - required # AWS details - required
AWS_DEFAULT_REGION="us-east-1" AWS_DEFAULT_REGION="us-east-1"
AWS_SECRET_KEY="<your-aws-secret-key>" AWS_SECRET_KEY="<your-aws-secret-key>"

View File

@@ -10,38 +10,38 @@ Unsend's codebase is fully [open-source on github](https://github.com/unsend-dev
Here is the codebase structure Here is the codebase structure
``` ```
apps apps
├── docs ├── docs
├── marketing ├── marketing
├── web ├── web
packages packages
├── eslint-config ├── eslint-config
├── sdk ├── sdk
├── tailwind-config ├── tailwind-config
├── typescript-config ├── typescript-config
├── ui ├── 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 ## Running Unsend locally
@@ -62,9 +62,10 @@ To run Unsend, locally you will need to setup the following:
<Step title="Clone the repo"> <Step title="Clone the repo">
Once the repo is forked you can clone it on your local machine using: Once the repo is forked you can clone it on your local machine using:
```bash ```bash
git clone https://github.com/your-username/unsend.git git clone https://github.com/your-username/unsend.git
``` ```
</Step> </Step>
</Steps> </Steps>
@@ -95,42 +96,57 @@ pnpm install
<Step title="Nextauth secret"> <Step title="Nextauth secret">
Use the following command to generate a key and add it under ```NEXTAUTH_SECRET``` Use the following command to generate a key and add it under ```NEXTAUTH_SECRET```
```bash ```bash
openssl rand -base64 32 openssl rand -base64 32
``` ```
</Step> </Step>
<Step title="Setup Github Oauth"> <Step title="Setup Github Oauth (optional)">
<Note>
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.
</Note>
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: Add the homepage as:
```
http://localhost:3000/login
```
and callback URL as: ```
``` http://localhost:3000/login
http://localhost:3000/api/auth/callback/github ```
```
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```
</Step> </Step>
<Step title="Setup AWS credentials"> <Step title="Setup AWS credentials (Optional)">
<Note>
You don't need this setup if you are using the local-sen-sns image. But email
will not be sent out.
</Note>
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=<access-key-id>
AWS_SECRET_KEY=<secret-access-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=<access-key-id>
AWS_SECRET_KEY=<secret-access-key>
```
</Step> </Step>
</Steps> </Steps>
## Running Unsend locally ## 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. 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 ### Option 1: Using Docker Recommended
<Steps> <Steps>
<Step title="Start the dashboard"> <Step title="Start the dashboard">
```bash ```bash
@@ -147,6 +163,7 @@ Landing page will be started on
```bash ```bash
http://localhost:3001 http://localhost:3001
``` ```
</Step> </Step>
<Step title="Once you login with Github you will be prompted with SES settings. You will need to run cloudflare tunnel to add the callback URL"> <Step title="Once you login with Github you will be prompted with SES settings. You will need to run cloudflare tunnel to add the callback URL">
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/) 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
</Steps> </Steps>
### Option 2: Using your own database or hosted database ### Option 2: Using your own database or hosted database
<Steps> <Steps>
<Step title="Set up your PostgreSQL and Redis database in Environment Variables"> <Step title="Set up your PostgreSQL and Redis database in Environment Variables">
```bash ```bash
@@ -188,7 +206,6 @@ You can paste the URL provided by cloudflare in the Callback URL section
</Step> </Step>
</Steps> </Steps>
## Run documentation ## Run documentation
To run the documentation run the following command: To run the documentation run the following command:

View File

@@ -289,16 +289,17 @@ model Campaign {
} }
model Template { model Template {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
teamId Int teamId Int
subject String subject String
html String? html String?
content String? content String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
@@index([createdAt(sort: Desc)]) @@index([createdAt(sort: Desc)])
} }

View File

@@ -1,3 +1,4 @@
import { env } from "~/env";
import { db } from "~/server/db"; import { db } from "~/server/db";
import { parseSesHook, SesHookParser } from "~/server/service/ses-hook-parser"; import { parseSesHook, SesHookParser } from "~/server/service/ses-hook-parser";
import { SesSettingsService } from "~/server/service/ses-settings-service"; 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 * A simple check to ensure that the event is from the correct topic
*/ */
async function checkEventValidity(message: SnsNotificationMessage) { async function checkEventValidity(message: SnsNotificationMessage) {
if (env.NODE_ENV === "development") {
return true;
}
const { TopicArn } = message; const { TopicArn } = message;
const configuredTopicArn = await SesSettingsService.getTopicArns(); const configuredTopicArn = await SesSettingsService.getTopicArns();

View File

@@ -17,6 +17,7 @@ import { Input } from "@unsend/ui/src/input";
import { Button } from "@unsend/ui/src/button"; import { Button } from "@unsend/ui/src/button";
import Spinner from "@unsend/ui/src/spinner"; import Spinner from "@unsend/ui/src/spinner";
import { toast } from "@unsend/ui/src/toaster"; import { toast } from "@unsend/ui/src/toaster";
import { isLocalhost } from "~/utils/client";
const FormSchema = z.object({ const FormSchema = z.object({
region: z.string(), region: z.string(),
@@ -65,14 +66,16 @@ export const AddSesSettingsForm: React.FC<SesSettingsProps> = ({
}); });
function onSubmit(data: z.infer<typeof FormSchema>) { function onSubmit(data: z.infer<typeof FormSchema>) {
if (!data.unsendUrl.startsWith("https://")) { const localhost = isLocalhost();
if (!data.unsendUrl.startsWith("https://") && !localhost) {
form.setError("unsendUrl", { form.setError("unsendUrl", {
message: "URL must start with https://", message: "URL must start with https://",
}); });
return; return;
} }
if (data.unsendUrl.includes("localhost")) { if (data.unsendUrl.includes("localhost") && !localhost) {
form.setError("unsendUrl", { form.setError("unsendUrl", {
message: "URL must be a valid url", message: "URL must be a valid url",
}); });

View File

@@ -29,14 +29,15 @@ export const env = createEnv({
// VERCEL_URL doesn't include `https` so it cant be validated as a URL // VERCEL_URL doesn't include `https` so it cant be validated as a URL
process.env.VERCEL ? z.string() : z.string().url() process.env.VERCEL ? z.string() : z.string().url()
), ),
GITHUB_ID: z.string(), GITHUB_ID: z.string().optional(),
GITHUB_SECRET: z.string(), GITHUB_SECRET: z.string().optional(),
AWS_ACCESS_KEY: z.string(), AWS_ACCESS_KEY: z.string(),
AWS_SECRET_KEY: z.string(), AWS_SECRET_KEY: z.string(),
UNSEND_API_KEY: z.string().optional(), UNSEND_API_KEY: z.string().optional(),
GOOGLE_CLIENT_ID: z.string().optional(), GOOGLE_CLIENT_ID: z.string().optional(),
GOOGLE_CLIENT_SECRET: z.string().optional(), GOOGLE_CLIENT_SECRET: z.string().optional(),
AWS_SES_ENDPOINT: z.string().optional(), AWS_SES_ENDPOINT: z.string().optional(),
AWS_SNS_ENDPOINT: z.string().optional(),
AWS_DEFAULT_REGION: z.string().default("us-east-1"), AWS_DEFAULT_REGION: z.string().default("us-east-1"),
API_RATE_LIMIT: z API_RATE_LIMIT: z
.string() .string()
@@ -83,6 +84,7 @@ export const env = createEnv({
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
AWS_DEFAULT_REGION: process.env.AWS_DEFAULT_REGION, AWS_DEFAULT_REGION: process.env.AWS_DEFAULT_REGION,
AWS_SES_ENDPOINT: process.env.AWS_SES_ENDPOINT, AWS_SES_ENDPOINT: process.env.AWS_SES_ENDPOINT,
AWS_SNS_ENDPOINT: process.env.AWS_SNS_ENDPOINT,
API_RATE_LIMIT: process.env.API_RATE_LIMIT, API_RATE_LIMIT: process.env.API_RATE_LIMIT,
NEXT_PUBLIC_IS_CLOUD: process.env.NEXT_PUBLIC_IS_CLOUD, NEXT_PUBLIC_IS_CLOUD: process.env.NEXT_PUBLIC_IS_CLOUD,
ADMIN_EMAIL: process.env.ADMIN_EMAIL, ADMIN_EMAIL: process.env.ADMIN_EMAIL,

View File

@@ -44,13 +44,17 @@ declare module "next-auth" {
* Auth providers * Auth providers
*/ */
const providers: Provider[] = [ const providers: Provider[] = [];
GitHubProvider({
clientId: env.GITHUB_ID, if (env.GITHUB_ID && env.GITHUB_SECRET) {
clientSecret: env.GITHUB_SECRET, providers.push(
allowDangerousEmailAccountLinking: true, GitHubProvider({
}), clientId: env.GITHUB_ID,
]; clientSecret: env.GITHUB_SECRET,
allowDangerousEmailAccountLinking: true,
})
);
}
if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) { if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
providers.push( 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. * Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
* *
@@ -100,7 +108,7 @@ export const authOptions: NextAuthOptions = {
events: { events: {
createUser: async ({ user }) => { createUser: async ({ user }) => {
// No waitlist for self hosting // 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({ await db.user.update({
where: { id: user.id }, where: { id: user.id },
data: { isBetaUser: true }, data: { isBetaUser: true },

View File

@@ -8,6 +8,7 @@ import { env } from "~/env";
function getSnsClient(region: string) { function getSnsClient(region: string) {
return new SNSClient({ return new SNSClient({
endpoint: env.AWS_SNS_ENDPOINT,
region: region, region: region,
credentials: { credentials: {
accessKeyId: env.AWS_ACCESS_KEY, accessKeyId: env.AWS_ACCESS_KEY,
@@ -44,6 +45,5 @@ export async function subscribeEndpoint(
const client = getSnsClient(region); const client = getSnsClient(region);
const data = await client.send(subscribeCommand); const data = await client.send(subscribeCommand);
console.log(data.SubscriptionArn);
return data.SubscriptionArn; return data.SubscriptionArn;
} }

View File

@@ -18,7 +18,7 @@ export async function sendSignUpEmail(
const { host } = new URL(url); const { host } = new URL(url);
if (env.NODE_ENV === "development") { if (env.NODE_ENV === "development") {
console.log("Sending sign in email", email, url, token); console.log("Sending sign in email", { email, url, token });
return; return;
} }

View File

@@ -131,9 +131,10 @@ export class SesSettingsService {
try { try {
await sns.deleteTopic(topicArn, region); await sns.deleteTopic(topicArn, region);
} catch (deleteError) { } 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; throw error;
} }
} }

View File

@@ -0,0 +1,3 @@
export const isLocalhost = () => {
return location.hostname === "localhost";
};

View File

@@ -24,6 +24,15 @@ services:
- redis:/data - redis:/data
command: ["redis-server", "--maxmemory-policy", "noeviction"] 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: minio:
image: minio/minio image: minio/minio
container_name: unsend-storage-dev container_name: unsend-storage-dev