make dev setup better (#116)
* make dev setup better * chage docs * remove the need of github login
This commit is contained in:
11
.env.example
11
.env.example
@@ -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
|
||||||
|
@@ -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>"
|
||||||
|
@@ -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
|
http://localhost:3000/login
|
||||||
```
|
```
|
||||||
|
|
||||||
and callback URL as:
|
and callback URL as:
|
||||||
```
|
|
||||||
http://localhost:3000/api/auth/callback/github
|
```
|
||||||
```
|
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)">
|
||||||
|
|
||||||
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:
|
<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>
|
||||||
|
```
|
||||||
|
|
||||||
```
|
|
||||||
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:
|
||||||
|
@@ -299,6 +299,7 @@ model Template {
|
|||||||
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)])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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();
|
||||||
|
|
||||||
|
@@ -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",
|
||||||
});
|
});
|
||||||
|
@@ -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,
|
||||||
|
@@ -44,13 +44,17 @@ declare module "next-auth" {
|
|||||||
* Auth providers
|
* Auth providers
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const providers: Provider[] = [
|
const providers: Provider[] = [];
|
||||||
|
|
||||||
|
if (env.GITHUB_ID && env.GITHUB_SECRET) {
|
||||||
|
providers.push(
|
||||||
GitHubProvider({
|
GitHubProvider({
|
||||||
clientId: env.GITHUB_ID,
|
clientId: env.GITHUB_ID,
|
||||||
clientSecret: env.GITHUB_SECRET,
|
clientSecret: env.GITHUB_SECRET,
|
||||||
allowDangerousEmailAccountLinking: true,
|
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 },
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
3
apps/web/src/utils/client.ts
Normal file
3
apps/web/src/utils/client.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const isLocalhost = () => {
|
||||||
|
return location.hostname === "localhost";
|
||||||
|
};
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user