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:
Carl-Gerhard Lindesvärd
2024-12-18 21:30:39 +01:00
committed by Carl-Gerhard Lindesvärd
parent f28802b1c2
commit d31d9924a5
151 changed files with 18484 additions and 12853 deletions

View 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);
}
};

View 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,
},
});
}
}

View 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('└──┘');
}

View 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();

View File

@@ -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;

View File

@@ -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",

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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")
);

View File

@@ -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;

View File

@@ -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")
);

View File

@@ -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");

View File

@@ -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")
}

View File

@@ -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;
}

View File

@@ -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,
},
}),

View File

@@ -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],
};
}