feature(auth): replace clerk.com with custom auth (#103)
* feature(auth): replace clerk.com with custom auth * minor fixes * remove notification preferences * decrease live events interval fix(api): cookies.. # Conflicts: # .gitignore # apps/api/src/index.ts # apps/dashboard/src/app/providers.tsx # packages/trpc/src/trpc.ts
This commit is contained in:
committed by
Carl-Gerhard Lindesvärd
parent
f28802b1c2
commit
d31d9924a5
119
packages/db/code-migrations/1-settings.ts
Normal file
119
packages/db/code-migrations/1-settings.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
chQuery,
|
||||
db,
|
||||
getClientByIdCached,
|
||||
getProjectByIdCached,
|
||||
} from '../index';
|
||||
|
||||
import { stripTrailingSlash } from '@openpanel/common';
|
||||
|
||||
const pickBestDomain = (domains: string[]): string | null => {
|
||||
// Filter out invalid domains
|
||||
const validDomains = domains.filter(
|
||||
(domain) =>
|
||||
domain &&
|
||||
!domain.includes('*') &&
|
||||
!domain.includes('localhost') &&
|
||||
!domain.includes('127.0.0.1'),
|
||||
);
|
||||
|
||||
if (validDomains.length === 0) return null;
|
||||
|
||||
// Score each domain
|
||||
const scoredDomains = validDomains.map((domain) => {
|
||||
let score = 0;
|
||||
|
||||
// Prefer https (highest priority)
|
||||
if (domain.startsWith('https://')) score += 100;
|
||||
|
||||
// Penalize domains from common providers like vercel, netlify, etc.
|
||||
if (
|
||||
domain.includes('vercel.app') ||
|
||||
domain.includes('netlify.app') ||
|
||||
domain.includes('herokuapp.com') ||
|
||||
domain.includes('github.io') ||
|
||||
domain.includes('gitlab.io') ||
|
||||
domain.includes('surge.sh') ||
|
||||
domain.includes('cloudfront.net') ||
|
||||
domain.includes('firebaseapp.com') ||
|
||||
domain.includes('azurestaticapps.net') ||
|
||||
domain.includes('pages.dev') ||
|
||||
domain.includes('ngrok-free.app') ||
|
||||
domain.includes('ngrok.app')
|
||||
) {
|
||||
score -= 50;
|
||||
}
|
||||
|
||||
// Penalize subdomains
|
||||
const domainParts = domain
|
||||
.replace('https://', '')
|
||||
.replace('http://', '')
|
||||
.split('.');
|
||||
if (domainParts.length <= 2) score += 50;
|
||||
|
||||
// Tiebreaker: prefer shorter domains
|
||||
score -= domain.length;
|
||||
|
||||
return { domain, score };
|
||||
});
|
||||
|
||||
// Sort by score (highest first) and return the best domain
|
||||
const bestDomain = scoredDomains.sort((a, b) => b.score - a.score)[0];
|
||||
return bestDomain?.domain || null;
|
||||
};
|
||||
|
||||
export const up = async () => {
|
||||
const projects = await db.project.findMany({
|
||||
include: {
|
||||
clients: true,
|
||||
},
|
||||
});
|
||||
|
||||
const matches = [];
|
||||
for (const project of projects) {
|
||||
if (project.cors.length > 0 || project.domain) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (project.clients.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const cors = [];
|
||||
let crossDomain = false;
|
||||
for (const client of project.clients) {
|
||||
if (client.crossDomain) {
|
||||
crossDomain = true;
|
||||
}
|
||||
cors.push(
|
||||
...(client.cors?.split(',') ?? []).map((c) =>
|
||||
stripTrailingSlash(c.trim()),
|
||||
),
|
||||
);
|
||||
await getClientByIdCached.clear(client.id);
|
||||
}
|
||||
|
||||
let domain = pickBestDomain(cors);
|
||||
|
||||
if (!domain) {
|
||||
const res = await chQuery<{ origin: string }>(
|
||||
`SELECT origin FROM events_distributed WHERE project_id = '${project.id}' and origin != ''`,
|
||||
);
|
||||
if (res.length) {
|
||||
domain = pickBestDomain(res.map((r) => r.origin));
|
||||
matches.push(domain);
|
||||
}
|
||||
}
|
||||
|
||||
await db.project.update({
|
||||
where: { id: project.id },
|
||||
data: {
|
||||
cors,
|
||||
crossDomain,
|
||||
domain,
|
||||
},
|
||||
});
|
||||
|
||||
await getProjectByIdCached.clear(project.id);
|
||||
}
|
||||
};
|
||||
88
packages/db/code-migrations/2-accounts.ts
Normal file
88
packages/db/code-migrations/2-accounts.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { db } from '../index';
|
||||
import { printBoxMessage } from './helpers';
|
||||
|
||||
const simpleCsvParser = (csv: string): Record<string, unknown>[] => {
|
||||
const rows = csv.split('\n');
|
||||
const headers = rows[0]!.split(',');
|
||||
return rows.slice(1).map((row) =>
|
||||
row.split(',').reduce(
|
||||
(acc, curr, index) => {
|
||||
acc[headers[index]!] = curr;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, unknown>,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
async function checkFileExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
return true; // File exists
|
||||
} catch (error) {
|
||||
return false; // File does not exist
|
||||
}
|
||||
}
|
||||
|
||||
export async function up() {
|
||||
const accountCount = await db.account.count();
|
||||
const userCount = await db.user.count();
|
||||
if (accountCount > 0) {
|
||||
printBoxMessage('⏭️ Skipping Migration ⏭️', ['Accounts already migrated']);
|
||||
return;
|
||||
}
|
||||
|
||||
if (userCount === 0) {
|
||||
printBoxMessage('⏭️ Skipping Migration ⏭️', [
|
||||
'No users found, skipping migration',
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
const dumppath = path.join(__dirname, 'users-dump.csv');
|
||||
// check if file exists
|
||||
if (!(await checkFileExists(dumppath))) {
|
||||
printBoxMessage('⚠️ Missing Required File ⚠️', [
|
||||
`File not found: ${dumppath}`,
|
||||
'This file is required to run this migration',
|
||||
'',
|
||||
'You can export it from:',
|
||||
'Clerk > Configure > Settings > Export all users',
|
||||
]);
|
||||
throw new Error('Required users dump file not found');
|
||||
}
|
||||
const csv = await fs.readFile(path.join(__dirname, 'users-dump.csv'), 'utf8');
|
||||
const data = simpleCsvParser(csv);
|
||||
|
||||
for (const row of data) {
|
||||
const email =
|
||||
row.primary_email_address ||
|
||||
row.verified_email_addresses ||
|
||||
row.unverified_email_addresses;
|
||||
|
||||
if (!email) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const user = await db.user.findUnique({
|
||||
where: {
|
||||
email: String(email),
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await db.account.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
provider: row.password_digest ? 'email' : 'oauth',
|
||||
providerId: null,
|
||||
password: row.password_digest ? String(row.password_digest) : null,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
13
packages/db/code-migrations/helpers.ts
Normal file
13
packages/db/code-migrations/helpers.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export function printBoxMessage(title: string, lines: (string | unknown)[]) {
|
||||
console.log('┌──┐');
|
||||
console.log('│');
|
||||
if (title) {
|
||||
console.log(`│ ${title}`);
|
||||
console.log('│');
|
||||
}
|
||||
lines.forEach((line) => {
|
||||
console.log(`│ ${line}`);
|
||||
});
|
||||
console.log('│');
|
||||
console.log('└──┘');
|
||||
}
|
||||
62
packages/db/code-migrations/migrate.ts
Normal file
62
packages/db/code-migrations/migrate.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { ch, db } from '../index';
|
||||
import { printBoxMessage } from './helpers';
|
||||
|
||||
async function migrate() {
|
||||
const args = process.argv.slice(2);
|
||||
const migration = args[0];
|
||||
|
||||
const migrationsDir = path.join(__dirname, '..', 'code-migrations');
|
||||
const migrations = fs.readdirSync(migrationsDir).filter((file) => {
|
||||
const version = file.split('-')[0];
|
||||
return (
|
||||
!Number.isNaN(Number.parseInt(version ?? '')) && file.endsWith('.ts')
|
||||
);
|
||||
});
|
||||
|
||||
if (migration) {
|
||||
await runMigration(migrationsDir, migration);
|
||||
} else {
|
||||
const finishedMigrations = await db.codeMigration.findMany();
|
||||
|
||||
for (const file of migrations) {
|
||||
if (finishedMigrations.some((migration) => migration.name === file)) {
|
||||
printBoxMessage('⏭️ Skipping Migration ⏭️', [`${file}`]);
|
||||
continue;
|
||||
}
|
||||
|
||||
await runMigration(migrationsDir, file);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Migrations finished');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
async function runMigration(migrationsDir: string, file: string) {
|
||||
printBoxMessage('⚡️ Running Migration ⚡️ ', [`${file}`]);
|
||||
try {
|
||||
const migration = await import(path.join(migrationsDir, file));
|
||||
await migration.up();
|
||||
await db.codeMigration.upsert({
|
||||
where: {
|
||||
name: file,
|
||||
},
|
||||
update: {
|
||||
name: file,
|
||||
},
|
||||
create: {
|
||||
name: file,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
printBoxMessage('❌ Migration Failed ❌', [
|
||||
`Error running migration ${file}:`,
|
||||
error,
|
||||
]);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
migrate();
|
||||
@@ -252,8 +252,9 @@ CREATE TABLE IF NOT EXISTS profile_aliases_distributed ON CLUSTER '{cluster}' AS
|
||||
-- +goose StatementBegin
|
||||
INSERT INTO events_replicated
|
||||
SELECT *
|
||||
FROM events_v2 -- +goose StatementEnd
|
||||
-- +goose StatementBegin
|
||||
FROM events_v2;
|
||||
-- +goose StatementEnd
|
||||
-- +goose StatementBegin
|
||||
INSERT INTO events_bots_replicated
|
||||
SELECT *
|
||||
FROM events_bots;
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
"goose": "pnpm with-env ./migrations/goose",
|
||||
"codegen": "pnpm with-env prisma generate",
|
||||
"migrate": "pnpm with-env prisma migrate dev",
|
||||
"migrate:deploy:db:code": "pnpm with-env jiti ./code-migrations/migrate.ts",
|
||||
"migrate:deploy:db": "pnpm with-env prisma migrate deploy",
|
||||
"migrate:deploy:ch": "pnpm goose up",
|
||||
"migrate:deploy": "pnpm migrate:deploy:db && pnpm migrate:deploy:ch",
|
||||
"migrate:deploy": "pnpm migrate:deploy:db && pnpm migrate:deploy:db:code && pnpm migrate:deploy:ch",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"with-env": "dotenv -e ../../.env -c --"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clerk/nextjs": "^5.0.2",
|
||||
"@clickhouse/client": "^1.2.0",
|
||||
"@openpanel/common": "workspace:*",
|
||||
"@openpanel/constants": "workspace:*",
|
||||
@@ -22,6 +22,7 @@
|
||||
"@openpanel/redis": "workspace:*",
|
||||
"@openpanel/validation": "workspace:*",
|
||||
"@prisma/client": "^5.1.1",
|
||||
"jiti": "^2.4.1",
|
||||
"prisma-json-types-generator": "^3.1.1",
|
||||
"ramda": "^0.29.1",
|
||||
"sqlstring": "^2.3.3",
|
||||
@@ -30,7 +31,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openpanel/tsconfig": "workspace:*",
|
||||
"@types/node": "^18.16.0",
|
||||
"@types/node": "20.14.8",
|
||||
"@types/ramda": "^0.29.6",
|
||||
"@types/sqlstring": "^2.3.2",
|
||||
"@types/uuid": "^9.0.8",
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Session" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `Session` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Session" DROP CONSTRAINT "Session_userId_fkey";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "Session";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "accounts" (
|
||||
"id" TEXT NOT NULL DEFAULT gen_random_uuid(),
|
||||
"userId" TEXT NOT NULL,
|
||||
"providerId" TEXT NOT NULL,
|
||||
"accessToken" TEXT,
|
||||
"refreshToken" TEXT,
|
||||
"accessTokenExpiresAt" TIMESTAMP(3),
|
||||
"refreshTokenExpiresAt" TIMESTAMP(3),
|
||||
"scope" TEXT,
|
||||
"password" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "accounts_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "sessions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "sessions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `provider` to the `accounts` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "accounts" ADD COLUMN "provider" TEXT NOT NULL,
|
||||
ALTER COLUMN "providerId" DROP NOT NULL;
|
||||
@@ -0,0 +1,19 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "invites" (
|
||||
"id" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"createdById" TEXT NOT NULL,
|
||||
"organizationId" TEXT NOT NULL,
|
||||
"projectAccess" TEXT[],
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "invites_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "invites" ADD CONSTRAINT "invites_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "invites" ADD CONSTRAINT "invites_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `role` to the `invites` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "invites" ADD COLUMN "role" TEXT NOT NULL;
|
||||
@@ -0,0 +1,11 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "members" DROP CONSTRAINT "members_organizationId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "members" DROP CONSTRAINT "members_userId_fkey";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "members" ADD CONSTRAINT "members_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "members" ADD CONSTRAINT "members_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,29 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "invites" DROP CONSTRAINT "invites_organizationId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "project_access" DROP CONSTRAINT "project_access_organizationId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "project_access" DROP CONSTRAINT "project_access_projectId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "project_access" DROP CONSTRAINT "project_access_userId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "projects" DROP CONSTRAINT "projects_organizationId_fkey";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "invites" ADD CONSTRAINT "invites_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "projects" ADD CONSTRAINT "projects_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "project_access" ADD CONSTRAINT "project_access_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "project_access" ADD CONSTRAINT "project_access_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "project_access" ADD CONSTRAINT "project_access_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "reset_password" (
|
||||
"id" TEXT NOT NULL,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "reset_password_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `accountId` to the `reset_password` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "reset_password" ADD COLUMN "accountId" TEXT NOT NULL;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "reset_password" ADD CONSTRAINT "reset_password_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "accounts"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "__code_migrations" (
|
||||
"id" TEXT NOT NULL DEFAULT gen_random_uuid(),
|
||||
"name" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "__code_migrations_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[name]` on the table `__code_migrations` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "__code_migrations_name_key" ON "__code_migrations"("name");
|
||||
@@ -15,6 +15,15 @@ datasource db {
|
||||
directUrl = env("DATABASE_URL_DIRECT")
|
||||
}
|
||||
|
||||
model CodeMigration {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()"))
|
||||
name String @unique
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
|
||||
@@map("__code_migrations")
|
||||
}
|
||||
|
||||
enum ProjectType {
|
||||
website
|
||||
app
|
||||
@@ -35,6 +44,7 @@ model Organization {
|
||||
Dashboard Dashboard[]
|
||||
ShareOverview ShareOverview[]
|
||||
integrations Integration[]
|
||||
invites Invite[]
|
||||
|
||||
@@map("organizations")
|
||||
}
|
||||
@@ -51,21 +61,54 @@ model User {
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
deletedAt DateTime?
|
||||
ProjectAccess ProjectAccess[]
|
||||
sessions Session[]
|
||||
accounts Account[]
|
||||
invites Invite[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model Account {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()"))
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
provider String
|
||||
providerId String?
|
||||
accessToken String?
|
||||
refreshToken String?
|
||||
accessTokenExpiresAt DateTime?
|
||||
refreshTokenExpiresAt DateTime?
|
||||
scope String?
|
||||
password String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
resetPasswords ResetPassword[]
|
||||
|
||||
@@map("accounts")
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id
|
||||
userId String
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
user User @relation(references: [id], fields: [userId], onDelete: Cascade)
|
||||
|
||||
@@map("sessions")
|
||||
}
|
||||
|
||||
model Member {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
role String
|
||||
email String
|
||||
// userId is nullable because we want to allow invites to be sent to emails that are not registered
|
||||
userId String?
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
invitedById String?
|
||||
invitedBy User? @relation("invitedBy", fields: [invitedById], references: [id])
|
||||
organizationId String
|
||||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
meta Json?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
@@ -73,10 +116,26 @@ model Member {
|
||||
@@map("members")
|
||||
}
|
||||
|
||||
model Invite {
|
||||
id String @id
|
||||
email String
|
||||
createdBy User @relation(fields: [createdById], references: [id])
|
||||
createdById String
|
||||
organizationId String
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
projectAccess String[]
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
role String
|
||||
|
||||
@@map("invites")
|
||||
}
|
||||
|
||||
model Project {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()"))
|
||||
name String
|
||||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
organizationId String
|
||||
eventsCount Int @default(0)
|
||||
types ProjectType[] @default([])
|
||||
@@ -114,11 +173,11 @@ enum AccessLevel {
|
||||
model ProjectAccess {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
projectId String
|
||||
project Project @relation(fields: [projectId], references: [id])
|
||||
organization Organization @relation(fields: [organizationId], references: [id])
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
|
||||
organizationId String
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
level AccessLevel
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
@@ -369,3 +428,14 @@ model Integration {
|
||||
|
||||
@@map("integrations")
|
||||
}
|
||||
|
||||
model ResetPassword {
|
||||
id String @id
|
||||
accountId String
|
||||
account Account @relation(fields: [accountId], references: [id])
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
|
||||
@@map("reset_password")
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
|
||||
import type { Organization, Prisma, ProjectAccess } from '../prisma-client';
|
||||
import type {
|
||||
Invite,
|
||||
Organization,
|
||||
Prisma,
|
||||
ProjectAccess,
|
||||
User,
|
||||
} from '../prisma-client';
|
||||
import { db } from '../prisma-client';
|
||||
|
||||
export type IServiceOrganization = ReturnType<typeof transformOrganization>;
|
||||
export type IServiceInvite = Prisma.MemberGetPayload<{
|
||||
include: { user: true };
|
||||
}>;
|
||||
export type IServiceInvite = Invite;
|
||||
export type IServiceMember = Prisma.MemberGetPayload<{
|
||||
include: { user: true };
|
||||
}> & { access: ProjectAccess[] };
|
||||
@@ -21,15 +23,14 @@ export function transformOrganization(org: Organization) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function getCurrentOrganizations() {
|
||||
const session = auth();
|
||||
if (!session.userId) return [];
|
||||
export async function getOrganizations(userId: string | null) {
|
||||
if (!userId) return [];
|
||||
|
||||
const organizations = await db.organization.findMany({
|
||||
where: {
|
||||
members: {
|
||||
some: {
|
||||
userId: session.userId,
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -67,13 +68,25 @@ export async function getOrganizationByProjectId(projectId: string) {
|
||||
}
|
||||
|
||||
export async function getInvites(organizationId: string) {
|
||||
return db.member.findMany({
|
||||
return db.invite.findMany({
|
||||
where: {
|
||||
organizationId,
|
||||
userId: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function getInviteById(inviteId: string) {
|
||||
return db.invite.findUnique({
|
||||
where: {
|
||||
id: inviteId,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
organization: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -112,3 +125,56 @@ export async function getMember(organizationId: string, userId: string) {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function connectUserToOrganization({
|
||||
user,
|
||||
inviteId,
|
||||
}: {
|
||||
user: User;
|
||||
inviteId: string;
|
||||
}) {
|
||||
const invite = await db.invite.findUnique({
|
||||
where: {
|
||||
id: inviteId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!invite) {
|
||||
throw new Error('Invite not found');
|
||||
}
|
||||
|
||||
if (invite.expiresAt < new Date()) {
|
||||
throw new Error('Invite expired');
|
||||
}
|
||||
|
||||
const member = await db.member.create({
|
||||
data: {
|
||||
organizationId: invite.organizationId,
|
||||
userId: user.id,
|
||||
role: invite.role,
|
||||
email: user.email,
|
||||
invitedById: invite.createdById,
|
||||
},
|
||||
});
|
||||
|
||||
if (invite.projectAccess.length > 0) {
|
||||
for (const projectId of invite.projectAccess) {
|
||||
await db.projectAccess.create({
|
||||
data: {
|
||||
projectId,
|
||||
userId: user.id,
|
||||
organizationId: invite.organizationId,
|
||||
level: 'write',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await db.invite.delete({
|
||||
where: {
|
||||
id: inviteId,
|
||||
},
|
||||
});
|
||||
|
||||
return member;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
|
||||
import { cacheable } from '@openpanel/redis';
|
||||
import type { Prisma, Project } from '../prisma-client';
|
||||
import { db } from '../prisma-client';
|
||||
@@ -55,9 +53,14 @@ export async function getProjectsByOrganizationId(organizationId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCurrentProjects(organizationId: string) {
|
||||
const session = auth();
|
||||
if (!session.userId) {
|
||||
export async function getProjects({
|
||||
organizationId,
|
||||
userId,
|
||||
}: {
|
||||
organizationId: string;
|
||||
userId: string | null;
|
||||
}) {
|
||||
if (!userId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -72,13 +75,13 @@ export async function getCurrentProjects(organizationId: string) {
|
||||
}),
|
||||
db.member.findMany({
|
||||
where: {
|
||||
userId: session.userId,
|
||||
userId,
|
||||
organizationId,
|
||||
},
|
||||
}),
|
||||
db.projectAccess.findMany({
|
||||
where: {
|
||||
userId: session.userId,
|
||||
userId,
|
||||
organizationId,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
|
||||
import { db } from '../prisma-client';
|
||||
|
||||
export async function getCurrentUser() {
|
||||
const session = auth();
|
||||
if (!session.userId) {
|
||||
return null;
|
||||
}
|
||||
return getUserById(session.userId);
|
||||
}
|
||||
|
||||
export async function getUserById(id: string) {
|
||||
return db.user.findUniqueOrThrow({
|
||||
where: {
|
||||
@@ -17,3 +7,33 @@ export async function getUserById(id: string) {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getUserAccount({
|
||||
email,
|
||||
provider,
|
||||
providerId,
|
||||
}: { email: string; provider: string; providerId?: string }) {
|
||||
const res = await db.user.findFirst({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
include: {
|
||||
accounts: {
|
||||
where: {
|
||||
provider,
|
||||
providerId: providerId ? String(providerId) : undefined,
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!res?.accounts[0]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...res,
|
||||
account: res?.accounts[0],
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user