Skip to content
129 changes: 64 additions & 65 deletions apps/backend/prisma/schema.prisma

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CardView and EngagementEvent are doing the same job

Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")

}

model User {
Expand All @@ -23,15 +23,14 @@ model User {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

platformLinks PlatformLink[]
cards Card[]
oauthTokens OAuthToken[]
ownedViews CardView[] @relation("cardOwner")
viewedCards CardView[] @relation("cardViewer")
followLogs FollowLog[]
organizer Event[]
attendedEvents EventAttendee[]

platformLinks PlatformLink[]
cards Card[]
oauthTokens OAuthToken[]
ownedViews CardView[] @relation("cardOwner")
viewedCards CardView[] @relation("cardViewer")
followLogs FollowLog[]
organizer Event[]
attendedEvents EventAttendee[]
ownedTeams Team[] @relation("TeamOwner")
teamMemberships TeamMember[] @relation("TeamMember")

Expand All @@ -40,13 +39,13 @@ model User {
}

model PlatformLink {
id String @id @default(uuid())
userId String @map("user_id")
id String @id @default(uuid())
userId String @map("user_id")
platform String
username String
url String
displayOrder Int @default(0) @map("display_order")
createdAt DateTime @default(now()) @map("created_at")
displayOrder Int @default(0) @map("display_order")
createdAt DateTime @default(now()) @map("created_at")

user User @relation(fields: [userId], references: [id], onDelete: Cascade)
cardLinks CardLink[]
Expand All @@ -55,12 +54,12 @@ model PlatformLink {
}

model Card {
id String @id @default(uuid())
userId String @map("user_id")
id String @id @default(uuid())
userId String @map("user_id")
title String
isDefault Boolean @default(false) @map("is_default")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
isDefault Boolean @default(false) @map("is_default")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

user User @relation(fields: [userId], references: [id], onDelete: Cascade)
cardLinks CardLink[]
Expand Down Expand Up @@ -101,17 +100,17 @@ model OAuthToken {

model CardView {
id String @id @default(uuid())
cardId String? @map("card_id") // null = default profile view
ownerId String @map("owner_id") // card/profile owner
viewerId String? @map("viewer_id") // null = anonymous web viewer
viewerIp String? @map("viewer_ip")
cardId String? @map("card_id") // null = default profile view
ownerId String @map("owner_id") // card/profile owner
viewerId String? @map("viewer_id") // null = anonymous web viewer
viewerIp String? @map("viewer_ip") // SHA-256 hash of viewer IP
viewerAgent String? @map("viewer_agent")
source String @default("qr") // "qr" | "link" | "web" | "app"
source String @default("qr") // "qr" | "link" | "web" | "app"
createdAt DateTime @default(now()) @map("created_at")

card Card? @relation(fields: [cardId], references: [id], onDelete: SetNull)
owner User @relation("cardOwner", fields: [ownerId], references: [id], onDelete: Cascade)
viewer User? @relation("cardViewer", fields: [viewerId], references: [id], onDelete: SetNull)
card Card? @relation(fields: [cardId], references: [id], onDelete: SetNull)
owner User @relation("cardOwner", fields: [ownerId], references: [id], onDelete: Cascade)
viewer User? @relation("cardViewer", fields: [viewerId], references: [id], onDelete: SetNull)

@@map("card_views")
}
Expand All @@ -121,8 +120,8 @@ model FollowLog {
followerId String @map("follower_id")
targetUsername String @map("target_username")
platform String
status String @default("success") // "success" | "error"
layer String // "api" | "webview" | "link"
status String @default("success") // "success" | "error"
layer String // "api" | "webview" | "link"
createdAt DateTime @default(now()) @map("created_at")

follower User @relation(fields: [followerId], references: [id], onDelete: Cascade)
Expand All @@ -131,29 +130,29 @@ model FollowLog {
}

model Event {
id String @id @default(uuid())
name String
slug String @unique
location String
id String @id @default(uuid())
name String
slug String @unique
location String
description String?
organizerId String
startDate DateTime
endDate DateTime
isPublic Boolean @default(true)
createdAt DateTime @default(now()) @map("created_at")
attendees EventAttendee[]
organizerId String
startDate DateTime
endDate DateTime
isPublic Boolean @default(true)
createdAt DateTime @default(now()) @map("created_at")
attendees EventAttendee[]

organizer User @relation(fields: [organizerId], references: [id])
}

model EventAttendee {
id String @id @default(uuid())
userId String
eventId String
joinedAt DateTime
id String @id @default(uuid())
userId String
eventId String
joinedAt DateTime

event Event @relation(fields: [eventId] , references: [id])
user User @relation(fields: [userId],references: [id])
event Event @relation(fields: [eventId], references: [id])
user User @relation(fields: [userId], references: [id])

@@unique([userId, eventId])
}
Expand All @@ -164,34 +163,34 @@ enum TeamRole {
MEMBER
}

model Team{
id String @id @default(uuid())
name String
slug String @unique
description String?
avatarUrl String?
ownerId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

owner User @relation("TeamOwner", fields: [ownerId], references: [id], onDelete: Restrict)
model Team {
id String @id @default(uuid())
name String
slug String @unique
description String?
avatarUrl String?
ownerId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

owner User @relation("TeamOwner", fields: [ownerId], references: [id], onDelete: Restrict)
members TeamMember[] @relation("TeamMember")

@@map("teams")
@@index([slug])
}

model TeamMember{
id String @id @default(uuid())
teamId String
userId String
role TeamRole
joinedAt DateTime
model TeamMember {
id String @id @default(uuid())
teamId String
userId String
role TeamRole
joinedAt DateTime

team Team @relation("TeamMember",fields: [teamId] , references: [id], onDelete: Cascade)
user User @relation("TeamMember",fields: [userId] , references: [id])
team Team @relation("TeamMember", fields: [teamId], references: [id], onDelete: Cascade)
user User @relation("TeamMember", fields: [userId], references: [id])

@@unique([userId, teamId])
@@index([userId])
@@map("team_members")
}
}
68 changes: 36 additions & 32 deletions apps/backend/src/routes/auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';

Check failure on line 1 in apps/backend/src/routes/auth.ts

View workflow job for this annotation

GitHub Actions / backend-ci

There should be at least one empty line between import groups
import { encrypt } from '../utils/encryption.js';

Check failure on line 2 in apps/backend/src/routes/auth.ts

View workflow job for this annotation

GitHub Actions / backend-ci

`../utils/encryption.js` import should occur before type import of `fastify`
import { buildOAuthState, getMobileRedirectUri } from '../services/authService.js';

Check failure on line 3 in apps/backend/src/routes/auth.ts

View workflow job for this annotation

GitHub Actions / backend-ci

`../services/authService.js` import should occur before type import of `fastify`

Expand All @@ -14,7 +14,7 @@
state?: string;
}

export async function authRoutes(app: FastifyInstance) {

Check warning on line 17 in apps/backend/src/routes/auth.ts

View workflow job for this annotation

GitHub Actions / backend-ci

Missing return type on function
// Developer login bypass (development only)
if (process.env.NODE_ENV !== 'production') {
app.post('/dev-login', async (request: FastifyRequest, reply: FastifyReply) => {
Expand All @@ -30,8 +30,8 @@
// GitHub OAuth start
app.get('/github', async (request: FastifyRequest, reply: FastifyReply) => {
const redirectUri = `${process.env.BACKEND_URL}/auth/github/callback`;
const clientState = (request.query as any).state || '';
const mobileRedirectUri = (request.query as any).mobile_redirect_uri || '';
const clientState = (request.query as { state?: string }).state || '';
const mobileRedirectUri = (request.query as { mobile_redirect_uri?: string }).mobile_redirect_uri || '';
const state = buildOAuthState(clientState, mobileRedirectUri);

reply.setCookie('oauth_state', state, {
Expand Down Expand Up @@ -79,46 +79,46 @@
}),
});

const tokenData = (await tokenRes.json()) as any;
const tokenData = (await tokenRes.json()) as Record<string, unknown>;
if (tokenData.error) {
app.log.error({ tokenData }, 'GitHub token error');
return reply.status(400).send({ error: 'Failed to authenticate with GitHub' });
}

const userRes = await fetch(GITHUB_USER_URL, { headers: { Authorization: `Bearer ${tokenData.access_token}` } });

Check failure on line 88 in apps/backend/src/routes/auth.ts

View workflow job for this annotation

GitHub Actions / backend-ci

Invalid type "unknown" of template literal expression
const githubUser = (await userRes.json()) as any;
const githubUser = (await userRes.json()) as Record<string, unknown>;

let email = githubUser.email;
let email = githubUser.email as string | undefined;
if (!email) {
const emailsRes = await fetch('https://api.github.com/user/emails', {
headers: { Authorization: `Bearer ${tokenData.access_token}` },

Check failure on line 94 in apps/backend/src/routes/auth.ts

View workflow job for this annotation

GitHub Actions / backend-ci

Invalid type "unknown" of template literal expression
});
const emails = (await emailsRes.json()) as any[];
const primary = emails.find((e: any) => e.primary && e.verified);
const emails = (await emailsRes.json()) as Array<{ primary?: boolean; verified?: boolean; email?: string }>;
const primary = emails.find((e) => e.primary && e.verified);
email = primary?.email || emails[0]?.email;
}

const user = await app.prisma.user.upsert({
where: { provider_providerId: { provider: 'github', providerId: String(githubUser.id) } },
update: {
email: email || `${githubUser.login}@github.local`,

Check failure on line 104 in apps/backend/src/routes/auth.ts

View workflow job for this annotation

GitHub Actions / backend-ci

Invalid type "unknown" of template literal expression
displayName: githubUser.name || githubUser.login,
avatarUrl: githubUser.avatar_url,
displayName: (githubUser.name as string) || (githubUser.login as string),
avatarUrl: githubUser.avatar_url as string,
},
create: {
email: email || `${githubUser.login}@github.local`,

Check failure on line 109 in apps/backend/src/routes/auth.ts

View workflow job for this annotation

GitHub Actions / backend-ci

Invalid type "unknown" of template literal expression
username: githubUser.login,
displayName: githubUser.name || githubUser.login,
bio: githubUser.bio,
company: githubUser.company,
avatarUrl: githubUser.avatar_url,
username: githubUser.login as string,
displayName: (githubUser.name as string) || (githubUser.login as string),
bio: githubUser.bio as string | undefined,
company: githubUser.company as string | undefined,
avatarUrl: githubUser.avatar_url as string,
provider: 'github',
providerId: String(githubUser.id),
},
});

try {
const encryptedToken = encrypt(tokenData.access_token);
const encryptedToken = encrypt(tokenData.access_token as string);
await app.prisma.oAuthToken.upsert({
where: { userId_platform: { userId: user.id, platform: 'github' } },
update: { accessToken: encryptedToken, scopes: 'read:user user:email' },
Expand Down Expand Up @@ -153,8 +153,8 @@
// Google OAuth start
app.get('/google', async (request: FastifyRequest, reply: FastifyReply) => {
const redirectUri = `${process.env.BACKEND_URL}/auth/google/callback`;
const clientState = (request.query as any).state || '';
const mobileRedirectUri = (request.query as any).mobile_redirect_uri || '';
const clientState = (request.query as { state?: string }).state || '';
const mobileRedirectUri = (request.query as { mobile_redirect_uri?: string }).mobile_redirect_uri || '';
const state = buildOAuthState(clientState, mobileRedirectUri);

reply.setCookie('oauth_state', state, {
Expand Down Expand Up @@ -206,27 +206,31 @@
}),
});

const tokenData = (await tokenRes.json()) as any;
const tokenData = (await tokenRes.json()) as Record<string, unknown>;
if (tokenData.error) {
app.log.error({ tokenData }, 'Google token error');
return reply.status(400).send({ error: 'Failed to authenticate with Google' });
}

const userRes = await fetch(GOOGLE_USER_URL, { headers: { Authorization: `Bearer ${tokenData.access_token}` } });

Check failure on line 215 in apps/backend/src/routes/auth.ts

View workflow job for this annotation

GitHub Actions / backend-ci

Invalid type "unknown" of template literal expression
const googleUser = (await userRes.json()) as any;
const googleUser = (await userRes.json()) as Record<string, unknown>;

const baseUsername = googleUser.email.split('@')[0].replace(/[^a-zA-Z0-9_-]/g, '');
const baseUsername = (googleUser.email as string).split('@')[0].replace(/[^a-zA-Z0-9_-]/g, '');

const user = await app.prisma.user.upsert({
where: { provider_providerId: { provider: 'google', providerId: googleUser.id } },
update: { email: googleUser.email, displayName: googleUser.name || baseUsername, avatarUrl: googleUser.picture },
where: { provider_providerId: { provider: 'google', providerId: googleUser.id as string } },
update: {
email: googleUser.email as string,
displayName: (googleUser.name as string) || baseUsername,
avatarUrl: googleUser.picture as string,
},
create: {
email: googleUser.email,
email: googleUser.email as string,
username: `${baseUsername}_${Date.now().toString(36)}`,
displayName: googleUser.name || baseUsername,
avatarUrl: googleUser.picture,
displayName: (googleUser.name as string) || baseUsername,
avatarUrl: googleUser.picture as string,
provider: 'google',
providerId: googleUser.id,
providerId: googleUser.id as string,
},
});

Expand Down Expand Up @@ -254,12 +258,12 @@

// Current user
app.get('/me', { preHandler: [async (request, reply) => {
const server = request.server as any;
if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return }
if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return }
try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) }
const server = request.server as FastifyInstance;
if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return; }
if (typeof app.authenticate === 'function') { await app.authenticate(request, reply); return; }
try { await request.jwtVerify(); } catch { reply.status(401).send({ error: 'Unauthorized' }); }
}] }, async (request: FastifyRequest, reply: FastifyReply) => {
const userId = (request.user as any).id;
const userId = (request.user as { id: string }).id;
const user = await app.prisma.user.findUnique({
where: { id: userId },
select: {
Expand All @@ -286,7 +290,7 @@
return { ...userData, connectedPlatforms: oauthTokens };
});

app.post('/logout', async (request: FastifyRequest, reply: FastifyReply) => {
app.post('/logout', async (_request: FastifyRequest, reply: FastifyReply) => {
reply.clearCookie('token', { path: '/' });
return { message: 'Logged out' };
});
Expand Down
Loading
Loading