Merge remote-tracking branch 'origin/auth-better' into fix/github-ready
This commit is contained in:
55
.dockerignore
Normal file
55
.dockerignore
Normal file
@@ -0,0 +1,55 @@
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Build outputs
|
||||
**/dist/
|
||||
**/build/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids/
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
|
||||
# Docker
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
.dockerignore
|
||||
|
||||
# Tests
|
||||
**/tests/
|
||||
**/*.test.*
|
||||
**/*.spec.*
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -62,6 +62,7 @@ yarn-error.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
!.env.example
|
||||
|
||||
# example env files
|
||||
!.env.example
|
||||
@@ -75,4 +76,5 @@ next-env.d.ts
|
||||
|
||||
*.rdb
|
||||
|
||||
packages/useautumn
|
||||
|
||||
shared/drizzle
|
||||
|
||||
19
commands.sh
Normal file
19
commands.sh
Normal file
@@ -0,0 +1,19 @@
|
||||
RESTART EVERYTHING
|
||||
docker system prune -a --volumes
|
||||
|
||||
# DB
|
||||
docker compose -f docker-compose.db.yml up --build
|
||||
|
||||
# Create DB tables
|
||||
pnpm
|
||||
|
||||
# docker volume rm main-repo_shared-node-modules main-repo_root-node-modules main-repo_vite-node-modules
|
||||
# Dev
|
||||
docker compose -f docker-compose.dev.yml down
|
||||
docker volume rm autumn-oss_shared-node-modules autumn-oss_root-node-modules autumn-oss_vite-node-modules
|
||||
docker compose -f docker-compose.dev.yml build --no-cache
|
||||
docker compose -f docker-compose.dev.yml up --build
|
||||
|
||||
# Prod
|
||||
docker compose -f docker-compose.prod.yml up --build
|
||||
---
|
||||
97
docker-compose.dev.yml
Normal file
97
docker-compose.dev.yml
Normal file
@@ -0,0 +1,97 @@
|
||||
services:
|
||||
valkey:
|
||||
image: docker.io/bitnami/valkey:8.0
|
||||
environment:
|
||||
- ALLOW_EMPTY_PASSWORD=yes
|
||||
- VALKEY_DISABLE_COMMANDS=FLUSHDB,FLUSHALL
|
||||
volumes:
|
||||
- valkey-data:/bitnami/valkey/data
|
||||
healthcheck:
|
||||
test: ['CMD', 'redis-cli', '-h', 'localhost', '-p', '6379', 'ping']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
# ports:
|
||||
# - "6379:6379"
|
||||
restart: unless-stopped
|
||||
|
||||
shared:
|
||||
build:
|
||||
dockerfile: docker/dev.dockerfile
|
||||
context: .
|
||||
target: shared
|
||||
volumes:
|
||||
- ./shared:/app/shared
|
||||
- shared-dist:/app/shared/dist
|
||||
- shared-node-modules:/app/shared/node_modules
|
||||
- root-node-modules:/app/node_modules
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
restart: unless-stopped
|
||||
|
||||
# Vite frontend
|
||||
vite:
|
||||
build:
|
||||
dockerfile: docker/dev.dockerfile
|
||||
context: .
|
||||
target: vite
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ./vite:/app/vite
|
||||
- shared-dist:/app/shared/dist
|
||||
- vite-node-modules:/app/vite/node_modules
|
||||
- root-node-modules:/app/node_modules
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
depends_on:
|
||||
- shared
|
||||
restart: unless-stopped
|
||||
|
||||
# Main Express server
|
||||
server:
|
||||
build:
|
||||
dockerfile: docker/dev.dockerfile
|
||||
context: .
|
||||
target: server
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./server:/app/server
|
||||
- shared-dist:/app/shared/dist
|
||||
- root-node-modules:/app/node_modules
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- REDIS_URL=redis://valkey:6379
|
||||
depends_on:
|
||||
- shared
|
||||
restart: unless-stopped
|
||||
|
||||
# BullMQ Workers
|
||||
workers:
|
||||
build:
|
||||
dockerfile: docker/dev.dockerfile
|
||||
context: .
|
||||
target: workers
|
||||
volumes:
|
||||
# Mount server source for hot reload (workers use server code)
|
||||
- ./server:/app/server
|
||||
- shared-dist:/app/shared/dist
|
||||
- root-node-modules:/app/node_modules
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- REDIS_URL=redis://valkey:6379
|
||||
depends_on:
|
||||
- shared
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
# Shared package dist output
|
||||
shared-dist:
|
||||
valkey-data:
|
||||
|
||||
# Node modules volumes to avoid host/container conflicts
|
||||
shared-node-modules:
|
||||
server-node-modules:
|
||||
vite-node-modules:
|
||||
root-node-modules:
|
||||
46
docker-compose.prod.yml
Normal file
46
docker-compose.prod.yml
Normal file
@@ -0,0 +1,46 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
valkey:
|
||||
image: docker.io/bitnami/valkey:8.0
|
||||
environment:
|
||||
- ALLOW_EMPTY_PASSWORD=yes
|
||||
- VALKEY_DISABLE_COMMANDS=FLUSHDB,FLUSHALL
|
||||
volumes:
|
||||
- valkey-data:/bitnami/valkey/data
|
||||
healthcheck:
|
||||
test: ['CMD', 'redis-cli', '-h', 'localhost', '-p', '6379', 'ping']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
ports:
|
||||
- "6379:6379"
|
||||
restart: unless-stopped
|
||||
|
||||
vite:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/prod.dockerfile
|
||||
target: vite-prod
|
||||
ports:
|
||||
- "3000:3000"
|
||||
restart: always
|
||||
|
||||
server:
|
||||
environment:
|
||||
- REDIS_URL=redis://valkey:6379
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/prod.dockerfile
|
||||
target: server-prod
|
||||
ports:
|
||||
- "8080:8080"
|
||||
restart: always
|
||||
|
||||
workers:
|
||||
environment:
|
||||
- REDIS_URL=redis://valkey:6379
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/prod.dockerfile
|
||||
target: workers-prod
|
||||
restart: always
|
||||
43
docker/dev.dockerfile
Normal file
43
docker/dev.dockerfile
Normal file
@@ -0,0 +1,43 @@
|
||||
# Multi-stage Dockerfile for Autumn development
|
||||
FROM node:18-alpine AS base
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN npm install -g pnpm
|
||||
|
||||
COPY package*.json ./
|
||||
COPY pnpm-workspace.yaml ./
|
||||
COPY pnpm-lock.yaml ./
|
||||
COPY shared/package*.json ./shared/
|
||||
COPY server/package*.json ./server/
|
||||
COPY vite/package*.json ./vite/
|
||||
|
||||
RUN pnpm install
|
||||
RUN npm install -g nodemon tsx
|
||||
|
||||
# Stage 2: /shared
|
||||
FROM base AS shared
|
||||
COPY shared/ ./shared/
|
||||
WORKDIR /app/shared
|
||||
RUN pnpm run build
|
||||
CMD ["pnpm", "run", "dev"]
|
||||
|
||||
# Stage 3: /vite
|
||||
FROM base AS vite
|
||||
WORKDIR /app/vite
|
||||
COPY vite/ ./
|
||||
EXPOSE 3000
|
||||
CMD ["pnpm", "run", "dev"]
|
||||
|
||||
# Stage 4: /server
|
||||
FROM base AS server
|
||||
WORKDIR /app/server
|
||||
COPY server/ ./
|
||||
EXPOSE 8080
|
||||
CMD ["pnpm", "run", "dev"]
|
||||
|
||||
# Stage 5: Workers
|
||||
FROM base AS workers
|
||||
WORKDIR /app/server
|
||||
COPY server/ ./
|
||||
CMD ["pnpm", "run", "workers"]
|
||||
42
docker/docker-compose.db.yml
Normal file
42
docker/docker-compose.db.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Redis for caching and BullMQ
|
||||
valkey:
|
||||
image: docker.io/bitnami/valkey:8.0
|
||||
environment:
|
||||
- ALLOW_EMPTY_PASSWORD=yes
|
||||
- VALKEY_DISABLE_COMMANDS=FLUSHDB,FLUSHALL
|
||||
volumes:
|
||||
- valkey-data:/bitnami/valkey/data
|
||||
healthcheck:
|
||||
test: ['CMD', 'redis-cli', '-h', 'localhost', '-p', '6379', 'ping']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# PostgreSQL database
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: autumn-postgres
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_DB: autumn
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: autumn_dev_password
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
valkey-data:
|
||||
postgres-data:
|
||||
|
||||
networks:
|
||||
autumn-network:
|
||||
60
docker/prod.dockerfile
Normal file
60
docker/prod.dockerfile
Normal file
@@ -0,0 +1,60 @@
|
||||
# ---- Base dependencies ----
|
||||
FROM node:18-alpine AS base
|
||||
WORKDIR /app
|
||||
RUN npm install -g pnpm serve typescript tsc tsc-alias tsx
|
||||
|
||||
COPY package*.json pnpm-workspace.yaml pnpm-lock.yaml ./
|
||||
COPY shared/package*.json ./shared/
|
||||
COPY server/package*.json ./server/
|
||||
COPY vite/package*.json ./vite/
|
||||
|
||||
RUN pnpm install
|
||||
|
||||
|
||||
# # ---- Build shared ----
|
||||
# FROM base AS shared-build
|
||||
# COPY shared/ ./shared/
|
||||
# RUN pnpm -F shared build
|
||||
|
||||
# # ---- Build frontend (vite) ----
|
||||
FROM base AS vite-build
|
||||
COPY . .
|
||||
WORKDIR /app
|
||||
RUN pnpm run vite:build
|
||||
|
||||
# ---- Build backend (server) ----
|
||||
FROM base AS server-build
|
||||
COPY . .
|
||||
WORKDIR /app
|
||||
RUN pnpm run server:build
|
||||
|
||||
# ---- Production frontend image ----
|
||||
FROM vite-build AS vite-prod
|
||||
EXPOSE 3000
|
||||
WORKDIR /app
|
||||
CMD ["pnpm", "run", "vite:start"]
|
||||
|
||||
FROM server-build AS server-prod
|
||||
EXPOSE 8080
|
||||
WORKDIR /app
|
||||
CMD ["pnpm", "run", "server:start"]
|
||||
|
||||
FROM server-build AS workers-prod
|
||||
EXPOSE 8080
|
||||
WORKDIR /app
|
||||
CMD ["pnpm", "run", "server:workers"]
|
||||
|
||||
# # ---- Production backend image ----
|
||||
# FROM node:18-alpine AS server-prod
|
||||
# COPY --from=server-build /app/server/dist ./dist
|
||||
# COPY --from=server-build /app/server/package.json ./
|
||||
# COPY --from=server-build /app/server/node_modules ./node_modules
|
||||
# EXPOSE 8080
|
||||
# CMD ["pnpm", "run", "server:start"]
|
||||
|
||||
# # ---- Production workers image ----
|
||||
# FROM node:18-alpine AS workers-prod
|
||||
# COPY --from=server-build /app/server/dist ./dist
|
||||
# COPY --from=server-build /app/server/package.json ./
|
||||
# COPY --from=server-build /app/server/node_modules ./node_modules
|
||||
# CMD ["pnpm", "run", "server:workers"]
|
||||
@@ -1,5 +0,0 @@
|
||||
https://docs.fontawesome.com/web/use-with/react
|
||||
|
||||
https://docs.fontawesome.com/web/setup/packages#1-configure-access
|
||||
|
||||
In the frontend (vite) run `npm link @useautumn/react`
|
||||
25
package.json
25
package.json
@@ -2,25 +2,32 @@
|
||||
"name": "autumn",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"frontend",
|
||||
"server",
|
||||
"shared",
|
||||
"tests",
|
||||
"vite"
|
||||
],
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"frontend:build": "npm run build -w shared && npm run build -w frontend",
|
||||
"frontend:start": "npm run start -w frontend",
|
||||
"vite:build": "npm run build -w shared && npm run build -w vite",
|
||||
"vite:start": "npm run start -w vite",
|
||||
"server:build": "npm run build -w shared && npm run prod:build -w server",
|
||||
"server:start": "npm run prod:start -w server",
|
||||
"vite:build": "pnpm -F shared build && pnpm -F vite build",
|
||||
"vite:start": "pnpm -F vite start",
|
||||
"server:build": "pnpm -F shared build && pnpm -F server prod:build",
|
||||
"server:start": "pnpm -F server prod:start",
|
||||
"server:cron": "npm run cron:start -w server",
|
||||
"server:workers": "npm run workers:start -w server",
|
||||
"dev": "concurrently \"cd server && npm run dev\" \"cd vite && npm run dev\" \"redis-server\""
|
||||
"dev": "concurrently \"cd server && npm run dev\" \"cd vite && npm run dev\" \"redis-server\"",
|
||||
"setup": "node setup.js",
|
||||
"db:push": "pnpm -F shared db:push",
|
||||
"db:generate": "pnpm -F shared db:generate",
|
||||
"db:migrate": "pnpm -F shared db:migrate"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
"drizzle-kit": "^0.31.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.3",
|
||||
"dotenv": "^16.5.0",
|
||||
"inquirer": "^12.6.3",
|
||||
"concurrently": "^9.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
10850
pnpm-lock.yaml
generated
Normal file
10850
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
4
pnpm-workspace.yaml
Normal file
4
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
- 'shared'
|
||||
- 'server'
|
||||
- 'vite'
|
||||
3
run.sh
Executable file
3
run.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
if [[ $1 == *"docker-compose"* ]]; then
|
||||
docker compose -f "$1" up
|
||||
fi
|
||||
15
server/.env.example
Normal file
15
server/.env.example
Normal file
@@ -0,0 +1,15 @@
|
||||
# AUTH
|
||||
BETTER_AUTH_SECRET=
|
||||
BETTER_AUTH_URL=http://localhost:8080
|
||||
CLIENT_URL=http://localhost:3000
|
||||
|
||||
# STRIPE REQUIRED
|
||||
ENCRYPTION_IV=
|
||||
ENCRYPTION_PASSWORD=
|
||||
SERVER_URL=
|
||||
|
||||
# DATABASE
|
||||
DATABASE_URL=
|
||||
|
||||
# IF using external redis
|
||||
# REDIS_URL=redis://valkey:6379
|
||||
112
server/auth-schema.ts
Normal file
112
server/auth-schema.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
boolean,
|
||||
integer,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
export const user = pgTable("user", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull().unique(),
|
||||
emailVerified: boolean("email_verified")
|
||||
.$defaultFn(() => false)
|
||||
.notNull(),
|
||||
image: text("image"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.$defaultFn(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true })
|
||||
.$defaultFn(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
role: text("role"),
|
||||
banned: boolean("banned"),
|
||||
banReason: text("ban_reason"),
|
||||
banExpires: timestamp("ban_expires"),
|
||||
}).enableRLS();
|
||||
|
||||
export const session = pgTable("session", {
|
||||
id: text("id").primaryKey(),
|
||||
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
||||
token: text("token").notNull().unique(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(),
|
||||
ipAddress: text("ip_address"),
|
||||
userAgent: text("user_agent"),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
impersonatedBy: text("impersonated_by"),
|
||||
activeOrganizationId: text("active_organization_id"),
|
||||
}).enableRLS();
|
||||
|
||||
export const account = pgTable("account", {
|
||||
id: text("id").primaryKey(),
|
||||
accountId: text("account_id").notNull(),
|
||||
providerId: text("provider_id").notNull(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
idToken: text("id_token"),
|
||||
accessTokenExpiresAt: timestamp("access_token_expires_at", {
|
||||
withTimezone: true,
|
||||
}),
|
||||
refreshTokenExpiresAt: timestamp("refresh_token_expires_at", {
|
||||
withTimezone: true,
|
||||
}),
|
||||
scope: text("scope"),
|
||||
password: text("password"),
|
||||
createdAt: timestamp("created_at").notNull(),
|
||||
updatedAt: timestamp("updated_at").notNull(),
|
||||
}).enableRLS();
|
||||
|
||||
export const verification = pgTable("verification", {
|
||||
id: text("id").primaryKey(),
|
||||
identifier: text("identifier").notNull(),
|
||||
value: text("value").notNull(),
|
||||
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).$defaultFn(
|
||||
() => /* @__PURE__ */ new Date(),
|
||||
),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).$defaultFn(
|
||||
() => /* @__PURE__ */ new Date(),
|
||||
),
|
||||
}).enableRLS();
|
||||
|
||||
export const organization = pgTable("organization", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
slug: text("slug").unique(),
|
||||
logo: text("logo"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
|
||||
metadata: text("metadata"),
|
||||
}).enableRLS();
|
||||
|
||||
export const member = pgTable("member", {
|
||||
id: text("id").primaryKey(),
|
||||
organizationId: text("organization_id")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
role: text("role").default("member").notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
|
||||
}).enableRLS();
|
||||
|
||||
export const invitation = pgTable("invitation", {
|
||||
id: text("id").primaryKey(),
|
||||
organizationId: text("organization_id")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
email: text("email").notNull(),
|
||||
role: text("role"),
|
||||
status: text("status").default("pending").notNull(),
|
||||
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
||||
inviterId: text("inviter_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
}).enableRLS();
|
||||
57
server/emails/OTPEmail.tsx
Normal file
57
server/emails/OTPEmail.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
Html,
|
||||
Head,
|
||||
Body,
|
||||
Container,
|
||||
Section,
|
||||
Text,
|
||||
Heading,
|
||||
Tailwind,
|
||||
} from "@react-email/components";
|
||||
|
||||
const OTPEmail = (props: { otpCode: string }) => {
|
||||
return (
|
||||
<Html lang="en" dir="ltr">
|
||||
<Tailwind>
|
||||
<Head />
|
||||
<Body className="bg-white font-sans">
|
||||
<Container className="bg-white max-w-[600px] mx-auto px-[40px] py-[40px]">
|
||||
<Heading className="text-gray-900 text-[24px] font-bold mb-[24px]">
|
||||
Verification code
|
||||
</Heading>
|
||||
|
||||
<Text className="text-gray-800 text-[16px] leading-[24px] mb-[24px]">
|
||||
Enter the following verification code when prompted:
|
||||
</Text>
|
||||
|
||||
<Text className="text-[32px] font-bold text-gray-900 font-mono mb-[24px]">
|
||||
{props.otpCode}
|
||||
</Text>
|
||||
|
||||
<Text className="text-gray-800 text-[16px] leading-[24px] mb-[40px]">
|
||||
To protect your account, do not share this code.
|
||||
</Text>
|
||||
|
||||
{/* Footer */}
|
||||
<Section className="border-t border-gray-200 pt-[24px]">
|
||||
<Text className="text-gray-500 text-[12px] leading-[16px] m-0">
|
||||
Autumn
|
||||
<br />
|
||||
2261 Market Street STE 22390
|
||||
<br />
|
||||
San Francisco, CA, US, 94114
|
||||
</Text>
|
||||
</Section>
|
||||
</Container>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
OTPEmail.PreviewProps = {
|
||||
otpCode: "884085",
|
||||
};
|
||||
|
||||
export default OTPEmail;
|
||||
@@ -5,6 +5,7 @@
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"email": "email dev -p 3001",
|
||||
"dev": "NODE_ENV=development nodemon --no-deprecation --exec tsx src/index.ts --ignore scripts --ignore tests",
|
||||
"start": "tsx src/index.ts",
|
||||
"workers": "tsx watch src/workers.ts",
|
||||
@@ -36,18 +37,20 @@
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^1.2.10",
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
"@autumn/shared": "*",
|
||||
"@autumn/shared": "workspace:*",
|
||||
"@clerk/express": "^1.3.22",
|
||||
"@date-fns/tz": "^1.2.0",
|
||||
"@date-fns/utc": "^2.1.0",
|
||||
"@logtail/node": "^0.5.2",
|
||||
"@react-email/components": "^0.0.42",
|
||||
"@supabase/supabase-js": "^2.46.2",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
"ai": "^4.3.10",
|
||||
"autumn-js": "^0.0.64",
|
||||
"better-auth": "^1.2.9",
|
||||
"body-parser": "^1.20.3",
|
||||
"bullmq": "^5.31.1",
|
||||
"bullmq": "^5.54.0",
|
||||
"chai": "^5.1.2",
|
||||
"chai-http": "^5.1.1",
|
||||
"chalk": "^5.3.0",
|
||||
@@ -58,6 +61,7 @@
|
||||
"currency-symbol-map": "^5.1.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"decimal.js": "^10.5.0",
|
||||
"detect-content-type": "^1.2.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"drizzle-orm": "^0.43.1",
|
||||
"express": "^4.21.1",
|
||||
@@ -66,6 +70,7 @@
|
||||
"ioredis": "^5.5.0",
|
||||
"ksuid": "^3.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mime-detect": "^1.3.0",
|
||||
"nodemon": "^3.1.7",
|
||||
"openai": "^4.85.2",
|
||||
"pg": "^8.13.1",
|
||||
@@ -73,12 +78,14 @@
|
||||
"pino-pretty": "^13.0.0",
|
||||
"postgres": "^3.4.7",
|
||||
"posthog-node": "^4.17.2",
|
||||
"react": "^18.2.0",
|
||||
"recaseai": "^0.0.37",
|
||||
"resend": "^4.1.1",
|
||||
"stripe": "^17.5.0",
|
||||
"svix": "^1.45.1",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"ws": "^8.18.0"
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.25.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^5.0.1",
|
||||
@@ -87,9 +94,13 @@
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "^22.13.4",
|
||||
"@types/pg": "^8.11.10",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@types/ws": "^8.18.1",
|
||||
"drizzle-kit": "^0.31.1",
|
||||
"mocha": "^11.1.0",
|
||||
"puppeteer": "^24.2.0",
|
||||
"react-email": "4.0.16",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tsx": "^4.19.4",
|
||||
|
||||
@@ -5,6 +5,8 @@ filename=$1
|
||||
|
||||
|
||||
# Check if the file path contains "shell"
|
||||
|
||||
|
||||
if [[ $filename == *"shell"* ]]; then
|
||||
$filename
|
||||
elif [[ $filename == *"/tests/"* ]]; then
|
||||
@@ -17,6 +19,9 @@ elif [[ $filename == *"/tests/"* ]]; then
|
||||
else
|
||||
./test.sh custom $path_after_tests
|
||||
fi
|
||||
|
||||
elif [[ $filename == *".sh"* ]]; then
|
||||
$filename
|
||||
else
|
||||
npx tsx $filename
|
||||
fi
|
||||
|
||||
@@ -3,7 +3,10 @@ dotenv.config();
|
||||
|
||||
import postgres from "postgres";
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import { schemas } from "@autumn/shared";
|
||||
import { schemas as schema } from "@autumn/shared";
|
||||
|
||||
export let client = postgres(process.env.DATABASE_URL!);
|
||||
export let db = drizzle(client, { schema });
|
||||
|
||||
export const initDrizzle = (params?: { maxConnections?: number }) => {
|
||||
let maxConnections = params?.maxConnections || 10;
|
||||
@@ -11,10 +14,7 @@ export const initDrizzle = (params?: { maxConnections?: number }) => {
|
||||
max: maxConnections,
|
||||
});
|
||||
|
||||
const db = drizzle(client, {
|
||||
schema: schemas,
|
||||
// logger: true, // Enable SQL logging for debugging
|
||||
});
|
||||
const db = drizzle(client, { schema });
|
||||
|
||||
return { db, client };
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import express from "express";
|
||||
import express, { Router } from "express";
|
||||
import { Webhook } from "svix";
|
||||
|
||||
export const autumnWebhookRouter = express.Router();
|
||||
export const autumnWebhookRouter: Router = express.Router();
|
||||
|
||||
const verifyAutumnWebhook = async (req: any, res: any) => {
|
||||
const wh = new Webhook(process.env.AUTUMN_WEBHOOK_SECRET!);
|
||||
@@ -54,5 +54,5 @@ autumnWebhookRouter.post(
|
||||
success: true,
|
||||
message: "Webhook received",
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
4
server/src/external/caching/CacheManager.ts
vendored
4
server/src/external/caching/CacheManager.ts
vendored
@@ -15,10 +15,10 @@ export class CacheManager {
|
||||
console.log("Initializing Cache Manager...");
|
||||
if (this.initialized) return;
|
||||
|
||||
const redisUrl = process.env.REDIS_BACKUP_URL;
|
||||
const redisUrl = process.env.REDIS_BACKUP_URL || process.env.REDIS_URL;
|
||||
|
||||
if (!redisUrl) {
|
||||
throw new Error("Redis URL not configured");
|
||||
throw new Error("Cache error: no redis connection string set in env");
|
||||
}
|
||||
|
||||
this.client = new Redis(redisUrl, {
|
||||
|
||||
55
server/src/external/logtail/logtailUtils.ts
vendored
55
server/src/external/logtail/logtailUtils.ts
vendored
@@ -1,3 +1,6 @@
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
import { initLogger } from "@/errors/logger.js";
|
||||
import { Logtail } from "@logtail/node";
|
||||
|
||||
@@ -36,6 +39,10 @@ const createLogMethod = (pinoMethod: any, logtailMethod: any) => {
|
||||
pinoMethod(message);
|
||||
}
|
||||
|
||||
if (!logtailMethod) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Logtail format: message first, then object (if exists)
|
||||
if (Object.keys(mergedObj).length > 0) {
|
||||
logtailMethod(message, mergedObj);
|
||||
@@ -45,34 +52,42 @@ const createLogMethod = (pinoMethod: any, logtailMethod: any) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const createLogtail = () => {
|
||||
const logtail = new Logtail(process.env.LOGTAIL_SOURCE_TOKEN!, {
|
||||
endpoint: process.env.LOGTAIL_INGESTING_HOST!,
|
||||
});
|
||||
export const createLogger = ({
|
||||
sourceToken,
|
||||
ingestingHost,
|
||||
}: {
|
||||
sourceToken: string;
|
||||
ingestingHost: string;
|
||||
}) => {
|
||||
let logtail: any;
|
||||
if (sourceToken && ingestingHost) {
|
||||
logtail = new Logtail(sourceToken, {
|
||||
endpoint: ingestingHost,
|
||||
});
|
||||
}
|
||||
|
||||
// Create a custom logger that logs to both Logtail and console
|
||||
const logger = {
|
||||
debug: createLogMethod(
|
||||
pinoLogger.debug.bind(pinoLogger),
|
||||
logtail.debug.bind(logtail),
|
||||
logtail?.debug.bind(logtail),
|
||||
),
|
||||
info: createLogMethod(
|
||||
pinoLogger.info.bind(pinoLogger),
|
||||
logtail.info.bind(logtail),
|
||||
logtail?.info.bind(logtail),
|
||||
),
|
||||
warn: createLogMethod(
|
||||
pinoLogger.warn.bind(pinoLogger),
|
||||
logtail.warn.bind(logtail),
|
||||
logtail?.warn.bind(logtail),
|
||||
),
|
||||
error: createLogMethod(
|
||||
pinoLogger.error.bind(pinoLogger),
|
||||
logtail.error.bind(logtail),
|
||||
logtail?.error.bind(logtail),
|
||||
),
|
||||
use: (fn: any) => {
|
||||
logtail.use(fn);
|
||||
logtail?.use(fn);
|
||||
},
|
||||
getLogtail: () => logtail,
|
||||
flush: () => logtail.flush(),
|
||||
flush: () => logtail?.flush(),
|
||||
};
|
||||
|
||||
return logger;
|
||||
@@ -90,7 +105,21 @@ export const createLogtailWithContext = (context: any) => {
|
||||
return logtail;
|
||||
};
|
||||
|
||||
export const createLogtail = () => {
|
||||
return createLogger({
|
||||
sourceToken: process.env.LOGTAIL_SOURCE_TOKEN!,
|
||||
ingestingHost: process.env.LOGTAIL_INGESTING_HOST!,
|
||||
});
|
||||
};
|
||||
|
||||
export const createLogtailAll = () => {
|
||||
if (
|
||||
!process.env.LOGTAIL_ALL_SOURCE_TOKEN ||
|
||||
!process.env.LOGTAIL_ALL_INGESTING_HOST
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const logtail = new Logtail(process.env.LOGTAIL_ALL_SOURCE_TOKEN!, {
|
||||
endpoint: process.env.LOGTAIL_ALL_INGESTING_HOST!,
|
||||
});
|
||||
@@ -98,5 +127,5 @@ export const createLogtailAll = () => {
|
||||
return logtail;
|
||||
};
|
||||
|
||||
// const logtail = createLogtailLogger();
|
||||
// export default logtail;
|
||||
export const logger = createLogtail();
|
||||
export const logtailAll = createLogtailAll();
|
||||
|
||||
@@ -2,11 +2,9 @@ import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
import { PostHog } from "posthog-node";
|
||||
import { initLogger } from "@/errors/logger.js";
|
||||
import { logger } from "../logtail/logtailUtils.js";
|
||||
|
||||
export const createPosthogCli = () => {
|
||||
const logger = initLogger();
|
||||
|
||||
if (!process.env.POSTHOG_API_KEY) {
|
||||
logger.warn("POSTHOG_API_KEY not set, skipping posthog");
|
||||
return null;
|
||||
|
||||
14
server/src/external/resend/resendUtils.ts
vendored
14
server/src/external/resend/resendUtils.ts
vendored
@@ -1,21 +1,23 @@
|
||||
import { Resend } from "resend";
|
||||
|
||||
export const createCli = () => {
|
||||
export const createResendCli = () => {
|
||||
return new Resend(process.env.RESEND_API_KEY);
|
||||
};
|
||||
|
||||
export const sendTextEmail = async ({
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
body,
|
||||
}: {
|
||||
from?: string;
|
||||
to: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
}) => {
|
||||
const resend = createCli();
|
||||
const resend = createResendCli();
|
||||
await resend.emails.send({
|
||||
from: `Ayush <ayush@${process.env.RESEND_DOMAIN}>`,
|
||||
from: from || `Ayush <ayush@${process.env.RESEND_DOMAIN}>`,
|
||||
to: to,
|
||||
subject: subject,
|
||||
text: body,
|
||||
@@ -23,17 +25,19 @@ export const sendTextEmail = async ({
|
||||
};
|
||||
|
||||
export const sendHtmlEmail = async ({
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
body,
|
||||
}: {
|
||||
from?: string;
|
||||
to: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
}) => {
|
||||
const resend = createCli();
|
||||
const resend = createResendCli();
|
||||
await resend.emails.send({
|
||||
from: `Ayush <ayush@${process.env.RESEND_DOMAIN}>`,
|
||||
from: from || `Ayush <ayush@${process.env.RESEND_DOMAIN}>`,
|
||||
to: to,
|
||||
subject: subject,
|
||||
html: body,
|
||||
|
||||
23
server/src/external/resend/safeResend.ts
vendored
Normal file
23
server/src/external/resend/safeResend.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
import { logger } from "../logtail/logtailUtils.js";
|
||||
|
||||
export function safeResend<T extends (...args: any[]) => any>({
|
||||
fn,
|
||||
action,
|
||||
}: {
|
||||
fn: T;
|
||||
action: string;
|
||||
}): (...args: Parameters<T>) => Promise<ReturnType<T> | undefined> {
|
||||
return async (...args: Parameters<T>) => {
|
||||
if (!process.env.RESEND_API_KEY || !process.env.RESEND_DOMAIN) {
|
||||
logger.warn(
|
||||
`RESEND_API_KEY or RESEND_DOMAIN is not set, skipping ${action}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
return await fn(...args);
|
||||
} catch (error) {
|
||||
logger.error(`Error ${action}: ${error}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -89,7 +89,7 @@ export const createStripeMeteredPrice = async ({
|
||||
const stripePrice = await stripeCli.prices.create({
|
||||
...productData,
|
||||
...priceAmountData,
|
||||
currency: org.default_currency,
|
||||
currency: org.default_currency || "usd",
|
||||
nickname: `Autumn Price (${feature!.name}) [Placeholder]`,
|
||||
recurring: {
|
||||
...(billingIntervalToStripe(price.config!.interval!) as any),
|
||||
@@ -194,7 +194,7 @@ export const createStripeArrearProrated = async ({
|
||||
|
||||
let stripePrice = await stripeCli.prices.create({
|
||||
...productData,
|
||||
currency: org.default_currency,
|
||||
currency: org.default_currency || "usd",
|
||||
...priceAmountData,
|
||||
recurring: {
|
||||
...(recurringData as any),
|
||||
|
||||
@@ -26,7 +26,7 @@ export const createStripeFixedPrice = async ({
|
||||
const stripePrice = await stripeCli.prices.create({
|
||||
product: product.processor!.id,
|
||||
unit_amount: amount,
|
||||
currency: org.default_currency,
|
||||
currency: org.default_currency!,
|
||||
recurring: {
|
||||
...(billingIntervalToStripe(config.interval!) as any),
|
||||
},
|
||||
|
||||
@@ -255,7 +255,7 @@ export const createStripeInArrearPrice = async ({
|
||||
const stripePrice = await stripeCli.prices.create({
|
||||
...productData,
|
||||
...priceAmountData,
|
||||
currency: org.default_currency,
|
||||
currency: org.default_currency!,
|
||||
recurring: {
|
||||
...(billingIntervalToStripe(price.config!.interval!) as any),
|
||||
meter: meter!.id,
|
||||
|
||||
@@ -110,7 +110,7 @@ export const createStripePrepaid = async ({
|
||||
...productData,
|
||||
// unit_amount_decimal: (amount * 100).toString(),
|
||||
unit_amount_decimal: unitAmountDecimalStr,
|
||||
currency: org.default_currency,
|
||||
currency: org.default_currency!,
|
||||
});
|
||||
|
||||
config.stripe_product_id = stripePrice.product as string;
|
||||
@@ -133,7 +133,7 @@ export const createStripePrepaid = async ({
|
||||
|
||||
stripePrice = await stripeCli.prices.create({
|
||||
...productData,
|
||||
currency: org.default_currency,
|
||||
currency: org.default_currency!,
|
||||
...priceAmountData,
|
||||
recurring: {
|
||||
...(recurringData as any),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { AppEnv, ErrCode } from "@autumn/shared";
|
||||
import Stripe from "stripe";
|
||||
|
||||
export const checkKeyValid = async (apiKey: string) => {
|
||||
@@ -18,8 +19,19 @@ export const createWebhookEndpoint = async (
|
||||
) => {
|
||||
const stripe = new Stripe(apiKey);
|
||||
|
||||
const webhookBaseUrl =
|
||||
process.env.SERVER_URL || process.env.STRIPE_WEBHOOK_URL;
|
||||
|
||||
if (!webhookBaseUrl) {
|
||||
throw new RecaseError({
|
||||
message: "Stripe webhook baseURL not found",
|
||||
code: ErrCode.StripeKeyInvalid,
|
||||
statusCode: 500,
|
||||
});
|
||||
}
|
||||
|
||||
const endpoint = await stripe.webhookEndpoints.create({
|
||||
url: `${process.env.SERVER_URL}/webhooks/stripe/${orgId}/${env}`,
|
||||
url: `${webhookBaseUrl}/webhooks/stripe/${orgId}/${env}`,
|
||||
enabled_events: [
|
||||
"customer.subscription.created",
|
||||
"customer.subscription.updated",
|
||||
|
||||
10
server/src/external/stripe/stripeWebhooks.ts
vendored
10
server/src/external/stripe/stripeWebhooks.ts
vendored
@@ -1,7 +1,10 @@
|
||||
import express, { Router } from "express";
|
||||
import stripe, { Stripe } from "stripe";
|
||||
import chalk from "chalk";
|
||||
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import { AuthType, LoggerAction, Organization } from "@autumn/shared";
|
||||
import express from "express";
|
||||
import stripe, { Stripe } from "stripe";
|
||||
|
||||
import { handleCheckoutSessionCompleted } from "./webhookHandlers/handleCheckoutCompleted.js";
|
||||
import { handleSubscriptionUpdated } from "./webhookHandlers/handleSubUpdated.js";
|
||||
import { handleSubDeleted } from "./webhookHandlers/handleSubDeleted.js";
|
||||
@@ -10,7 +13,6 @@ import { getStripeWebhookSecret } from "@/internal/orgs/orgUtils.js";
|
||||
import { handleInvoicePaid } from "./webhookHandlers/handleInvoicePaid.js";
|
||||
import { handleRequestError } from "@/utils/errorUtils.js";
|
||||
import { handleInvoiceCreated } from "./webhookHandlers/handleInvoiceCreated/handleInvoiceCreated.js";
|
||||
import chalk from "chalk";
|
||||
import { handleInvoiceFinalized } from "./webhookHandlers/handleInvoiceFinalized.js";
|
||||
import { handleSubscriptionScheduleCanceled } from "./webhookHandlers/handleSubScheduleCanceled.js";
|
||||
import { createLogtailWithContext } from "../logtail/logtailUtils.js";
|
||||
@@ -18,7 +20,7 @@ import { handleCusDiscountDeleted } from "./webhookHandlers/handleCusDiscountDel
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { createStripeCli } from "./utils.js";
|
||||
|
||||
export const stripeWebhookRouter = express.Router();
|
||||
export const stripeWebhookRouter: Router = express.Router();
|
||||
|
||||
const logStripeWebhook = ({
|
||||
req,
|
||||
|
||||
23
server/src/external/supabase/safeSb.ts
vendored
Normal file
23
server/src/external/supabase/safeSb.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
import { logger } from "../logtail/logtailUtils.js";
|
||||
|
||||
export function safeSb<T extends (...args: any[]) => any>({
|
||||
fn,
|
||||
action,
|
||||
}: {
|
||||
fn: T;
|
||||
action: string;
|
||||
}): (...args: Parameters<T>) => Promise<ReturnType<T> | undefined> {
|
||||
return async (...args: Parameters<T>) => {
|
||||
if (!process.env.SUPABASE_URL || !process.env.SUPABASE_SERVICE_KEY) {
|
||||
logger.warn(
|
||||
`SUPABASE_URL or SUPABASE_SERVICE_KEY is not set, skipping ${action}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
return await fn(...args);
|
||||
} catch (error) {
|
||||
logger.error(`Error ${action}: ${error}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
42
server/src/external/supabase/storageUtils.ts
vendored
Normal file
42
server/src/external/supabase/storageUtils.ts
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { createSupabaseClient } from "../supabaseUtils.js";
|
||||
|
||||
export const uploadFile = async ({
|
||||
path,
|
||||
file,
|
||||
contentType,
|
||||
}: {
|
||||
path: string;
|
||||
file: Buffer;
|
||||
contentType?: string;
|
||||
}) => {
|
||||
const sb = createSupabaseClient();
|
||||
|
||||
const { data, error } = await sb.storage.from("autumn").upload(path, file, {
|
||||
upsert: true,
|
||||
contentType,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getUploadUrl = async ({ path }: { path: string }) => {
|
||||
const sb = createSupabaseClient();
|
||||
await sb.storage.from("autumn").remove([path]);
|
||||
|
||||
const { data, error } = await sb.storage
|
||||
.from("autumn")
|
||||
.createSignedUploadUrl(path, {
|
||||
upsert: true,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
@@ -1,29 +1,32 @@
|
||||
import { clearOrgCache } from "@/internal/orgs/orgUtils/clearOrgCache.js";
|
||||
import { createSupabaseClient } from "../supabaseUtils.js";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { safeSb } from "./safeSb.js";
|
||||
|
||||
export const subscribeToOrgUpdates = ({ db }: { db: DrizzleCli }) => {
|
||||
try {
|
||||
const sb = createSupabaseClient();
|
||||
sb.channel("table-db-changes")
|
||||
.on(
|
||||
"postgres_changes",
|
||||
{
|
||||
event: "UPDATE",
|
||||
schema: "public",
|
||||
table: "organizations",
|
||||
},
|
||||
async (payload) => {
|
||||
// await clearOrgCache(payload.new.id);
|
||||
try {
|
||||
await clearOrgCache({ db, orgId: payload.new.id });
|
||||
} catch (error) {
|
||||
console.warn("Error clearing org cache:", error);
|
||||
}
|
||||
},
|
||||
)
|
||||
.subscribe();
|
||||
} catch (error) {
|
||||
console.warn("Error subscribing to org updates:", error);
|
||||
}
|
||||
};
|
||||
export const subscribeToOrgUpdates = safeSb({
|
||||
fn: ({ db }: { db: DrizzleCli }) => {
|
||||
try {
|
||||
const sb = createSupabaseClient();
|
||||
sb.channel("table-db-changes")
|
||||
.on(
|
||||
"postgres_changes",
|
||||
{
|
||||
event: "UPDATE",
|
||||
schema: "public",
|
||||
table: "organizations",
|
||||
},
|
||||
async (payload) => {
|
||||
try {
|
||||
await clearOrgCache({ db, orgId: payload.new.id });
|
||||
} catch (error) {
|
||||
console.warn("Error clearing org cache:", error);
|
||||
}
|
||||
},
|
||||
)
|
||||
.subscribe();
|
||||
} catch (error) {
|
||||
console.warn("Error subscribing to org updates:", error);
|
||||
}
|
||||
},
|
||||
action: "subscribe to org updates",
|
||||
});
|
||||
|
||||
68
server/src/external/svix/svixHelpers.ts
vendored
Normal file
68
server/src/external/svix/svixHelpers.ts
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
import { AppEnv, Organization } from "@autumn/shared";
|
||||
import { createSvixCli, getSvixAppId, safeSvix } from "./svixUtils.js";
|
||||
|
||||
export const createSvixApp = safeSvix({
|
||||
fn: async ({
|
||||
name,
|
||||
orgId,
|
||||
env,
|
||||
}: {
|
||||
name: string;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
}) => {
|
||||
const svix = createSvixCli();
|
||||
const app = await svix.application.create({
|
||||
name,
|
||||
metadata: {
|
||||
org_id: orgId,
|
||||
env,
|
||||
},
|
||||
});
|
||||
return app;
|
||||
},
|
||||
action: "createSvixApp",
|
||||
});
|
||||
|
||||
export const deleteSvixApp = safeSvix({
|
||||
fn: async ({ appId }: { appId: string }) => {
|
||||
const svix = createSvixCli();
|
||||
await svix.application.delete(appId);
|
||||
},
|
||||
action: "deleteSvixApp",
|
||||
});
|
||||
|
||||
export const sendSvixEvent = safeSvix({
|
||||
fn: async ({
|
||||
org,
|
||||
env,
|
||||
eventType,
|
||||
data,
|
||||
}: {
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
eventType: string;
|
||||
data: any;
|
||||
}) => {
|
||||
const svix = createSvixCli();
|
||||
return await svix.message.create(getSvixAppId({ org, env }), {
|
||||
eventType,
|
||||
payload: {
|
||||
type: eventType,
|
||||
data,
|
||||
},
|
||||
});
|
||||
},
|
||||
action: "sendSvixEvent",
|
||||
});
|
||||
|
||||
export const getSvixDashboardUrl = safeSvix({
|
||||
fn: async ({ org, env }: { org: Organization; env: AppEnv }) => {
|
||||
const appId = getSvixAppId({ org, env });
|
||||
|
||||
const svix = createSvixCli();
|
||||
const dashboard = await svix.authentication.appPortalAccess(appId, {});
|
||||
return dashboard.url;
|
||||
},
|
||||
action: "getSvixDashboardUrl",
|
||||
});
|
||||
81
server/src/external/svix/svixUtils.ts
vendored
81
server/src/external/svix/svixUtils.ts
vendored
@@ -1,11 +1,32 @@
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import { Organization } from "@autumn/shared";
|
||||
import { Svix } from "svix";
|
||||
import { logger } from "../logtail/logtailUtils.js";
|
||||
|
||||
export const createSvixCli = () => {
|
||||
return new Svix(process.env.SVIX_API_KEY as string);
|
||||
};
|
||||
|
||||
export function safeSvix<T extends (...args: any[]) => any>({
|
||||
fn,
|
||||
action,
|
||||
}: {
|
||||
fn: T;
|
||||
action: string;
|
||||
}): (...args: Parameters<T>) => Promise<ReturnType<T> | undefined> {
|
||||
return async (...args: Parameters<T>) => {
|
||||
if (!process.env.SVIX_API_KEY) {
|
||||
logger.warn(`SVIX_API_KEY is not set, skipping ${action}`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
return await fn(...args);
|
||||
} catch (error) {
|
||||
logger.error(`Error ${action}: ${error}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const getSvixAppId = ({
|
||||
org,
|
||||
env,
|
||||
@@ -18,63 +39,3 @@ export const getSvixAppId = ({
|
||||
? svixConfig.live_app_id
|
||||
: svixConfig.sandbox_app_id;
|
||||
};
|
||||
|
||||
export const createSvixApp = async ({
|
||||
name,
|
||||
orgId,
|
||||
env,
|
||||
}: {
|
||||
name: string;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
}) => {
|
||||
const svix = createSvixCli();
|
||||
const app = await svix.application.create({
|
||||
name,
|
||||
metadata: {
|
||||
org_id: orgId,
|
||||
env,
|
||||
},
|
||||
});
|
||||
return app;
|
||||
};
|
||||
|
||||
export const deleteSvixApp = async ({ appId }: { appId: string }) => {
|
||||
const svix = createSvixCli();
|
||||
await svix.application.delete(appId);
|
||||
};
|
||||
|
||||
export const sendSvixEvent = async ({
|
||||
org,
|
||||
env,
|
||||
eventType,
|
||||
data,
|
||||
}: {
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
eventType: string;
|
||||
data: any;
|
||||
}) => {
|
||||
const svix = createSvixCli();
|
||||
return await svix.message.create(getSvixAppId({ org, env }), {
|
||||
eventType,
|
||||
payload: {
|
||||
type: eventType,
|
||||
data,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getSvixDashboardUrl = async ({
|
||||
org,
|
||||
env,
|
||||
}: {
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
}) => {
|
||||
const appId = getSvixAppId({ org, env });
|
||||
|
||||
const svix = createSvixCli();
|
||||
const dashboard = await svix.authentication.appPortalAccess(appId, {});
|
||||
return dashboard.url;
|
||||
};
|
||||
|
||||
59
server/src/external/unkeyUtils.ts
vendored
59
server/src/external/unkeyUtils.ts
vendored
@@ -1,59 +0,0 @@
|
||||
// import { AppEnv } from "@autumn/shared";
|
||||
// import { Unkey } from "@unkey/api";
|
||||
|
||||
// const UNKEY_API_ID = "api_2fcMv43jiAbBySAgDubovfpVUABP";
|
||||
// const createUnkeyCli = () => {
|
||||
// return new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! });
|
||||
// };
|
||||
|
||||
// export const createKey = async ({
|
||||
// env,
|
||||
// name,
|
||||
// ownerId,
|
||||
// prefix,
|
||||
// meta,
|
||||
// }: {
|
||||
// env: AppEnv;
|
||||
// name: string;
|
||||
// ownerId: string;
|
||||
// prefix: string;
|
||||
// meta: any;
|
||||
// }) => {
|
||||
// const unkey = createUnkeyCli();
|
||||
// const key = await unkey.keys.create({
|
||||
// apiId: UNKEY_API_ID,
|
||||
// name,
|
||||
// prefix,
|
||||
// ownerId,
|
||||
// meta,
|
||||
// environment: env,
|
||||
// });
|
||||
// return key;
|
||||
// };
|
||||
|
||||
// export const updateKey = async (keyId: string, meta: any) => {
|
||||
// const unkey = createUnkeyCli();
|
||||
// await unkey.keys.update({
|
||||
// keyId,
|
||||
// meta,
|
||||
// });
|
||||
// };
|
||||
|
||||
// export const deleteKey = async (keyId: string) => {
|
||||
// const unkey = createUnkeyCli();
|
||||
// await unkey.keys.delete({ keyId });
|
||||
// };
|
||||
|
||||
// export const validateApiKey = async (apiKey: string) => {
|
||||
// const unkey = createUnkeyCli();
|
||||
// const { result, error } = await unkey.keys.verify({
|
||||
// apiId: UNKEY_API_ID,
|
||||
// key: apiKey,
|
||||
// });
|
||||
|
||||
// if (error || !result.valid) {
|
||||
// throw new Error("Invalid API key");
|
||||
// }
|
||||
|
||||
// return result;
|
||||
// };
|
||||
303
server/src/external/webhooks/clerkWebhooks.ts
vendored
303
server/src/external/webhooks/clerkWebhooks.ts
vendored
@@ -2,249 +2,96 @@ import { Request, Response } from "express";
|
||||
import { Webhook } from "svix";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { createClerkCli } from "../clerkUtils.js";
|
||||
import { sendOnboardingEmail } from "./sendOnboardingEmail.js";
|
||||
|
||||
import { AppEnv } from "autumn-js";
|
||||
|
||||
import { deleteSvixApp } from "../svix/svixUtils.js";
|
||||
import {
|
||||
deleteStripeWebhook,
|
||||
initOrgSvixApps,
|
||||
} from "@/internal/orgs/orgUtils.js";
|
||||
import { deleteSvixApp } from "@/external/svix/svixHelpers.js";
|
||||
import { deleteStripeWebhook } from "@/internal/orgs/orgUtils.js";
|
||||
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { constructOrg } from "@/internal/orgs/orgUtils.js";
|
||||
import { createOnboardingProducts } from "@/internal/orgs/onboarding/createOnboardingProducts.js";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { Organization, organizations } from "@autumn/shared";
|
||||
|
||||
const verifyClerkWebhook = async (req: Request, res: Response) => {
|
||||
const wh = new Webhook(process.env.CLERK_SIGNING_SECRET!);
|
||||
// const verifyClerkWebhook = async (req: Request, res: Response) => {
|
||||
// const wh = new Webhook(process.env.CLERK_SIGNING_SECRET!);
|
||||
|
||||
const headers = req.headers;
|
||||
const payload = req.body;
|
||||
// const headers = req.headers;
|
||||
// const payload = req.body;
|
||||
|
||||
const svix_id = headers["svix-id"];
|
||||
const svix_timestamp = headers["svix-timestamp"];
|
||||
const svix_signature = headers["svix-signature"];
|
||||
// const svix_id = headers["svix-id"];
|
||||
// const svix_timestamp = headers["svix-timestamp"];
|
||||
// const svix_signature = headers["svix-signature"];
|
||||
|
||||
if (!svix_id || !svix_timestamp || !svix_signature) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "Error: Missing svix headers",
|
||||
});
|
||||
return;
|
||||
}
|
||||
// if (!svix_id || !svix_timestamp || !svix_signature) {
|
||||
// res.status(400).json({
|
||||
// success: false,
|
||||
// message: "Error: Missing svix headers",
|
||||
// });
|
||||
// return;
|
||||
// }
|
||||
|
||||
let evt: any;
|
||||
try {
|
||||
evt = wh.verify(payload, {
|
||||
"svix-id": svix_id as string,
|
||||
"svix-timestamp": svix_timestamp as string,
|
||||
"svix-signature": svix_signature as string,
|
||||
});
|
||||
} catch (err) {
|
||||
console.log("Error: Could not verify webhook");
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "Error: Could not verify webhook",
|
||||
});
|
||||
return;
|
||||
}
|
||||
// let evt: any;
|
||||
// try {
|
||||
// evt = wh.verify(payload, {
|
||||
// "svix-id": svix_id as string,
|
||||
// "svix-timestamp": svix_timestamp as string,
|
||||
// "svix-signature": svix_signature as string,
|
||||
// });
|
||||
// } catch (err) {
|
||||
// console.log("Error: Could not verify webhook");
|
||||
// res.status(400).json({
|
||||
// success: false,
|
||||
// message: "Error: Could not verify webhook",
|
||||
// });
|
||||
// return;
|
||||
// }
|
||||
|
||||
return evt;
|
||||
};
|
||||
// return evt;
|
||||
// };
|
||||
|
||||
export const handleClerkWebhook = async (req: any, res: any) => {
|
||||
let event = await verifyClerkWebhook(req, res);
|
||||
// export const handleClerkWebhook = async (req: any, res: any) => {
|
||||
// let event = await verifyClerkWebhook(req, res);
|
||||
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
// if (!event) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
const eventType = event.type;
|
||||
const eventData = event.data;
|
||||
// const eventType = event.type;
|
||||
// const eventData = event.data;
|
||||
|
||||
try {
|
||||
switch (eventType) {
|
||||
case "organization.created":
|
||||
await saveOrgToDB({
|
||||
db: req.db,
|
||||
id: eventData.id,
|
||||
slug: eventData.slug,
|
||||
});
|
||||
break;
|
||||
// try {
|
||||
// switch (eventType) {
|
||||
// case "organization.created":
|
||||
// await saveOrgToDB({
|
||||
// db: req.db,
|
||||
// id: eventData.id,
|
||||
// slug: eventData.slug,
|
||||
// createdAt: eventData.created_at,
|
||||
// });
|
||||
// break;
|
||||
|
||||
case "organization.deleted":
|
||||
await handleOrgDeleted({
|
||||
db: req.db,
|
||||
eventData,
|
||||
});
|
||||
break;
|
||||
// case "organization.deleted":
|
||||
// await handleOrgDeleted({
|
||||
// db: req.db,
|
||||
// eventData,
|
||||
// });
|
||||
// break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
handleRequestError({
|
||||
req,
|
||||
error,
|
||||
res,
|
||||
action: "Handle Clerk Webhook",
|
||||
});
|
||||
return;
|
||||
}
|
||||
// default:
|
||||
// break;
|
||||
// }
|
||||
// } catch (error) {
|
||||
// handleRequestError({
|
||||
// req,
|
||||
// error,
|
||||
// res,
|
||||
// action: "Handle Clerk Webhook",
|
||||
// });
|
||||
// return;
|
||||
// }
|
||||
|
||||
return void res.status(200).json({
|
||||
success: true,
|
||||
message: "Webhook received",
|
||||
});
|
||||
};
|
||||
|
||||
export const saveOrgToDB = async ({
|
||||
db,
|
||||
id,
|
||||
slug,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
id: string;
|
||||
slug: string;
|
||||
}) => {
|
||||
console.log(`Handling organization.created: ${slug} (${id})`);
|
||||
|
||||
try {
|
||||
// 2. Insert org
|
||||
await OrgService.insert({
|
||||
db,
|
||||
org: constructOrg({
|
||||
id,
|
||||
slug,
|
||||
}),
|
||||
});
|
||||
|
||||
// 1. Create svix webhoooks
|
||||
const { sandboxApp, liveApp } = await initOrgSvixApps({
|
||||
slug,
|
||||
id,
|
||||
});
|
||||
|
||||
await OrgService.update({
|
||||
db,
|
||||
orgId: id,
|
||||
updates: {
|
||||
svix_config: { sandbox_app_id: sandboxApp.id, live_app_id: liveApp.id },
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Created svix webhooks for org ${id}`);
|
||||
} catch (error: any) {
|
||||
if (error?.data && error.data.code == "23505") {
|
||||
console.error(
|
||||
`Org ${id} already exists in Supabase -- skipping creationg`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
console.error(
|
||||
`Failed to insert org. Code: ${error.code}, message: ${error.message}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const batch = [];
|
||||
|
||||
try {
|
||||
// batch.push(
|
||||
// createOnboardingProducts({
|
||||
// db,
|
||||
// orgId: eventData.id,
|
||||
// }),
|
||||
// );
|
||||
|
||||
batch.push(
|
||||
sendOnboardingEmail({
|
||||
orgId: id,
|
||||
clerkCli: createClerkCli(),
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.all(batch);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Failed to create default products or send onboarding email",
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOrgDeleted = async ({
|
||||
db,
|
||||
eventData,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
eventData: any;
|
||||
}) => {
|
||||
// 1. Delete svix webhooks
|
||||
|
||||
try {
|
||||
console.log(`Handling organization.deleted: (${eventData.id})`);
|
||||
|
||||
const org = (await db.query.organizations.findFirst({
|
||||
where: eq(organizations.id, eventData.id),
|
||||
})) as unknown as Organization;
|
||||
|
||||
if (!org) {
|
||||
throw new RecaseError({
|
||||
message: `Clerk webhook, tried deleting org ${eventData.slug} but not found`,
|
||||
code: "org_not_found",
|
||||
statusCode: 404,
|
||||
});
|
||||
}
|
||||
|
||||
console.log("1. Deleting svix webhooks");
|
||||
const batch = [];
|
||||
if (org.svix_config?.sandbox_app_id) {
|
||||
batch.push(
|
||||
deleteSvixApp({
|
||||
appId: org.svix_config.sandbox_app_id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (org.svix_config?.live_app_id) {
|
||||
batch.push(
|
||||
deleteSvixApp({
|
||||
appId: org.svix_config.live_app_id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(batch);
|
||||
|
||||
// 2. Delete stripe webhooks
|
||||
console.log("2. Deleting stripe webhooks");
|
||||
if (org.stripe_config) {
|
||||
await deleteStripeWebhook({
|
||||
org: org,
|
||||
env: AppEnv.Sandbox,
|
||||
});
|
||||
|
||||
await deleteStripeWebhook({
|
||||
org: org,
|
||||
env: AppEnv.Live,
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Delete org
|
||||
console.log("3. Deleting org");
|
||||
await OrgService.delete({
|
||||
db,
|
||||
orgId: eventData.id,
|
||||
});
|
||||
|
||||
console.log(`Deleted org ${org.slug} (${org.id})`);
|
||||
} catch (error) {
|
||||
console.log("Failed to delete organization", error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
// return void res.status(200).json({
|
||||
// success: true,
|
||||
// message: "Webhook received",
|
||||
// });
|
||||
// };
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { ClerkClient } from "@clerk/express";
|
||||
import { sendHtmlEmail, sendTextEmail } from "../resend/resendUtils.js";
|
||||
|
||||
const getWelcomeEmailBody = (userFirstName: string) => {
|
||||
return `
|
||||
<p>Hey ${userFirstName} :)</p>
|
||||
|
||||
<p>Just wanted to say thank you for signing up to Autumn!</p>
|
||||
|
||||
<p>I'm curious--how did you hear about us? Also are you just looking around or do you have a specific use case I can help you with?</p>
|
||||
|
||||
<p>Whatever the reason, anything you need I'm here to help.</p>
|
||||
|
||||
<p>Ayush<br>
|
||||
Co-founder, Autumn</p>
|
||||
|
||||
<p>Oh, and join our <a href="https://discord.gg/STqxY92zuS">Discord community</a> to connect with us and other users</p>
|
||||
`;
|
||||
};
|
||||
|
||||
export const sendOnboardingEmail = async ({
|
||||
orgId,
|
||||
clerkCli,
|
||||
}: {
|
||||
orgId: string;
|
||||
clerkCli: ClerkClient;
|
||||
}) => {
|
||||
const memberships =
|
||||
await clerkCli.organizations.getOrganizationMembershipList({
|
||||
organizationId: orgId,
|
||||
});
|
||||
|
||||
for (let membership of memberships.data) {
|
||||
if (!membership.publicUserData) break;
|
||||
|
||||
const user = await clerkCli.users.getUser(membership.publicUserData.userId);
|
||||
const email = user.primaryEmailAddress?.emailAddress;
|
||||
|
||||
if (!email) break;
|
||||
|
||||
console.log("Sending onboarding email to", email);
|
||||
await sendHtmlEmail({
|
||||
to: email,
|
||||
subject: "Anything I can help with?",
|
||||
body: getWelcomeEmailBody(user.firstName ?? "there"),
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
13
server/src/external/webhooks/webhooksRouter.ts
vendored
13
server/src/external/webhooks/webhooksRouter.ts
vendored
@@ -1,19 +1,12 @@
|
||||
import express from "express";
|
||||
import bodyParser from "body-parser";
|
||||
import { handleClerkWebhook } from "./clerkWebhooks.js";
|
||||
import express, { Router } from "express";
|
||||
|
||||
import { stripeWebhookRouter } from "../stripe/stripeWebhooks.js";
|
||||
import { autumnWebhookRouter } from "../autumn/autumnWebhookRouter.js";
|
||||
|
||||
const webhooksRouter = express.Router();
|
||||
const webhooksRouter: Router = express.Router();
|
||||
|
||||
webhooksRouter.use("/stripe", stripeWebhookRouter);
|
||||
|
||||
webhooksRouter.use("/autumn", autumnWebhookRouter);
|
||||
|
||||
webhooksRouter.post(
|
||||
"/clerk",
|
||||
bodyParser.raw({ type: "application/json" }),
|
||||
handleClerkWebhook
|
||||
);
|
||||
|
||||
export default webhooksRouter;
|
||||
|
||||
@@ -8,56 +8,75 @@ import mainRouter from "./internal/mainRouter.js";
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import chalk from "chalk";
|
||||
import { apiRouter } from "./internal/api/apiRouter.js";
|
||||
|
||||
import http from "http";
|
||||
import webhooksRouter from "./external/webhooks/webhooksRouter.js";
|
||||
|
||||
import { initLogger } from "./errors/logger.js";
|
||||
import { apiRouter } from "./internal/api/apiRouter.js";
|
||||
import { QueueManager } from "./queue/QueueManager.js";
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import { createSupabaseClient } from "./external/supabaseUtils.js";
|
||||
import {
|
||||
createLogtail,
|
||||
createLogtailAll,
|
||||
} from "./external/logtail/logtailUtils.js";
|
||||
import { createLogtail } from "./external/logtail/logtailUtils.js";
|
||||
import { CacheManager } from "./external/caching/CacheManager.js";
|
||||
import { initDrizzle } from "./db/initDrizzle.js";
|
||||
import { createPosthogCli } from "./external/posthog/createPosthogCli.js";
|
||||
|
||||
import { logtailAll, logger } from "./external/logtail/logtailUtils.js";
|
||||
import { createPosthogCli } from "./external/posthog/createPosthogCli.js";
|
||||
import { generateId } from "./utils/genUtils.js";
|
||||
import { subscribeToOrgUpdates } from "./external/supabase/subscribeToOrgUpdates.js";
|
||||
import { client, db } from "./db/initDrizzle.js";
|
||||
import { toNodeHandler } from "better-auth/node";
|
||||
import { auth } from "./utils/auth.js";
|
||||
import { checkEnvVars } from "./utils/initUtils.js";
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error(`DATABASE_URL is not set`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { db, client } = initDrizzle({ maxConnections: 10 });
|
||||
|
||||
checkEnvVars();
|
||||
const init = async () => {
|
||||
const app = express();
|
||||
const logger = initLogger();
|
||||
|
||||
// Check if this blocks API calls...
|
||||
app.use(
|
||||
cors({
|
||||
origin: [
|
||||
"http://localhost:3000",
|
||||
"https://app.useautumn.com",
|
||||
"https://*.useautumn.com",
|
||||
process.env.CLIENT_URL || "",
|
||||
],
|
||||
credentials: true,
|
||||
allowedHeaders: [
|
||||
"app_env",
|
||||
"x-api-version",
|
||||
"Authorization",
|
||||
"Content-Type",
|
||||
"Accept",
|
||||
"Origin",
|
||||
"X-API-Version",
|
||||
"X-Requested-With",
|
||||
"Access-Control-Request-Method",
|
||||
"Access-Control-Request-Headers",
|
||||
"Cache-Control",
|
||||
"If-Match",
|
||||
"If-None-Match",
|
||||
"If-Modified-Since",
|
||||
"If-Unmodified-Since",
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
app.all("/api/auth/*", toNodeHandler(auth));
|
||||
|
||||
const server = http.createServer(app);
|
||||
const posthog = createPosthogCli();
|
||||
|
||||
server.keepAliveTimeout = 120000; // 120 seconds
|
||||
server.headersTimeout = 120000; // 120 seconds should be >= keepAliveTimeout
|
||||
|
||||
await QueueManager.getInstance(); // initialize the queue manager
|
||||
await CacheManager.getInstance();
|
||||
|
||||
const supabaseClient = createSupabaseClient();
|
||||
|
||||
// Optional services
|
||||
const logtailAll = createLogtailAll();
|
||||
const posthog = createPosthogCli();
|
||||
subscribeToOrgUpdates({ db });
|
||||
|
||||
app.use((req: any, res: any, next: any) => {
|
||||
req.sb = supabaseClient;
|
||||
req.db = db;
|
||||
|
||||
req.logger = logger;
|
||||
req.logtailAll = logtailAll;
|
||||
req.env = req.env = req.headers["app_env"] || AppEnv.Sandbox;
|
||||
|
||||
req.db = db;
|
||||
req.logtailAll = logtailAll;
|
||||
req.posthog = posthog;
|
||||
|
||||
@@ -69,7 +88,7 @@ const init = async () => {
|
||||
headersClone.authorization = undefined;
|
||||
headersClone.Authorization = undefined;
|
||||
|
||||
logtailAll.info(`${req.method} ${req.originalUrl}`, {
|
||||
logtailAll?.info(`${req.method} ${req.originalUrl}`, {
|
||||
url: req.originalUrl,
|
||||
method: req.method,
|
||||
headers: headersClone,
|
||||
@@ -77,9 +96,8 @@ const init = async () => {
|
||||
});
|
||||
|
||||
req.logtail = createLogtail();
|
||||
req.logger = req.logtail;
|
||||
} catch (error) {
|
||||
req.logtail = logtailAll; // fallback
|
||||
req.logtail = logger; // fallback
|
||||
console.error(`Error creating req.logtail`);
|
||||
console.error(error);
|
||||
}
|
||||
@@ -91,8 +109,6 @@ const init = async () => {
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(cors());
|
||||
|
||||
app.use("/webhooks", webhooksRouter);
|
||||
|
||||
app.use((req: any, res, next) => {
|
||||
@@ -142,14 +158,7 @@ if (process.env.NODE_ENV === "development") {
|
||||
}
|
||||
|
||||
cluster.on("exit", (worker, code, signal) => {
|
||||
try {
|
||||
let logtail = createLogtail();
|
||||
logtail.error(`WORKER DIED: ${worker.process.pid}`);
|
||||
logtail.flush();
|
||||
} catch (error) {
|
||||
console.log("Error sending log to logtail", error);
|
||||
}
|
||||
// LOG in Render
|
||||
logger.error(`WORKER DIED: ${worker.process.pid}`);
|
||||
cluster.fork();
|
||||
});
|
||||
} else {
|
||||
@@ -175,3 +184,21 @@ async function gracefulShutdown() {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Close connections gracefully?
|
||||
const closeConnections = async () => {
|
||||
console.log("Closing connections");
|
||||
await client.end();
|
||||
};
|
||||
|
||||
process.on("SIGTERM", async () => {
|
||||
console.log("SIGTERM received, shutting down gracefully");
|
||||
await closeConnections();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on("SIGINT", async () => {
|
||||
console.log("SIGINT received, shutting down gracefully");
|
||||
await closeConnections();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
158
server/src/internal/admin/adminRouter.ts
Normal file
158
server/src/internal/admin/adminRouter.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { handleFrontendReqError } from "@/utils/errorUtils.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { member, organizations, user } from "@autumn/shared";
|
||||
import { and, desc, eq, gt, gte, ilike, inArray, lt, or } from "drizzle-orm";
|
||||
import { Router } from "express";
|
||||
|
||||
export const adminRouter: Router = Router();
|
||||
|
||||
adminRouter.get("/users", async (req: any, res: any) => {
|
||||
try {
|
||||
const { db } = req as ExtendedRequest;
|
||||
|
||||
let { sortKey, search, after, before } = req.query;
|
||||
|
||||
if (after) {
|
||||
after = {
|
||||
id: after.split(",")[0],
|
||||
createdAt: new Date(after.split(",")[1]),
|
||||
};
|
||||
} else if (before) {
|
||||
before = {
|
||||
id: before.split(",")[0],
|
||||
createdAt: new Date(before.split(",")[1]),
|
||||
};
|
||||
}
|
||||
|
||||
const users = await db
|
||||
.select()
|
||||
.from(user)
|
||||
.where(
|
||||
and(
|
||||
search
|
||||
? or(
|
||||
ilike(user.email, `%${search as string}%`),
|
||||
ilike(user.name, `%${search as string}%`),
|
||||
ilike(user.id, `%${search as string}%`),
|
||||
)
|
||||
: undefined,
|
||||
after
|
||||
? or(
|
||||
lt(user.createdAt, after.createdAt),
|
||||
or(
|
||||
and(
|
||||
eq(user.createdAt, after.createdAt),
|
||||
lt(user.id, after.id),
|
||||
),
|
||||
),
|
||||
)
|
||||
: undefined,
|
||||
before
|
||||
? or(
|
||||
gte(user.createdAt, before.createdAt),
|
||||
or(
|
||||
and(
|
||||
eq(user.createdAt, before.createdAt),
|
||||
gt(user.id, before.id),
|
||||
),
|
||||
),
|
||||
)
|
||||
: undefined,
|
||||
),
|
||||
)
|
||||
.orderBy(desc(user.createdAt), desc(user.id))
|
||||
.limit(21);
|
||||
|
||||
res.json({
|
||||
rows: users.slice(0, 20),
|
||||
hasNextPage: users.length > 20,
|
||||
});
|
||||
} catch (error) {
|
||||
handleFrontendReqError({
|
||||
res,
|
||||
req,
|
||||
error,
|
||||
action: "admin: search users",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
adminRouter.get("/orgs", async (req: any, res: any) => {
|
||||
try {
|
||||
const { db } = req as ExtendedRequest;
|
||||
|
||||
let { search, after, before } = req.query;
|
||||
|
||||
if (after) {
|
||||
after = {
|
||||
id: after.split(",")[0],
|
||||
createdAt: new Date(after.split(",")[1]),
|
||||
};
|
||||
} else if (before) {
|
||||
before = {
|
||||
id: before.split(",")[0],
|
||||
createdAt: new Date(before.split(",")[1]),
|
||||
};
|
||||
}
|
||||
|
||||
const orgs = await db
|
||||
.select()
|
||||
.from(organizations)
|
||||
.where(
|
||||
and(
|
||||
search
|
||||
? or(ilike(organizations.name, `%${search as string}%`))
|
||||
: undefined,
|
||||
after
|
||||
? or(
|
||||
lt(organizations.createdAt, after.createdAt),
|
||||
or(
|
||||
and(
|
||||
eq(organizations.createdAt, after.createdAt),
|
||||
lt(organizations.id, after.id),
|
||||
),
|
||||
),
|
||||
)
|
||||
: undefined,
|
||||
before
|
||||
? or(
|
||||
gte(organizations.createdAt, before.createdAt),
|
||||
or(
|
||||
and(
|
||||
eq(organizations.createdAt, before.createdAt),
|
||||
gt(organizations.id, before.id),
|
||||
),
|
||||
),
|
||||
)
|
||||
: undefined,
|
||||
),
|
||||
)
|
||||
.orderBy(desc(organizations.createdAt), desc(organizations.id))
|
||||
.limit(21);
|
||||
|
||||
let orgIds = orgs.map((org) => org.id);
|
||||
|
||||
let memberships = await db
|
||||
.select()
|
||||
.from(member)
|
||||
.leftJoin(user, eq(member.userId, user.id))
|
||||
.where(inArray(member.organizationId, orgIds));
|
||||
|
||||
res.json({
|
||||
rows: orgs.slice(0, 20).map((org) => ({
|
||||
...org,
|
||||
users: memberships
|
||||
.filter((membership) => membership.member.organizationId === org.id)
|
||||
.map((membership) => membership.user?.email),
|
||||
})),
|
||||
hasNextPage: orgs.length > 20,
|
||||
});
|
||||
} catch (error) {
|
||||
handleFrontendReqError({
|
||||
res,
|
||||
req,
|
||||
error,
|
||||
action: "admin: search orgs",
|
||||
});
|
||||
}
|
||||
});
|
||||
14
server/src/internal/admin/adminUtils/userAnalytics.ts
Normal file
14
server/src/internal/admin/adminUtils/userAnalytics.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { user } from "@autumn/shared";
|
||||
import { count } from "drizzle-orm";
|
||||
|
||||
// Required stats (by interval):
|
||||
// 1. User count
|
||||
// 2. Retained count
|
||||
// 3. Churned count
|
||||
|
||||
export const getUserCount = async ({ db }: { db: DrizzleCli }) => {
|
||||
const userCount = await db.select({ count: count() }).from(user);
|
||||
return userCount[0].count;
|
||||
};
|
||||
34
server/src/internal/admin/withAdminAuth.ts
Normal file
34
server/src/internal/admin/withAdminAuth.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { ErrCode } from "@/errors/errCodes.js";
|
||||
import { auth } from "@/utils/auth.js";
|
||||
import { ADMIN_USER_IDs } from "@/utils/constants.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { NextFunction } from "express";
|
||||
|
||||
export const withAdminAuth = async (req: any, res: any, next: NextFunction) => {
|
||||
const { logtail: logger, userId } = req as ExtendedRequest;
|
||||
|
||||
try {
|
||||
const data = await auth.api.getSession({
|
||||
headers: req.headers,
|
||||
});
|
||||
|
||||
if (
|
||||
!ADMIN_USER_IDs.includes(data?.session?.userId || "") &&
|
||||
!ADMIN_USER_IDs.includes(data?.session?.impersonatedBy || "")
|
||||
) {
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
code: ErrCode.InvalidRequest,
|
||||
message: "Method not allowed",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Admin auth passed");
|
||||
|
||||
next();
|
||||
} catch (error: any) {
|
||||
logger.error(`Admin req failed: ${error.message}`);
|
||||
return res.status(400).json();
|
||||
}
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
Organization,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import { sendSvixEvent } from "../../../external/svix/svixUtils.js";
|
||||
import { sendSvixEvent } from "@/external/svix/svixHelpers.js";
|
||||
import { CusService } from "@/internal/customers/CusService.js";
|
||||
|
||||
import { getCustomerDetails } from "@/internal/customers/cusUtils/getCustomerDetails.js";
|
||||
@@ -205,7 +205,7 @@ export const handleProductsUpdated = async ({
|
||||
}
|
||||
|
||||
// 2. Send Svix event
|
||||
const res = await sendSvixEvent({
|
||||
await sendSvixEvent({
|
||||
org,
|
||||
env,
|
||||
eventType: "customer.products.updated",
|
||||
|
||||
@@ -21,18 +21,12 @@ import { analyticsMiddleware } from "@/middleware/analyticsMiddleware.js";
|
||||
import rewardRouter from "./rewards/rewardRouter.js";
|
||||
import expireRouter from "../customers/expire/expireRouter.js";
|
||||
|
||||
const apiRouter = Router();
|
||||
const apiRouter: Router = Router();
|
||||
|
||||
apiRouter.use(apiAuthMiddleware);
|
||||
apiRouter.use(pricingMiddleware);
|
||||
apiRouter.use(analyticsMiddleware);
|
||||
|
||||
apiRouter.get("/auth", (req: any, res) => {
|
||||
res.json({
|
||||
message: `Authenticated -- Hello ${req.minOrg?.slug}!`,
|
||||
});
|
||||
});
|
||||
|
||||
apiRouter.use("/customers", cusRouter);
|
||||
apiRouter.use("/invoices", invoiceRouter);
|
||||
apiRouter.use("/products", productApiRouter);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { FullCusProduct, CusProductStatus, ProductV2 } from "@autumn/shared";
|
||||
import { Router } from "express";
|
||||
|
||||
export const componentRouter = Router();
|
||||
export const componentRouter: Router = Router();
|
||||
|
||||
componentRouter.get("/pricing_table", async (req: any, res) =>
|
||||
routeHandler({
|
||||
|
||||
@@ -20,9 +20,8 @@ import { handleUpdateCustomer } from "../customers/handlers/handleUpdateCustomer
|
||||
import { handleCreateBillingPortal } from "../customers/handlers/handleCreateBillingPortal.js";
|
||||
import { handleGetCustomer } from "../customers/handlers/handleGetCustomer.js";
|
||||
import { CusSearchService } from "@/internal/customers/CusSearchService.js";
|
||||
import assert from "assert";
|
||||
|
||||
export const cusRouter = Router();
|
||||
export const cusRouter: Router = Router();
|
||||
|
||||
cusRouter.post("/all/search", async (req: any, res: any) => {
|
||||
try {
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { Router } from "express";
|
||||
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { EntityService } from "./EntityService.js";
|
||||
import { CusService } from "@/internal/customers/CusService.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { ErrCode } from "@autumn/shared";
|
||||
import { handleGetEntity } from "./handlers/handleGetEntity.js";
|
||||
import { handlePostEntityRequest } from "../../entities/handlers/handleCreateEntity/handleCreateEntity.js";
|
||||
import { handleDeleteEntity } from "./handlers/handleDeleteEntity.js";
|
||||
|
||||
export const entityRouter = Router({ mergeParams: true });
|
||||
export const entityRouter: Router = Router({ mergeParams: true });
|
||||
|
||||
// List entityes
|
||||
entityRouter.get("", (req, res) =>
|
||||
entityRouter.get("", (req: any, res: any) =>
|
||||
routeHandler({
|
||||
req,
|
||||
res,
|
||||
|
||||
@@ -30,7 +30,7 @@ import { getOrCreateCustomer } from "@/internal/customers/cusUtils/getOrCreateCu
|
||||
import { getCheckPreview } from "./getCheckPreview.js";
|
||||
import { orgToVersion } from "@/utils/versionUtils.js";
|
||||
|
||||
export const entitledRouter = Router();
|
||||
export const entitledRouter: Router = Router();
|
||||
|
||||
const getRequiredAndActualBalance = ({
|
||||
cusEnts,
|
||||
|
||||
@@ -30,7 +30,7 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { getEventTimestamp } from "./eventUtils.js";
|
||||
|
||||
export const eventsRouter = Router();
|
||||
export const eventsRouter: Router = Router();
|
||||
|
||||
const getEventAndCustomer = async ({
|
||||
req,
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
CusProductStatus,
|
||||
Customer,
|
||||
ErrCode,
|
||||
Event,
|
||||
EventInsert,
|
||||
FeatureType,
|
||||
FeatureUsageType,
|
||||
@@ -12,22 +11,17 @@ import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
|
||||
import { generateId, nullish } from "@/utils/genUtils.js";
|
||||
|
||||
import { EventService } from "./EventService.js";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import { FeatureService } from "@/internal/features/FeatureService.js";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import { QueueManager } from "@/queue/QueueManager.js";
|
||||
|
||||
import { JobName } from "@/queue/JobName.js";
|
||||
import { getOrCreateCustomer } from "@/internal/customers/cusUtils/getOrCreateCustomer.js";
|
||||
import { creditSystemContainsFeature } from "@/internal/features/creditSystemUtils.js";
|
||||
import { addTaskToQueue } from "@/queue/queueUtils.js";
|
||||
import { getOrgAndFeatures } from "@/internal/orgs/orgUtils.js";
|
||||
import { getEventTimestamp } from "./eventUtils.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { runUpdateBalanceTask } from "@/trigger/updateBalanceTask.js";
|
||||
import { runUpdateUsageTask } from "@/trigger/updateUsageTask.js";
|
||||
export const eventsRouter = Router();
|
||||
export const usageRouter = Router();
|
||||
|
||||
export const eventsRouter: Router = Router();
|
||||
export const usageRouter: Router = Router();
|
||||
|
||||
const getCusFeatureAndOrg = async ({
|
||||
req,
|
||||
|
||||
@@ -6,7 +6,7 @@ import RecaseError, {
|
||||
import { handleUpdateFeature } from "./handlers/handleUpdateFeature.js";
|
||||
import { Feature, FeatureResponseSchema, FeatureType } from "@autumn/shared";
|
||||
import { CreateFeatureSchema } from "@autumn/shared";
|
||||
import express from "express";
|
||||
import express, { Router } from "express";
|
||||
import { generateId } from "@/utils/genUtils.js";
|
||||
import { ErrCode } from "@/errors/errCodes.js";
|
||||
|
||||
@@ -20,7 +20,7 @@ import { JobName } from "@/queue/JobName.js";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import { handleDeleteFeature } from "./handlers/handleDeleteFeature.js";
|
||||
|
||||
export const featureApiRouter = express.Router();
|
||||
export const featureApiRouter: Router = express.Router();
|
||||
|
||||
export const validateFeature = (data: any) => {
|
||||
let featureType = data.type;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import { handleRequestError } from "@/utils/errorUtils.js";
|
||||
import { Router } from "express";
|
||||
|
||||
export const invoiceRouter = Router();
|
||||
export const invoiceRouter: Router = Router();
|
||||
|
||||
invoiceRouter.get("/:stripe_invoice_id/stripe", async (req: any, res: any) => {
|
||||
try {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { handleCopyProduct } from "./handlers/handleCopyProduct.js";
|
||||
import { handleCreateProduct } from "./handlers/handleCreateProduct.js";
|
||||
import { handleListProducts } from "./handlers/handleListProducts.js";
|
||||
|
||||
export const productApiRouter = Router();
|
||||
export const productApiRouter: Router = Router();
|
||||
|
||||
productApiRouter.get("", handleListProducts);
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@ import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionServ
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { generateId, notNullish } from "@/utils/genUtils.js";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { Customer, ErrCode, RewardTriggerEvent } from "@autumn/shared";
|
||||
import express from "express";
|
||||
import { ReferralCode, RewardRedemption } from "@autumn/shared";
|
||||
import { ErrCode, RewardTriggerEvent } from "@autumn/shared";
|
||||
import express, { Router } from "express";
|
||||
import { RewardRedemption } from "@autumn/shared";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js";
|
||||
|
||||
export const referralRouter = express.Router();
|
||||
export const referralRouter: Router = express.Router();
|
||||
|
||||
// 1. Get referral code
|
||||
referralRouter.post("/code", (req, res) =>
|
||||
@@ -238,7 +238,7 @@ referralRouter.post("/redeem", (req, res) =>
|
||||
}),
|
||||
);
|
||||
|
||||
export const redemptionRouter = express.Router();
|
||||
export const redemptionRouter: Router = express.Router();
|
||||
|
||||
redemptionRouter.get("/:redemptionId", (req, res) =>
|
||||
routeHandler({
|
||||
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
ErrCode,
|
||||
RewardTriggerEvent,
|
||||
} from "@autumn/shared";
|
||||
import express from "express";
|
||||
import express, { Router } from "express";
|
||||
|
||||
export const rewardProgramRouter = express.Router();
|
||||
export const rewardProgramRouter: Router = express.Router();
|
||||
|
||||
rewardProgramRouter.post("", (req, res) =>
|
||||
routeHandler({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import express from "express";
|
||||
import express, { Router } from "express";
|
||||
import {
|
||||
CreateRewardSchema,
|
||||
ErrCode,
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
getRewardCat,
|
||||
} from "@/internal/rewards/rewardUtils.js";
|
||||
|
||||
const rewardRouter = express.Router();
|
||||
const rewardRouter: Router = express.Router();
|
||||
|
||||
rewardRouter.post("", async (req: any, res: any) => {
|
||||
try {
|
||||
|
||||
@@ -189,194 +189,6 @@ export class CusService {
|
||||
return customer as Customer;
|
||||
}
|
||||
|
||||
//search customers
|
||||
|
||||
static addPaginationAndSearch = ({
|
||||
query,
|
||||
search,
|
||||
pageNumber,
|
||||
pageSize,
|
||||
lastItem,
|
||||
customerPrefix = "",
|
||||
}: {
|
||||
query: any;
|
||||
search: string;
|
||||
pageNumber: number | null;
|
||||
pageSize: number;
|
||||
lastItem: any;
|
||||
customerPrefix: string;
|
||||
}) => {
|
||||
if (search && search !== "") {
|
||||
query.or(
|
||||
`"name".ilike.%${search}%, ` +
|
||||
`"email".ilike.%${search}%, ` +
|
||||
`"id".ilike.%${search}%`,
|
||||
customerPrefix && {
|
||||
foreignTable: "customers",
|
||||
referencedTable: "customers",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (lastItem) {
|
||||
query.or(
|
||||
`"internal_id".lt.${lastItem.internal_id}`,
|
||||
// `"created_at".lt.${lastItem.created_at},` +
|
||||
// `and("created_at".eq.${lastItem.created_at},"internal_id".gt.${lastItem.internal_id})`,
|
||||
customerPrefix && {
|
||||
foreignTable: "customers",
|
||||
referencedTable: "customers",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (customerPrefix) {
|
||||
query.order(`customer(internal_id)`, { ascending: false });
|
||||
} else {
|
||||
query.order("internal_id", { ascending: false });
|
||||
// query
|
||||
// .order("created_at", { ascending: false })
|
||||
// .order("internal_id", { ascending: true });
|
||||
}
|
||||
|
||||
query.limit(pageSize);
|
||||
};
|
||||
|
||||
static async searchCustomersByProduct({
|
||||
sb,
|
||||
orgId,
|
||||
env,
|
||||
search,
|
||||
filters,
|
||||
pageSize,
|
||||
lastItem,
|
||||
pageNumber,
|
||||
}: {
|
||||
sb: SupabaseClient;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
search: string;
|
||||
filters: any;
|
||||
pageSize: number;
|
||||
lastItem: any;
|
||||
pageNumber: number;
|
||||
}) {
|
||||
const query = sb
|
||||
.from("customer_products")
|
||||
.select(
|
||||
`*,
|
||||
customer:customers!inner(*), product:products!inner(id, name, version)`,
|
||||
{
|
||||
count: "exact",
|
||||
},
|
||||
)
|
||||
.eq("customer.org_id", orgId)
|
||||
.eq("customer.env", env)
|
||||
.in("status", [CusProductStatus.Active, CusProductStatus.PastDue]);
|
||||
|
||||
if (filters.product_id) {
|
||||
query.eq("product.id", filters.product_id);
|
||||
}
|
||||
|
||||
if (filters?.status === "canceled") {
|
||||
console.log("Adding canceled filter");
|
||||
query
|
||||
.eq("status", CusProductStatus.Active)
|
||||
.not("canceled_at", "is", null);
|
||||
} else if (filters?.status === "free_trial") {
|
||||
console.log("Adding free trial filter");
|
||||
query
|
||||
.eq("status", CusProductStatus.Active)
|
||||
.gt("trial_ends_at", Date.now());
|
||||
}
|
||||
|
||||
this.addPaginationAndSearch({
|
||||
query,
|
||||
search,
|
||||
pageNumber,
|
||||
pageSize,
|
||||
lastItem,
|
||||
customerPrefix: "customers.",
|
||||
});
|
||||
|
||||
const { data, count, error } = await query;
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Flip
|
||||
|
||||
const customers = flipProductResults(data);
|
||||
|
||||
return { data: customers, count };
|
||||
}
|
||||
|
||||
static async searchCustomers({
|
||||
sb,
|
||||
orgId,
|
||||
env,
|
||||
search,
|
||||
pageSize = 50,
|
||||
filters,
|
||||
lastItem,
|
||||
pageNumber,
|
||||
}: {
|
||||
sb: SupabaseClient;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
search: string;
|
||||
lastItem?: { created_at: string; name: string; internal_id: string } | null;
|
||||
filters: any;
|
||||
pageSize?: number;
|
||||
pageNumber: number;
|
||||
}) {
|
||||
if (filters.product_id || filters.status) {
|
||||
return await this.searchCustomersByProduct({
|
||||
sb,
|
||||
orgId,
|
||||
env,
|
||||
search,
|
||||
filters,
|
||||
pageSize,
|
||||
lastItem,
|
||||
pageNumber,
|
||||
});
|
||||
}
|
||||
|
||||
let select =
|
||||
"*, customer_products:customer_products(*, product:products(*))";
|
||||
|
||||
let query = sb
|
||||
.from("customers")
|
||||
.select(select, {
|
||||
count: "exact",
|
||||
// count: "planned", // use for 1M rows...?
|
||||
})
|
||||
.eq("org_id", orgId)
|
||||
.eq("env", env);
|
||||
|
||||
this.addPaginationAndSearch({
|
||||
query,
|
||||
search,
|
||||
pageNumber: null,
|
||||
pageSize,
|
||||
lastItem,
|
||||
customerPrefix: "",
|
||||
});
|
||||
|
||||
const { data, count, error } = await query;
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const totalCount = count && count + pageSize * (pageNumber - 1);
|
||||
return { data, count: totalCount };
|
||||
}
|
||||
|
||||
// End of search customers
|
||||
|
||||
static async insert({ db, data }: { db: DrizzleCli; data: Customer }) {
|
||||
try {
|
||||
const results = await db
|
||||
@@ -469,192 +281,3 @@ export class CusService {
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
// static async getWithProductsDrizzle({
|
||||
// db,
|
||||
// idOrInternalId,
|
||||
// orgId,
|
||||
// env,
|
||||
// inStatuses = [
|
||||
// CusProductStatus.Active,
|
||||
// CusProductStatus.PastDue,
|
||||
// CusProductStatus.Scheduled,
|
||||
// ],
|
||||
// withEntities = false,
|
||||
// entityId,
|
||||
// expand,
|
||||
// withSubs = false,
|
||||
// }: {
|
||||
// db: DrizzleCli;
|
||||
// idOrInternalId: string;
|
||||
// orgId: string;
|
||||
// env: AppEnv;
|
||||
// inStatuses?: CusProductStatus[];
|
||||
// withEntities?: boolean;
|
||||
// entityId?: string;
|
||||
// expand?: (CusExpand | EntityExpand)[];
|
||||
// withSubs?: boolean;
|
||||
// }) {
|
||||
// // 1. Call RPC function
|
||||
// let data: {
|
||||
// customer: Customer | null;
|
||||
// products: FullCusProduct[] | null;
|
||||
// entities: Entity[] | null;
|
||||
// entity: Entity | null;
|
||||
// trials_used: any[] | null;
|
||||
// subscriptions: any[] | null;
|
||||
// invoices: any[] | null;
|
||||
// };
|
||||
|
||||
// try {
|
||||
// const result = await db.execute(sql`
|
||||
// SELECT * FROM get_cus_with_products(
|
||||
// p_cus_id => ${idOrInternalId}::text,
|
||||
// p_org_id => ${orgId}::text,
|
||||
// p_env => ${env}::text,
|
||||
// p_statuses => ARRAY[${sql.join(
|
||||
// inStatuses.map((status) => sql`${status}`),
|
||||
// sql`, `,
|
||||
// )}]::text[],
|
||||
// p_with_entities => ${withEntities}::boolean,
|
||||
// p_entity_id => ${entityId || null}::text,
|
||||
// p_with_trials_used => ${expand?.includes(CusExpand.TrialsUsed) || false}::boolean,
|
||||
// p_with_subs => ${withSubs}::boolean,
|
||||
// p_with_invoices => ${expand?.includes(CusExpand.Invoices) || false}::boolean
|
||||
// )
|
||||
// `);
|
||||
|
||||
// if (!result || result.length == 0 || !result[0].get_cus_with_products) {
|
||||
// throw new RecaseError({
|
||||
// message: "Calling get_cus_with_products RPC returned wrong shape",
|
||||
// code: ErrCode.GetCusWithProductsFailed,
|
||||
// statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
// data: result,
|
||||
// });
|
||||
// }
|
||||
|
||||
// data = result[0].get_cus_with_products as any;
|
||||
// } catch (error) {
|
||||
// throw error;
|
||||
// }
|
||||
|
||||
// if (!data || !data.customer) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// let { customer, products, entities, entity } = data;
|
||||
|
||||
// if (!products) {
|
||||
// products = [];
|
||||
// }
|
||||
|
||||
// for (let product of products) {
|
||||
// if (!product.customer_prices) {
|
||||
// product.customer_prices = [];
|
||||
// }
|
||||
|
||||
// if (!product.customer_entitlements) {
|
||||
// product.customer_entitlements = [];
|
||||
// }
|
||||
// }
|
||||
|
||||
// let trialsUsed = data.trials_used;
|
||||
// if (trialsUsed) {
|
||||
// trialsUsed = trialsUsed.filter(
|
||||
// (trial: any, index: number, self: any) =>
|
||||
// index ===
|
||||
// self.findIndex((t: any) => t.product_id === trial.product_id),
|
||||
// );
|
||||
// }
|
||||
|
||||
// return {
|
||||
// ...customer,
|
||||
// customer_products: products,
|
||||
// entities: entities,
|
||||
// entity: entity,
|
||||
// trials_used: trialsUsed,
|
||||
// subscriptions: data.subscriptions,
|
||||
// invoices: data.invoices,
|
||||
// } as FullCustomer;
|
||||
// }
|
||||
|
||||
// static async getWithProducts({
|
||||
// sb,
|
||||
// idOrInternalId,
|
||||
// orgId,
|
||||
// env,
|
||||
// inStatuses = [
|
||||
// CusProductStatus.Active,
|
||||
// CusProductStatus.PastDue,
|
||||
// CusProductStatus.Scheduled,
|
||||
// ],
|
||||
// withEntities = false,
|
||||
// entityId,
|
||||
// expand,
|
||||
// withSubs = false,
|
||||
// }: {
|
||||
// sb: SupabaseClient;
|
||||
// idOrInternalId: string;
|
||||
// orgId: string;
|
||||
// env: AppEnv;
|
||||
// inStatuses?: CusProductStatus[];
|
||||
// withEntities?: boolean;
|
||||
// entityId?: string;
|
||||
// expand?: (CusExpand | EntityExpand)[];
|
||||
// withSubs?: boolean;
|
||||
// }) {
|
||||
// const { data, error } = await sb.rpc("get_cus_with_products", {
|
||||
// p_cus_id: idOrInternalId,
|
||||
// p_org_id: orgId,
|
||||
// p_env: env,
|
||||
// p_statuses: inStatuses,
|
||||
// p_with_entities: withEntities,
|
||||
// p_entity_id: entityId,
|
||||
// p_with_trials_used: expand?.includes(CusExpand.TrialsUsed),
|
||||
// p_with_subs: withSubs,
|
||||
// p_with_invoices: expand?.includes(CusExpand.Invoices),
|
||||
// });
|
||||
|
||||
// if (error) {
|
||||
// throw error;
|
||||
// }
|
||||
|
||||
// if (!data || !data.customer) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// let { customer, products, entities, entity } = data;
|
||||
|
||||
// if (!products) {
|
||||
// products = [];
|
||||
// }
|
||||
|
||||
// for (let product of products) {
|
||||
// if (!product.customer_prices) {
|
||||
// product.customer_prices = [];
|
||||
// }
|
||||
|
||||
// if (!product.customer_entitlements) {
|
||||
// product.customer_entitlements = [];
|
||||
// }
|
||||
// }
|
||||
|
||||
// let trialsUsed = data.trials_used;
|
||||
// if (trialsUsed) {
|
||||
// trialsUsed = trialsUsed.filter(
|
||||
// (trial: any, index: number, self: any) =>
|
||||
// index ===
|
||||
// self.findIndex((t: any) => t.product_id === trial.product_id),
|
||||
// );
|
||||
// }
|
||||
|
||||
// return {
|
||||
// ...customer,
|
||||
// customer_products: products,
|
||||
// entities: entities,
|
||||
// entity: entity,
|
||||
// trials_used: trialsUsed,
|
||||
// subscriptions: data.subscriptions,
|
||||
// invoices: data.invoices,
|
||||
// };
|
||||
// }
|
||||
|
||||
@@ -86,18 +86,18 @@ export const handleOneOffFunction = async ({
|
||||
// Create invoice
|
||||
logger.info("1. Creating invoice");
|
||||
const stripeInvoice = await stripeCli.invoices.create({
|
||||
customer: customer.processor.id,
|
||||
customer: customer.processor.id!,
|
||||
auto_advance: false,
|
||||
currency: org.default_currency,
|
||||
currency: org.default_currency!,
|
||||
});
|
||||
|
||||
logger.info("2. Creating invoice items");
|
||||
for (const invoiceItem of invoiceItems) {
|
||||
await stripeCli.invoiceItems.create({
|
||||
...invoiceItem,
|
||||
customer: customer.processor.id,
|
||||
customer: customer.processor.id!,
|
||||
invoice: stripeInvoice.id,
|
||||
});
|
||||
} as any);
|
||||
}
|
||||
|
||||
// Create invoice items
|
||||
|
||||
@@ -42,7 +42,7 @@ import { processAttachBody } from "./attachUtils/attachParams/processAttachBody.
|
||||
import { handleAttachPreview } from "./handleAttachPreview/handleAttachPreview.js";
|
||||
import { handleAttach } from "./handleAttach.js";
|
||||
|
||||
export const attachRouter = Router();
|
||||
export const attachRouter: Router = Router();
|
||||
|
||||
export const handlePrepaidErrors = async ({
|
||||
attachParams,
|
||||
@@ -257,10 +257,6 @@ const handleAttachOld = async (req: any, res: any) =>
|
||||
}
|
||||
|
||||
logger.info("--------------------------------");
|
||||
let publicStr = req.isPublic ? "(Public) " : "";
|
||||
logger.info(
|
||||
`${publicStr}ATTACH PRODUCT REQUEST (from ${req.minOrg.slug})`,
|
||||
);
|
||||
|
||||
const {
|
||||
customer,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CusProductStatus, ErrCode, FullCusProduct } from "@autumn/shared";
|
||||
import { Router } from "express";
|
||||
import { expireCusProduct } from "../handlers/handleCusProductExpired.js";
|
||||
|
||||
const expireRouter = Router();
|
||||
const expireRouter: Router = Router();
|
||||
|
||||
expireRouter.post("", async (req, res) =>
|
||||
routeHandler({
|
||||
|
||||
@@ -40,7 +40,7 @@ export const handleGetCustomer = async (req: any, res: any) =>
|
||||
|
||||
if (!customer) {
|
||||
req.logtail.warn(
|
||||
`GET /customers/${customerId}: not found | Org: ${req.minOrg.slug}`,
|
||||
`GET /customers/${customerId}: not found | Org: ${org.slug}`,
|
||||
);
|
||||
res.status(StatusCodes.NOT_FOUND).json({
|
||||
message: `Customer ${customerId} not found`,
|
||||
|
||||
@@ -56,9 +56,7 @@ export const handlePostCustomerRequest = async (req: any, res: any) => {
|
||||
error instanceof RecaseError &&
|
||||
error.code === ErrCode.DuplicateCustomerId
|
||||
) {
|
||||
logger.warn(
|
||||
`POST /customers: ${error.message} (org: ${req.minOrg.slug})`,
|
||||
);
|
||||
logger.warn(`POST /customers: ${error.message} (org: ${req.org?.slug})`);
|
||||
res.status(error.statusCode).json({
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
|
||||
@@ -82,7 +82,7 @@ export const handleUpdateBalances = async (req: any, res: any) => {
|
||||
|
||||
logger.info("--------------------------------");
|
||||
logger.info(
|
||||
`REQUEST: UPDATE BALANCES FOR CUSTOMER ${customer.id}, ORG: ${req.minOrg.slug}`,
|
||||
`REQUEST: UPDATE BALANCES FOR CUSTOMER ${customer.id}, ORG: ${org.slug}`,
|
||||
);
|
||||
logger.info(
|
||||
`Features to update: ${balances.map(
|
||||
|
||||
@@ -26,7 +26,7 @@ import { StatusCodes } from "http-status-codes";
|
||||
import { cusProductToProduct } from "./cusProducts/cusProductUtils/convertCusProduct.js";
|
||||
import { createOrgResponse } from "../orgs/orgUtils.js";
|
||||
|
||||
export const cusRouter = Router();
|
||||
export const cusRouter: Router = Router();
|
||||
|
||||
cusRouter.get("/:customer_id/data", async (req: any, res: any) => {
|
||||
try {
|
||||
|
||||
@@ -30,7 +30,7 @@ export const getProductChargeText = ({
|
||||
itemStrs.push(
|
||||
formatCurrency({
|
||||
amount: total,
|
||||
defaultCurrency: org.default_currency,
|
||||
defaultCurrency: org.default_currency!,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,17 +26,17 @@ export const formatTiers = ({
|
||||
if (tiers.length == 1) {
|
||||
return formatCurrency({
|
||||
amount: tiers[0].amount,
|
||||
defaultCurrency: org.default_currency,
|
||||
defaultCurrency: org.default_currency!,
|
||||
});
|
||||
}
|
||||
|
||||
let tiersStart = formatCurrency({
|
||||
amount: tiers[0].amount,
|
||||
defaultCurrency: org.default_currency,
|
||||
defaultCurrency: org.default_currency!,
|
||||
});
|
||||
let tiersEnd = formatCurrency({
|
||||
amount: tiers[tiers.length - 1].amount,
|
||||
defaultCurrency: org.default_currency,
|
||||
defaultCurrency: org.default_currency!,
|
||||
});
|
||||
|
||||
return `${tiersStart} - ${tiersEnd}`;
|
||||
@@ -58,13 +58,13 @@ export const getItemsHtml = ({
|
||||
if (pricedItems.length == 1) {
|
||||
html += `<br/><p style="font-size: 1.1em;"><strong>${formatCurrency({
|
||||
amount: totalAmount,
|
||||
defaultCurrency: org.default_currency,
|
||||
defaultCurrency: org.default_currency!,
|
||||
})}</strong></p>`;
|
||||
} else {
|
||||
html += `<br/><ul>${itemsToHtml({ items: pricedItems })}</ul>`;
|
||||
html += `<br/><p style="font-size: 1.1em;">Total: ${formatCurrency({
|
||||
amount: totalAmount,
|
||||
defaultCurrency: org.default_currency,
|
||||
defaultCurrency: org.default_currency!,
|
||||
})}</p>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -115,32 +115,3 @@ export class CachedKeyService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// const { data, error } = await sb.rpc("verify_api_key", {
|
||||
// p_hashed_key: hashedKey,
|
||||
// p_env: env,
|
||||
// });
|
||||
|
||||
// if (error) {
|
||||
// throw error;
|
||||
// }
|
||||
|
||||
// if (!data.success || !data.organization) {
|
||||
// console.warn(`(warning) failed to verify secret key: ${data.error}`);
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// let org = structuredClone(data.organization);
|
||||
// delete org.features;
|
||||
|
||||
// // Add org config and api version
|
||||
// org.config = OrgConfigSchema.parse(org.config || {});
|
||||
// org.api_version = getApiVersion({
|
||||
// createdAt: org.created_at,
|
||||
// });
|
||||
|
||||
// return {
|
||||
// org,
|
||||
// features: data.organization?.features || [],
|
||||
// env,
|
||||
// };
|
||||
|
||||
@@ -4,13 +4,13 @@ import { Router } from "express";
|
||||
import { ApiKeyService } from "./ApiKeyService.js";
|
||||
import { OrgService } from "../orgs/OrgService.js";
|
||||
import { createKey } from "./api-keys/apiKeyUtils.js";
|
||||
import { getSvixDashboardUrl } from "@/external/svix/svixUtils.js";
|
||||
import { getSvixDashboardUrl } from "@/external/svix/svixHelpers.js";
|
||||
import { handleRequestError } from "@/utils/errorUtils.js";
|
||||
import { CacheManager } from "@/external/caching/CacheManager.js";
|
||||
import { CacheType } from "@/external/caching/cacheActions.js";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
|
||||
export const devRouter = Router();
|
||||
export const devRouter: Router = Router();
|
||||
|
||||
devRouter.get("/data", withOrgAuth, async (req: any, res) => {
|
||||
try {
|
||||
@@ -57,9 +57,7 @@ devRouter.post("/api_key", withOrgAuth, async (req: any, res) =>
|
||||
name,
|
||||
orgId,
|
||||
prefix,
|
||||
meta: {
|
||||
org_slug: req.minOrg.slug,
|
||||
},
|
||||
meta: {},
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
|
||||
40
server/src/internal/emails/sendInvitationEmail.ts
Normal file
40
server/src/internal/emails/sendInvitationEmail.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { sendTextEmail } from "@/external/resend/resendUtils.js";
|
||||
import { safeResend } from "@/external/resend/safeResend.js";
|
||||
|
||||
const getInvitationEmailBody = ({
|
||||
orgName,
|
||||
inviteLink,
|
||||
}: {
|
||||
orgName: string;
|
||||
inviteLink: string;
|
||||
}) => {
|
||||
return `Hey there! You've been invited to join ${orgName} on Autumn.
|
||||
|
||||
Click the link below to create an account / sign in and you'll be automatically added to the organization.
|
||||
|
||||
${process.env.CLIENT_URL}/sign-in
|
||||
`;
|
||||
};
|
||||
|
||||
export const sendInvitationEmail = safeResend({
|
||||
fn: async ({
|
||||
email,
|
||||
|
||||
orgName,
|
||||
inviteLink,
|
||||
}: {
|
||||
email: string;
|
||||
|
||||
orgName: string;
|
||||
inviteLink: string;
|
||||
}) => {
|
||||
console.log("Sending invitation email to", email);
|
||||
await sendTextEmail({
|
||||
from: `Autumn <hey@${process.env.RESEND_DOMAIN}>`,
|
||||
to: email,
|
||||
subject: `Join ${orgName} on Autumn`,
|
||||
body: getInvitationEmailBody({ orgName, inviteLink }),
|
||||
});
|
||||
},
|
||||
action: "send org invitation email",
|
||||
});
|
||||
49
server/src/internal/emails/sendMigrationEmail.ts
Normal file
49
server/src/internal/emails/sendMigrationEmail.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { MigrationService } from "../migrations/MigrationService.js";
|
||||
import { sendTextEmail } from "@/external/resend/resendUtils.js";
|
||||
import { MigrationJobStep, Organization } from "@autumn/shared";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { safeResend } from "@/external/resend/safeResend.js";
|
||||
|
||||
export const sendMigrationEmail = safeResend({
|
||||
fn: async ({
|
||||
db,
|
||||
migrationJobId,
|
||||
org,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
migrationJobId: string;
|
||||
org: Organization;
|
||||
}) => {
|
||||
let migrationJob = await MigrationService.getJob({
|
||||
db,
|
||||
id: migrationJobId,
|
||||
});
|
||||
|
||||
// Send email
|
||||
let getCustomersStep =
|
||||
migrationJob.step_details[MigrationJobStep.GetCustomers];
|
||||
let migrateStep =
|
||||
migrationJob.step_details[MigrationJobStep.MigrateCustomers];
|
||||
|
||||
console.log("Sending migration email");
|
||||
await sendTextEmail({
|
||||
to: "johnyeocx@gmail.com",
|
||||
subject: `Migration Job Finished -- ${migrationJob.id}`,
|
||||
body: `
|
||||
|
||||
ORG: ${org.id}, ${org.slug}
|
||||
Step: Get migration customers
|
||||
|
||||
1. Total customers: ${getCustomersStep?.total_customers}
|
||||
2. Canceled customers: ${getCustomersStep?.canceled_customers}
|
||||
|
||||
Step: Migrate customers
|
||||
|
||||
1. Number of errors: ${migrateStep?.num_errors}
|
||||
2. Failed customers:
|
||||
${migrateStep?.failed_customers}
|
||||
`,
|
||||
});
|
||||
},
|
||||
action: "send migration email",
|
||||
});
|
||||
20
server/src/internal/emails/sendOTPEmail.ts
Normal file
20
server/src/internal/emails/sendOTPEmail.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { logger } from "@/external/logtail/logtailUtils.js";
|
||||
import { createResendCli } from "@/external/resend/resendUtils.js";
|
||||
import OTPEmail from "@emails/OTPEmail.js";
|
||||
|
||||
const sendOTPEmail = async ({ email, otp }: { email: string; otp: string }) => {
|
||||
if (!process.env.RESEND_API_KEY || !process.env.RESEND_DOMAIN) {
|
||||
logger.warn(`RESEND NOT SET UP, SIGN IN OTP: ${otp}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const resend = createResendCli();
|
||||
await resend.emails.send({
|
||||
from: `Autumn <hey@${process.env.RESEND_DOMAIN}>`,
|
||||
to: email,
|
||||
subject: "Your verification code for Autumn",
|
||||
react: OTPEmail({ otpCode: otp }),
|
||||
});
|
||||
};
|
||||
|
||||
export default sendOTPEmail;
|
||||
32
server/src/internal/emails/sendOnboardingEmail.ts
Normal file
32
server/src/internal/emails/sendOnboardingEmail.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { sendHtmlEmail } from "@/external/resend/resendUtils.js";
|
||||
import { safeResend } from "@/external/resend/safeResend.js";
|
||||
|
||||
const getWelcomeEmailBody = (userFirstName: string) => {
|
||||
return `
|
||||
<p>Hey ${userFirstName} :)</p>
|
||||
|
||||
<p>Just wanted to say thank you for signing up to Autumn!</p>
|
||||
|
||||
<p>I'm curious--how did you hear about us? Also are you just looking around or do you have a specific use case I can help you with?</p>
|
||||
|
||||
<p>Whatever the reason, anything you need I'm here to help.</p>
|
||||
|
||||
<p>Ayush<br>
|
||||
Co-founder, Autumn</p>
|
||||
|
||||
<p>Oh, and join our <a href="https://discord.gg/STqxY92zuS">Discord community</a> to connect with us and other users</p>
|
||||
`;
|
||||
};
|
||||
|
||||
export const sendOnboardingEmail = safeResend({
|
||||
fn: async ({ name, email }: { name: string; email: string }) => {
|
||||
const firstName = name.split(" ")[0];
|
||||
|
||||
await sendHtmlEmail({
|
||||
to: email,
|
||||
subject: "Anything I can help with?",
|
||||
body: getWelcomeEmailBody(firstName),
|
||||
});
|
||||
},
|
||||
action: "send onboarding email",
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
import express from "express";
|
||||
import express, { Router } from "express";
|
||||
|
||||
import { FeatureService } from "./FeatureService.js";
|
||||
|
||||
export const featureRouter = express.Router();
|
||||
export const featureRouter: Router = express.Router();
|
||||
|
||||
featureRouter.get("", async (req: any, res) => {
|
||||
featureRouter.get("", async (req: any, res: any) => {
|
||||
try {
|
||||
let features = await FeatureService.getFromReq(req);
|
||||
res.status(200).json({ features });
|
||||
|
||||
@@ -159,6 +159,13 @@ export const runSaveFeatureDisplayTask = async ({
|
||||
}) => {
|
||||
let display;
|
||||
try {
|
||||
if (!process.env.ANTHROPIC_API_KEY) {
|
||||
logger.warn(
|
||||
"ANTHROPIC_API_KEY is not set, skipping feature display generation",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Generating feature display for ${feature.id} (org: ${org.slug})`,
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import "dotenv/config";
|
||||
|
||||
import { orgRouter } from "./orgs/orgRouter.js";
|
||||
import { Router } from "express";
|
||||
@@ -13,17 +12,18 @@ import { onboardingRouter } from "./orgs/onboarding/onboardingRouter.js";
|
||||
import { handlePostOrg } from "./orgs/handlers/handlePostOrg.js";
|
||||
import { Autumn } from "autumn-js";
|
||||
import { autumnHandler } from "autumn-js/express";
|
||||
import { parseAuthHeader } from "@/utils/authUtils.js";
|
||||
import { withAdminAuth } from "./admin/withAdminAuth.js";
|
||||
import { adminRouter } from "./admin/adminRouter.js";
|
||||
|
||||
const mainRouter = Router();
|
||||
const mainRouter: Router = Router();
|
||||
|
||||
mainRouter.get("", async (req: any, res) => {
|
||||
res.status(200).json({ message: "Hello World" });
|
||||
});
|
||||
|
||||
mainRouter.post("/organization", withAuth, handlePostOrg);
|
||||
mainRouter.use("/admin", withAdminAuth, adminRouter);
|
||||
mainRouter.use("/users", withAuth, userRouter);
|
||||
|
||||
mainRouter.use("/onboarding", withOrgAuth, onboardingRouter);
|
||||
mainRouter.use("/organization", withOrgAuth, orgRouter);
|
||||
mainRouter.use("/features", withOrgAuth, featureRouter);
|
||||
@@ -31,34 +31,35 @@ mainRouter.use("/products", withOrgAuth, productRouter);
|
||||
mainRouter.use("/dev", devRouter);
|
||||
mainRouter.use("/customers", withOrgAuth, cusRouter);
|
||||
|
||||
mainRouter.use(
|
||||
"/api/autumn",
|
||||
withOrgAuth,
|
||||
autumnHandler({
|
||||
identify: async (req: any) => {
|
||||
return {
|
||||
customerId: req.org?.id,
|
||||
customerData: {
|
||||
name: req.org?.slug,
|
||||
email: req.user?.email,
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
);
|
||||
// Optional...
|
||||
if (process.env.AUTUMN_SECRET_KEY) {
|
||||
mainRouter.use(
|
||||
"/api/autumn",
|
||||
withOrgAuth,
|
||||
autumnHandler({
|
||||
identify: async (req: any) => {
|
||||
return {
|
||||
customerId: req.org?.id,
|
||||
customerData: {
|
||||
name: req.org?.slug,
|
||||
email: req.user?.email,
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
mainRouter.use(
|
||||
"/demo/api/autumn",
|
||||
withOrgAuth,
|
||||
|
||||
autumnHandler({
|
||||
autumn: (req: any) => {
|
||||
let bearerToken = parseAuthHeader(req);
|
||||
|
||||
return new Autumn({
|
||||
secretKey: bearerToken,
|
||||
let client = new Autumn({
|
||||
url: "http://localhost:8080/v1",
|
||||
}) as any;
|
||||
headers: req.headers,
|
||||
});
|
||||
return client as any;
|
||||
},
|
||||
identify: async (req: any) => {
|
||||
return {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import express from "express";
|
||||
import express, { Router } from "express";
|
||||
import { constructMigrationJob } from "@/internal/migrations/migrationUtils.js";
|
||||
import { MigrationService } from "@/internal/migrations/MigrationService.js";
|
||||
import { JobName } from "@/queue/JobName.js";
|
||||
@@ -20,7 +20,7 @@ import { isFreeProduct } from "@/internal/products/productUtils.js";
|
||||
import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js";
|
||||
import { findPrepaidPrice } from "../products/prices/priceUtils/findPriceUtils.js";
|
||||
|
||||
export const migrationRouter = express.Router();
|
||||
export const migrationRouter: Router = express.Router();
|
||||
|
||||
migrationRouter.post("", async (req: any, res: any) => {
|
||||
return routeHandler({
|
||||
|
||||
@@ -11,7 +11,7 @@ import { MigrationService } from "../MigrationService.js";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import { migrateCustomer } from "./migrateCustomer.js";
|
||||
import { sendMigrationEmail } from "./sendMigrationEmail.js";
|
||||
import { sendMigrationEmail } from "../../emails/sendMigrationEmail.js";
|
||||
import { createStripePriceIFNotExist } from "@/external/stripe/createStripePrice/createStripePrice.js";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { MigrationService } from "../MigrationService.js";
|
||||
import { sendTextEmail } from "@/external/resend/resendUtils.js";
|
||||
import { MigrationJobStep, Organization } from "@autumn/shared";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
|
||||
export const sendMigrationEmail = async ({
|
||||
db,
|
||||
migrationJobId,
|
||||
org,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
migrationJobId: string;
|
||||
org: Organization;
|
||||
}) => {
|
||||
let migrationJob = await MigrationService.getJob({
|
||||
db,
|
||||
id: migrationJobId,
|
||||
});
|
||||
|
||||
// Send email
|
||||
let getCustomersStep =
|
||||
migrationJob.step_details[MigrationJobStep.GetCustomers];
|
||||
let migrateStep =
|
||||
migrationJob.step_details[MigrationJobStep.MigrateCustomers];
|
||||
|
||||
console.log("Sending migration email");
|
||||
await sendTextEmail({
|
||||
to: "johnyeocx@gmail.com",
|
||||
subject: `Migration Job Finished -- ${migrationJob.id}`,
|
||||
body: `
|
||||
|
||||
ORG: ${org.id}, ${org.slug}
|
||||
Step: Get migration customers
|
||||
|
||||
1. Total customers: ${getCustomersStep?.total_customers}
|
||||
2. Canceled customers: ${getCustomersStep?.canceled_customers}
|
||||
|
||||
Step: Migrate customers
|
||||
|
||||
1. Number of errors: ${migrateStep?.num_errors}
|
||||
2. Failed customers:
|
||||
${migrateStep?.failed_customers}
|
||||
`,
|
||||
});
|
||||
};
|
||||
34
server/src/internal/orgs/AuthService.ts
Normal file
34
server/src/internal/orgs/AuthService.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// import { db } from "@/db/initDrizzle.js";
|
||||
// import { OrgService } from "./OrgService.js";
|
||||
// import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
// import { member, organizations } from "@autumn/shared";
|
||||
// import { generateId } from "@/utils/genUtils.js";
|
||||
|
||||
// export class AuthService {
|
||||
// static async createOrg({
|
||||
// db,
|
||||
// name,
|
||||
// slug,
|
||||
// userId,
|
||||
// }: {
|
||||
// db: DrizzleCli;
|
||||
// name: string;
|
||||
// slug: string;
|
||||
// userId: string;
|
||||
// }) {
|
||||
// // 1. Create org
|
||||
// await db.insert(organizations).values({
|
||||
// id: generateId("org"),
|
||||
// name,
|
||||
// slug,
|
||||
// createdAt: new Date(),
|
||||
// });
|
||||
|
||||
// await db.insert(member).values({
|
||||
// id: generateId("mem"),
|
||||
// organizationId: org.id,
|
||||
// userId,
|
||||
// createdAt: new Date(),
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
@@ -1,18 +1,21 @@
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
AppEnv,
|
||||
ErrCode,
|
||||
Feature,
|
||||
features,
|
||||
invitation,
|
||||
member,
|
||||
Organization,
|
||||
OrgConfigSchema,
|
||||
user,
|
||||
} from "@autumn/shared";
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
|
||||
import { getApiVersion } from "@/utils/versionUtils.js";
|
||||
import { clearOrgCache } from "./orgUtils/clearOrgCache.js";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { organizations, apiKeys } from "@autumn/shared";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
|
||||
export class OrgService {
|
||||
static async getFromReq(req: any) {
|
||||
@@ -32,6 +35,30 @@ export class OrgService {
|
||||
return await this.get({ db: req.db, orgId: req.orgId });
|
||||
}
|
||||
|
||||
static async getMembers({ db, orgId }: { db: DrizzleCli; orgId: string }) {
|
||||
const results = await db
|
||||
.select()
|
||||
.from(member)
|
||||
.where(eq(member.organizationId, orgId))
|
||||
.innerJoin(user, eq(member.userId, user.id));
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
static async getInvites({ db, orgId }: { db: DrizzleCli; orgId: string }) {
|
||||
const results = await db
|
||||
.select()
|
||||
.from(invitation)
|
||||
.where(
|
||||
and(
|
||||
eq(invitation.organizationId, orgId),
|
||||
eq(invitation.status, "pending"),
|
||||
),
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Drizzle get
|
||||
static async get({ db, orgId }: { db: DrizzleCli; orgId: string }) {
|
||||
const result = await db.query.organizations.findFirst({
|
||||
|
||||
109
server/src/internal/orgs/handlers/handleDeleteOrg.ts
Normal file
109
server/src/internal/orgs/handlers/handleDeleteOrg.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { deleteSvixApp } from "@/external/svix/svixHelpers.js";
|
||||
import RecaseError, { handleFrontendReqError } from "@/utils/errorUtils.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { AppEnv, customers, ErrCode, Organization } from "@autumn/shared";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { Response } from "express";
|
||||
import { deleteStripeWebhook } from "../orgUtils.js";
|
||||
|
||||
const deleteSvixWebhooks = async ({
|
||||
org,
|
||||
logger,
|
||||
}: {
|
||||
org: Organization;
|
||||
logger: any;
|
||||
}) => {
|
||||
const batch = [];
|
||||
if (org.svix_config?.sandbox_app_id) {
|
||||
batch.push(
|
||||
deleteSvixApp({
|
||||
appId: org.svix_config.sandbox_app_id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (org.svix_config?.live_app_id) {
|
||||
batch.push(
|
||||
deleteSvixApp({
|
||||
appId: org.svix_config.live_app_id,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(batch);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to delete svix webhooks for ${org.id}, ${org.slug}`);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteStripeWebhooks = async ({
|
||||
org,
|
||||
logger,
|
||||
}: {
|
||||
org: Organization;
|
||||
logger: any;
|
||||
}) => {
|
||||
if (org.stripe_config) {
|
||||
try {
|
||||
await deleteStripeWebhook({
|
||||
org: org,
|
||||
env: AppEnv.Sandbox,
|
||||
});
|
||||
|
||||
await deleteStripeWebhook({
|
||||
org: org,
|
||||
env: AppEnv.Live,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error(
|
||||
`Failed to delete stripe webhooks for ${org.id}, ${org.slug}. ${error.message})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const handleDeleteOrg = async (req: ExtendedRequest, res: Response) => {
|
||||
try {
|
||||
const { org, db, logtail: logger } = req;
|
||||
|
||||
// 1. Check if any customers
|
||||
let hasCustomers = await db.query.customers.findFirst({
|
||||
where: eq(customers.org_id, org.id),
|
||||
});
|
||||
|
||||
if (hasCustomers)
|
||||
throw new RecaseError({
|
||||
message: "Cannot delete org with production mode customers",
|
||||
code: ErrCode.OrgHasCustomers,
|
||||
statusCode: 400,
|
||||
});
|
||||
|
||||
// 2. Delete svix webhooks
|
||||
logger.info("1. Deleting svix webhooks");
|
||||
await deleteSvixWebhooks({ org, logger });
|
||||
|
||||
// 3. Delete stripe webhooks
|
||||
logger.info("2. Deleting stripe webhooks");
|
||||
await deleteStripeWebhooks({ org, logger });
|
||||
|
||||
// 4. Delete all sandbox customers
|
||||
logger.info("3. Deleting sandbox customers");
|
||||
await db
|
||||
.delete(customers)
|
||||
.where(
|
||||
and(eq(customers.org_id, org.id), eq(customers.env, AppEnv.Sandbox)),
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
message: "Org deleted",
|
||||
});
|
||||
} catch (error) {
|
||||
handleFrontendReqError({
|
||||
res,
|
||||
error,
|
||||
req,
|
||||
action: "delete-org",
|
||||
});
|
||||
}
|
||||
};
|
||||
29
server/src/internal/orgs/handlers/handleGetOrgMembers.ts
Normal file
29
server/src/internal/orgs/handlers/handleGetOrgMembers.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { handleFrontendReqError } from "@/utils/errorUtils.js";
|
||||
import { OrgService } from "../OrgService.js";
|
||||
import { auth } from "@/utils/auth.js";
|
||||
|
||||
export const handleGetOrgMembers = async (req: any, res: any) => {
|
||||
try {
|
||||
const { org, db } = req;
|
||||
const orgId = org.id;
|
||||
|
||||
const memberships = await OrgService.getMembers({ db, orgId });
|
||||
const invites = await OrgService.getInvites({ db, orgId });
|
||||
|
||||
res.status(200).json({
|
||||
memberships,
|
||||
invites,
|
||||
});
|
||||
// res.status(200).json({
|
||||
// memberships: Array(10).fill(memberships[0]),
|
||||
// invites: Array(10).fill(invites[0]),
|
||||
// });
|
||||
} catch (error) {
|
||||
handleFrontendReqError({
|
||||
req,
|
||||
error,
|
||||
res,
|
||||
action: "get org members",
|
||||
});
|
||||
}
|
||||
};
|
||||
32
server/src/internal/orgs/handlers/handleGetUploadUrl.ts
Normal file
32
server/src/internal/orgs/handlers/handleGetUploadUrl.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { logger } from "@/external/logtail/logtailUtils.js";
|
||||
import { getUploadUrl } from "@/external/supabase/storageUtils.js";
|
||||
import RecaseError, { handleFrontendReqError } from "@/utils/errorUtils.js";
|
||||
import { ErrCode } from "@autumn/shared";
|
||||
|
||||
export const handleGetUploadUrl = async (req: any, res: any) => {
|
||||
try {
|
||||
const { org } = req;
|
||||
|
||||
let path = `logo/${org.id}`;
|
||||
|
||||
if (!process.env.SUPABASE_URL || !process.env.SUPABASE_SERVICE_KEY) {
|
||||
logger.warn("Supabase storage not set up");
|
||||
res.status(400).json({
|
||||
message: "Supabase storage not set up",
|
||||
code: ErrCode.SupabaseNotFound,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await getUploadUrl({ path });
|
||||
|
||||
res.status(200).json(data);
|
||||
} catch (error) {
|
||||
handleFrontendReqError({
|
||||
req,
|
||||
error,
|
||||
res,
|
||||
action: "get upload url",
|
||||
});
|
||||
}
|
||||
};
|
||||
48
server/src/internal/orgs/handlers/handleInvite.ts
Normal file
48
server/src/internal/orgs/handlers/handleInvite.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { auth } from "@/utils/auth.js";
|
||||
import {
|
||||
handleFrontendReqError,
|
||||
handleRequestError,
|
||||
} from "@/utils/errorUtils.js";
|
||||
import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js";
|
||||
import { user } from "@autumn/shared";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { Request, Response } from "express";
|
||||
|
||||
export const handleInvite = async (
|
||||
req: ExtendedRequest,
|
||||
res: ExtendedResponse,
|
||||
) => {
|
||||
try {
|
||||
const { email, role } = req.body;
|
||||
const { org, db } = req;
|
||||
|
||||
const emailUser = await db.query.user.findFirst({
|
||||
where: eq(user.email, email),
|
||||
});
|
||||
|
||||
if (emailUser) {
|
||||
await auth.api.addMember({
|
||||
body: {
|
||||
organizationId: org.id,
|
||||
userId: emailUser.id,
|
||||
role: role,
|
||||
},
|
||||
});
|
||||
res.status(200).send({
|
||||
message: "User added to organization",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(202).send({
|
||||
message: "Send invitation to user",
|
||||
});
|
||||
} catch (error) {
|
||||
handleFrontendReqError({
|
||||
req,
|
||||
res,
|
||||
error,
|
||||
action: "handleInvite",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,3 @@
|
||||
import { createClerkCli } from "@/external/clerkUtils.js";
|
||||
import { saveOrgToDB } from "@/external/webhooks/clerkWebhooks.js";
|
||||
|
||||
import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
|
||||
@@ -12,41 +9,51 @@ export const handlePostOrg = async (req: any, res: any) =>
|
||||
handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
|
||||
const { userId, db } = req;
|
||||
|
||||
const clerk = createClerkCli();
|
||||
const user = await clerk.users.getUser(userId!);
|
||||
console.log("userId", userId);
|
||||
|
||||
let userMemberships = await clerk.users.getOrganizationMembershipList({
|
||||
userId: userId!,
|
||||
});
|
||||
// const userMemberships = await auth.api.({
|
||||
// userId: userId!,
|
||||
// });
|
||||
|
||||
let org;
|
||||
// const clerk = createClerkCli();
|
||||
// const user = await clerk.users.getUser(userId!);
|
||||
|
||||
if (userMemberships.data.length === 0) {
|
||||
org = await clerk.organizations.createOrganization({
|
||||
name: `${user.firstName}'s Org`,
|
||||
});
|
||||
// let userMemberships = await clerk.users.getOrganizationMembershipList({
|
||||
// userId: userId!,
|
||||
// });
|
||||
|
||||
// 2. Create org membership for user
|
||||
await clerk.organizations.createOrganizationMembership({
|
||||
organizationId: org.id,
|
||||
userId: userId!,
|
||||
role: "org:admin",
|
||||
});
|
||||
// let org;
|
||||
|
||||
await saveOrgToDB({
|
||||
db,
|
||||
id: org.id,
|
||||
slug: org.slug,
|
||||
});
|
||||
// if (userMemberships.data.length === 0) {
|
||||
// org = await clerk.organizations.createOrganization({
|
||||
// name: `${user.firstName}'s Org`,
|
||||
// });
|
||||
|
||||
console.log(`Created new org: ${org.id} (${org.slug})`);
|
||||
} else {
|
||||
org = userMemberships.data[0].organization;
|
||||
}
|
||||
// // 2. Create org membership for user
|
||||
// await clerk.organizations.createOrganizationMembership({
|
||||
// organizationId: org.id,
|
||||
// userId: userId!,
|
||||
// role: "org:admin",
|
||||
// });
|
||||
|
||||
// await saveOrgToDB({
|
||||
// db,
|
||||
// id: org.id,
|
||||
// slug: org.slug,
|
||||
// });
|
||||
|
||||
// console.log(`Created new org: ${org.id} (${org.slug})`);
|
||||
// } else {
|
||||
// org = userMemberships.data[0].organization;
|
||||
// }
|
||||
|
||||
res.status(200).json({
|
||||
id: org.id,
|
||||
slug: org.slug,
|
||||
id: "123",
|
||||
slug: "123",
|
||||
});
|
||||
// res.status(200).json({
|
||||
// id: org.id,
|
||||
// slug: org.slug,
|
||||
// });
|
||||
},
|
||||
});
|
||||
|
||||
23
server/src/internal/orgs/handlers/handleUpdateOrg.ts
Normal file
23
server/src/internal/orgs/handlers/handleUpdateOrg.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// import { auth } from "@/utils/auth.js";
|
||||
// import { handleFrontendReqError } from "@/utils/errorUtils.js";
|
||||
|
||||
// export const handleUpdateOrg = async (req: any, res: any) => {
|
||||
// try {
|
||||
// await auth.api.updateOrganization({
|
||||
// data: {
|
||||
// name: req.body.name,
|
||||
// slug: req.body.slug,
|
||||
// },
|
||||
// organizationId: req.org.id,
|
||||
// });
|
||||
|
||||
// res.status(200).json({ success: true });
|
||||
// } catch (error) {
|
||||
// handleFrontendReqError({
|
||||
// req,
|
||||
// error,
|
||||
// res,
|
||||
// action: "update org",
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
@@ -3,7 +3,6 @@ import { Router } from "express";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { createClerkCli } from "@/external/clerkUtils.js";
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import { parseChatResultFeatures } from "./parseChatFeatures.js";
|
||||
import { parseChatProducts } from "./parseChatProducts.js";
|
||||
@@ -14,7 +13,7 @@ import { EntitlementService } from "@/internal/products/entitlements/Entitlement
|
||||
import { PriceService } from "@/internal/products/prices/PriceService.js";
|
||||
import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js";
|
||||
|
||||
export const onboardingRouter = Router();
|
||||
export const onboardingRouter: Router = Router();
|
||||
|
||||
onboardingRouter.post("", async (req: Request, res: any) =>
|
||||
routeHandler({
|
||||
|
||||
@@ -1,23 +1,38 @@
|
||||
import express, { Router } from "express";
|
||||
import Stripe from "stripe";
|
||||
|
||||
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
|
||||
import { encryptData } from "@/utils/encryptUtils.js";
|
||||
|
||||
import { ErrCode } from "@/errors/errCodes.js";
|
||||
import { createClerkCli } from "@/external/clerkUtils.js";
|
||||
import {
|
||||
checkKeyValid,
|
||||
createWebhookEndpoint,
|
||||
} from "@/external/stripe/stripeOnboardingUtils.js";
|
||||
import { encryptData } from "@/utils/encryptUtils.js";
|
||||
import RecaseError, {
|
||||
handleFrontendReqError,
|
||||
handleRequestError,
|
||||
} from "@/utils/errorUtils.js";
|
||||
import express from "express";
|
||||
import Stripe from "stripe";
|
||||
|
||||
import { OrgService } from "./OrgService.js";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import { nullish } from "@/utils/genUtils.js";
|
||||
import { clearOrgCache } from "./orgUtils/clearOrgCache.js";
|
||||
import { createOrgResponse } from "./orgUtils.js";
|
||||
import { handleGetOrgMembers } from "./handlers/handleGetOrgMembers.js";
|
||||
import { handleInvite } from "./handlers/handleInvite.js";
|
||||
import { handleGetUploadUrl } from "./handlers/handleGetUploadUrl.js";
|
||||
import { handleDeleteOrg } from "./handlers/handleDeleteOrg.js";
|
||||
|
||||
export const orgRouter = express.Router();
|
||||
export const orgRouter: Router = express.Router();
|
||||
orgRouter.get("/members", handleGetOrgMembers);
|
||||
orgRouter.get("/upload_url", handleGetUploadUrl);
|
||||
orgRouter.post("/invite", handleInvite as any);
|
||||
orgRouter.delete("", handleDeleteOrg as any);
|
||||
|
||||
orgRouter.delete("/delete-user", async (req: any, res) => {
|
||||
res.status(200).json({
|
||||
message: "User deleted",
|
||||
});
|
||||
});
|
||||
|
||||
orgRouter.get("", async (req: any, res) => {
|
||||
try {
|
||||
@@ -30,9 +45,7 @@ orgRouter.get("", async (req: any, res) => {
|
||||
|
||||
const org = await OrgService.getFromReq(req);
|
||||
|
||||
res.status(200).json({
|
||||
org,
|
||||
});
|
||||
res.status(200).json(createOrgResponse(org));
|
||||
} catch (error) {
|
||||
handleRequestError({
|
||||
req,
|
||||
@@ -90,8 +103,6 @@ orgRouter.post("/stripe", async (req: any, res) => {
|
||||
let testWebhook: Stripe.WebhookEndpoint;
|
||||
let liveWebhook: Stripe.WebhookEndpoint;
|
||||
try {
|
||||
console.log(`Creating stripe webhook for URL: ${process.env.SERVER_URL}`);
|
||||
|
||||
testWebhook = await createWebhookEndpoint(
|
||||
testApiKey,
|
||||
AppEnv.Sandbox,
|
||||
@@ -128,15 +139,6 @@ orgRouter.post("/stripe", async (req: any, res) => {
|
||||
},
|
||||
});
|
||||
|
||||
// 2. Update org in Clerk
|
||||
const clerkCli = createClerkCli();
|
||||
await clerkCli.organizations.updateOrganization(req.orgId, {
|
||||
publicMetadata: {
|
||||
stripe_connected: true,
|
||||
default_currency: defaultCurrency,
|
||||
},
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
message: "Stripe connected",
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { decryptData, generatePublishableKey } from "@/utils/encryptUtils.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { AppEnv, ErrCode, Organization } from "@autumn/shared";
|
||||
import { createSvixApp } from "@/external/svix/svixUtils.js";
|
||||
import { AppEnv, ErrCode, FrontendOrg, Organization } from "@autumn/shared";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import { OrgService } from "./OrgService.js";
|
||||
import { FeatureService } from "../features/FeatureService.js";
|
||||
import { createSvixApp } from "@/external/svix/svixHelpers.js";
|
||||
|
||||
export const constructOrg = ({ id, slug }: { id: string; slug: string }) => {
|
||||
return {
|
||||
@@ -23,33 +23,6 @@ export const constructOrg = ({ id, slug }: { id: string; slug: string }) => {
|
||||
config: {} as any,
|
||||
};
|
||||
};
|
||||
export const initOrgSvixApps = async ({
|
||||
id,
|
||||
slug,
|
||||
}: {
|
||||
id: string;
|
||||
slug: string;
|
||||
}) => {
|
||||
const batchCreate = [];
|
||||
batchCreate.push(
|
||||
createSvixApp({
|
||||
name: `${slug}_${AppEnv.Sandbox}`,
|
||||
orgId: id,
|
||||
env: AppEnv.Sandbox,
|
||||
}),
|
||||
);
|
||||
batchCreate.push(
|
||||
createSvixApp({
|
||||
name: `${slug}_${AppEnv.Live}`,
|
||||
orgId: id,
|
||||
env: AppEnv.Live,
|
||||
}),
|
||||
);
|
||||
|
||||
const [sandboxApp, liveApp] = await Promise.all(batchCreate);
|
||||
|
||||
return { sandboxApp, liveApp };
|
||||
};
|
||||
|
||||
export const deleteStripeWebhook = async ({
|
||||
org,
|
||||
@@ -104,13 +77,15 @@ export const initDefaultConfig = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const createOrgResponse = (org: Organization) => {
|
||||
export const createOrgResponse = (org: Organization): FrontendOrg => {
|
||||
return {
|
||||
id: org.id,
|
||||
name: org.name,
|
||||
logo: org.logo,
|
||||
slug: org.slug,
|
||||
default_currency: org.default_currency,
|
||||
stripe_connected: org.stripe_connected,
|
||||
created_at: org.created_at,
|
||||
default_currency: org.default_currency || "USD",
|
||||
stripe_connected: org.stripe_connected || false,
|
||||
created_at: new Date(org.createdAt).getTime(),
|
||||
test_pkey: org.test_pkey,
|
||||
live_pkey: org.live_pkey,
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ import { isFeaturePriceItem } from "./product-items/productItemUtils/getItemType
|
||||
import RecaseError, { handleFrontendReqError } from "@/utils/errorUtils.js";
|
||||
import { createOrgResponse } from "../orgs/orgUtils.js";
|
||||
|
||||
export const productRouter = Router({ mergeParams: true });
|
||||
export const productRouter: Router = Router({ mergeParams: true });
|
||||
|
||||
productRouter.get("/data", async (req: any, res) => {
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
|
||||
import { nullish } from "@/utils/genUtils.js";
|
||||
import { numberWithCommas } from "tests/utils/general/numberUtils.js";
|
||||
import { getFeatureName } from "@/internal/features/utils/displayUtils.js";
|
||||
|
||||
import {
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
ErrCode,
|
||||
Infinite,
|
||||
FullCusProduct,
|
||||
numberWithCommas,
|
||||
} from "@autumn/shared";
|
||||
import { isPriceItem } from "../product-items/productItemUtils/getItemType.js";
|
||||
import { isFeaturePriceItem } from "../product-items/productItemUtils/getItemType.js";
|
||||
|
||||
@@ -252,17 +252,5 @@ export class RewardProgramService {
|
||||
);
|
||||
|
||||
return result[0].count;
|
||||
|
||||
// const { data, error, count } = await sb
|
||||
// .from("reward_redemptions")
|
||||
// .select("*, reward_program:reward_programs!inner(*)", { count: "exact" })
|
||||
// .eq("referral_code_id", referralCodeId)
|
||||
// .eq("triggered", true);
|
||||
|
||||
// if (error) {
|
||||
// throw error;
|
||||
// }
|
||||
|
||||
// return count;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,41 +72,6 @@ export class RewardRedemptionService {
|
||||
});
|
||||
|
||||
return data as any;
|
||||
|
||||
// let query = sb
|
||||
// .from("reward_redemptions")
|
||||
// .select(
|
||||
// `
|
||||
// *
|
||||
// ${
|
||||
// withRewardProgram
|
||||
// ? ", reward_program:reward_programs!inner(*, reward:rewards!inner(*))"
|
||||
// : ""
|
||||
// }
|
||||
// ${withReferralCode ? ", referral_code:referral_codes!inner(*)" : ""}
|
||||
// `,
|
||||
// )
|
||||
// .eq("internal_customer_id", internalCustomerId);
|
||||
|
||||
// if (notNullish(internalRewardProgramId)) {
|
||||
// query = query.eq("internal_reward_program_id", internalRewardProgramId);
|
||||
// }
|
||||
|
||||
// if (notNullish(triggered)) {
|
||||
// query = query.eq("triggered", triggered);
|
||||
// }
|
||||
|
||||
// if (notNullish(limit)) {
|
||||
// query = query.limit(limit);
|
||||
// }
|
||||
|
||||
// const { data, error } = await query;
|
||||
|
||||
// if (error) {
|
||||
// throw error;
|
||||
// }
|
||||
|
||||
// return data;
|
||||
}
|
||||
|
||||
static async getByReferrer({
|
||||
@@ -142,23 +107,6 @@ export class RewardRedemptionService {
|
||||
}));
|
||||
|
||||
return processed;
|
||||
|
||||
// const { data, error } = await sb
|
||||
// .from("reward_redemptions")
|
||||
// .select(
|
||||
// `
|
||||
// *, referral_code:referral_codes!inner(*)
|
||||
// ${withCustomer ? ", customer:customers!inner(*)" : ""}
|
||||
// `,
|
||||
// )
|
||||
// .eq("referral_code.internal_customer_id", internalCustomerId)
|
||||
// .limit(limit);
|
||||
|
||||
// if (error) {
|
||||
// throw error;
|
||||
// }
|
||||
|
||||
// return data;
|
||||
}
|
||||
|
||||
static async insert({
|
||||
|
||||
@@ -1,23 +1,7 @@
|
||||
import { createSupabaseClient } from "@/external/supabaseUtils.js";
|
||||
import { Router } from "express";
|
||||
|
||||
export const userRouter = Router();
|
||||
export const userRouter: Router = Router();
|
||||
|
||||
userRouter.get("", async (req: any, res) => {
|
||||
const supabase = createSupabaseClient();
|
||||
|
||||
res.status(200).json({ userId: req.userId });
|
||||
// const { data, error } = await supabase
|
||||
// .from("users")
|
||||
// .select("id, email, initialized")
|
||||
// .eq("id", req.userId)
|
||||
// .single();
|
||||
|
||||
// if (error) {
|
||||
// console.error("Error fetching user data:", error);
|
||||
// res.status(500).json({ error: error.message });
|
||||
// return;
|
||||
// }
|
||||
|
||||
// res.status(200).json(data);
|
||||
});
|
||||
|
||||
@@ -8,40 +8,44 @@ const handleResFinish = (req: any, res: any, logtailContext: any) => {
|
||||
if (skipUrls.includes(req.originalUrl)) {
|
||||
return;
|
||||
}
|
||||
req.logtailAll.info(
|
||||
`[${res.statusCode}] ${req.method} ${req.originalUrl} (${req.org?.slug})`,
|
||||
{
|
||||
req: {
|
||||
...logtailContext,
|
||||
|
||||
// Only log to logtailAll if it exists
|
||||
if (req.logtailAll) {
|
||||
req.logtailAll.info(
|
||||
`[${res.statusCode}] ${req.method} ${req.originalUrl} (${req.org?.slug})`,
|
||||
{
|
||||
req: {
|
||||
...logtailContext,
|
||||
},
|
||||
statusCode: res.statusCode,
|
||||
res: res.locals.responseBody,
|
||||
},
|
||||
statusCode: res.statusCode,
|
||||
res: res.locals.responseBody,
|
||||
},
|
||||
);
|
||||
req.logtailAll.flush();
|
||||
);
|
||||
req.logtailAll.flush();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to log response to logtailAll");
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
// Post hog
|
||||
let posthogUrls = ["/v1/attach"];
|
||||
if (req.posthog && posthogUrls.includes(req.originalUrl)) {
|
||||
posthogCapture({
|
||||
posthog: req.posthog,
|
||||
params: {
|
||||
distinctId: req.org?.id,
|
||||
event: `${req.method} ${req.originalUrl}`,
|
||||
properties: {
|
||||
authType: req.auth,
|
||||
orgSlug: req.org?.slug,
|
||||
statusCode: res.statusCode,
|
||||
res: res.locals.responseBody,
|
||||
req: req.body,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
// Save to PostHog
|
||||
// let posthogUrls = ["/v1/attach"];
|
||||
// if (req.posthog && posthogUrls.includes(req.originalUrl)) {
|
||||
// posthogCapture({
|
||||
// posthog: req.posthog,
|
||||
// params: {
|
||||
// distinctId: req.org?.id,
|
||||
// event: `${req.method} ${req.originalUrl}`,
|
||||
// properties: {
|
||||
// authType: req.auth,
|
||||
// orgSlug: req.org?.slug,
|
||||
// statusCode: res.statusCode,
|
||||
// res: res.locals.responseBody,
|
||||
// req: req.body,
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
};
|
||||
|
||||
export const analyticsMiddleware = async (req: any, res: any, next: any) => {
|
||||
|
||||
@@ -8,6 +8,7 @@ export const verifySecretKey = async (req: any, res: any, next: any) => {
|
||||
const authHeader =
|
||||
req.headers["authorization"] || req.headers["Authorization"];
|
||||
|
||||
const logger = req.logtail;
|
||||
const version = req.headers["x-api-version"];
|
||||
|
||||
if (version) {
|
||||
@@ -56,7 +57,6 @@ export const verifySecretKey = async (req: any, res: any, next: any) => {
|
||||
|
||||
// Try verify via Autumn
|
||||
|
||||
let logger = req.logtail;
|
||||
try {
|
||||
const { valid, data } = await verifyKey({
|
||||
db: req.db,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import { AuthType } from "@autumn/shared";
|
||||
import { auth } from "@/utils/auth.js";
|
||||
import { AuthType, ErrCode } from "@autumn/shared";
|
||||
import { verifyToken } from "@clerk/express";
|
||||
import { fromNodeHeaders } from "better-auth/node";
|
||||
import { NextFunction } from "express";
|
||||
|
||||
const getTokenData = async (req: any, res: any) => {
|
||||
@@ -33,59 +35,73 @@ const getTokenData = async (req: any, res: any) => {
|
||||
};
|
||||
|
||||
export const withOrgAuth = async (req: any, res: any, next: NextFunction) => {
|
||||
try {
|
||||
let tokenData = await getTokenData(req, res);
|
||||
const { logtail: logger } = req;
|
||||
|
||||
if (!tokenData?.org_id) {
|
||||
throw new Error("token data has no org_id");
|
||||
try {
|
||||
// let tokenData = await getTokenData(req, res);
|
||||
const session = await auth.api.getSession({
|
||||
headers: fromNodeHeaders(req.headers),
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
logger.info(`Unauthorized - no session found (${req.originalUrl})`);
|
||||
return res
|
||||
.status(401)
|
||||
.json({ message: "Unauthorized - no session found" });
|
||||
}
|
||||
|
||||
let tokenOrg = tokenData!.org as any;
|
||||
const orgId = session?.session?.activeOrganizationId;
|
||||
|
||||
if (!orgId) {
|
||||
logger.info(`Unauthorized - no org id found`);
|
||||
return res
|
||||
.status(401)
|
||||
.json({ message: "Unauthorized - no org id found" });
|
||||
}
|
||||
|
||||
// let tokenOrg = tokenData!.org as any;
|
||||
|
||||
let data = await OrgService.getWithFeatures({
|
||||
db: req.db,
|
||||
orgId: tokenOrg.id,
|
||||
orgId: orgId,
|
||||
env: req.env,
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return res.status(404).json({ message: "Org not found" });
|
||||
logger.warn(`Org ${orgId} not found in DB`);
|
||||
return res
|
||||
.status(500)
|
||||
.json({ message: "Org not found", code: ErrCode.OrgNotFound });
|
||||
}
|
||||
|
||||
const { org, features } = data;
|
||||
|
||||
req.minOrg = {
|
||||
id: tokenOrg?.id,
|
||||
slug: tokenOrg?.slug,
|
||||
};
|
||||
|
||||
req.orgId = tokenData!.org_id;
|
||||
req.user = tokenData!.user;
|
||||
req.user = session?.user;
|
||||
req.orgId = orgId;
|
||||
req.org = org;
|
||||
req.features = features;
|
||||
req.authType = AuthType.Dashboard;
|
||||
|
||||
next();
|
||||
} catch (error: any) {
|
||||
console.log(
|
||||
// `withOrgAuth error (${req.headers["authorization"]}):`,
|
||||
`(warning) clerk auth failed:`,
|
||||
error?.message || error,
|
||||
);
|
||||
// console.log(`(warning) clerk auth failed:`, error?.message || error);
|
||||
logger.warn(`(warning) withOrgAuth failed:`, error?.message || error);
|
||||
res.status(401).json({ message: "Unauthorized" });
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const withAuth = async (req: any, res: any, next: NextFunction) => {
|
||||
const tokenData = await getTokenData(req, res);
|
||||
const session = await auth.api.getSession({
|
||||
headers: fromNodeHeaders(req.headers),
|
||||
});
|
||||
|
||||
if (!tokenData) {
|
||||
if (!session) {
|
||||
res.status(401).json({ message: "Unauthorized" });
|
||||
return;
|
||||
}
|
||||
|
||||
req.userId = tokenData?.user_id;
|
||||
req.userId = session?.user.id;
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import "dotenv/config";
|
||||
|
||||
import { Queue } from "bullmq";
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
const BACKUP_REDIS_URL = process.env.REDIS_BACKUP_URL || process.env.REDIS_URL;
|
||||
const MAIN_REDIS_URL = process.env.REDIS_URL;
|
||||
|
||||
export class QueueManager {
|
||||
private static instance: QueueManager;
|
||||
private queue: Queue | null = null;
|
||||
@@ -31,16 +36,7 @@ export class QueueManager {
|
||||
useBackup: boolean;
|
||||
keepConnection?: boolean;
|
||||
}) {
|
||||
// 1. Connect to redis
|
||||
|
||||
if (useBackup && !process.env.REDIS_BACKUP_URL) {
|
||||
console.warn(`REDIS_BACKUP_URL not set, using main redis`);
|
||||
useBackup = false;
|
||||
}
|
||||
|
||||
const redisUrl = useBackup
|
||||
? process.env.REDIS_BACKUP_URL
|
||||
: process.env.REDIS_URL;
|
||||
const redisUrl = useBackup ? BACKUP_REDIS_URL : MAIN_REDIS_URL;
|
||||
|
||||
const connection = new Redis(redisUrl!, {
|
||||
retryStrategy: (times) => {
|
||||
@@ -52,7 +48,7 @@ export class QueueManager {
|
||||
console.log(
|
||||
`Redis connection error (${useBackup ? "backup" : "main"}): ${
|
||||
error.message
|
||||
}`,
|
||||
}`
|
||||
);
|
||||
|
||||
if (!keepConnection) {
|
||||
@@ -95,7 +91,7 @@ export class QueueManager {
|
||||
console.log("2. Initializing main & backup queues");
|
||||
const mainQueue = new Queue("autumn", {
|
||||
connection: {
|
||||
url: process.env.REDIS_URL,
|
||||
url: MAIN_REDIS_URL,
|
||||
enableOfflineQueue: false,
|
||||
retryStrategy: (times) => {
|
||||
return 5000;
|
||||
@@ -105,7 +101,7 @@ export class QueueManager {
|
||||
|
||||
const backupQueue = new Queue("autumn", {
|
||||
connection: {
|
||||
url: process.env.REDIS_BACKUP_URL,
|
||||
url: BACKUP_REDIS_URL,
|
||||
enableOfflineQueue: false,
|
||||
},
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user