Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 49 additions & 8 deletions apps/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ datasource db {
url = env("DATABASE_URL")

}

enum Role{
SUPERADMIN
ADMIN
USER

}
model User {
id String @id @default(uuid())
email String @unique
Expand All @@ -15,28 +20,64 @@ model User {
bio String?
pronouns String?
role String?
authRole Role @default(USER)
company String?
avatarUrl String? @map("avatar_url")
accentColor String @default("#6366f1") @map("accent_color")
provider String
providerId String @map("provider_id")
emailVerified Boolean @default(false) @map("email_verified")
phoneNumber String? @unique @map("phone_number")
lastSignInAt DateTime? @map("last_sign_in_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
isActive Boolean @default(false)

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

ownedTeams Team[] @relation("TeamOwner")
teamMemberships TeamMember[] @relation("TeamMember")
@@map("users")
}

model UserIdentity {
id String @id @default(uuid())
userId String @map("user_id")
provider String // "google.com" | "apple.com" | "firebase" | "phone"
providerId String @map("provider_id") // Google sub / Apple sub / Firebase UID
createdAt DateTime @default(now()) @map("created_at")

user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@unique([provider, providerId])
@@map("users")
@@index([userId])
@@map("user_identities")
}


model RefreshToken {
id String @id @default(uuid())
userId String @map("user_id")
tokenHash String @unique @map("token_hash") //SHA-256 hash
family String // token rotation
expiresAt DateTime @map("expires_at")
revokedAt DateTime? @map("revoked_at") // null = still valid
createdAt DateTime @default(now()) @map("created_at")
userAgent String? @map("user_agent")
ip String? //hash

user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@index([userId])
@@index([family])
@@map("refresh_tokens")
}

model PlatformLink {
Expand Down
22 changes: 11 additions & 11 deletions apps/backend/src/__tests__/logout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ async function buildTestApp(mockRedis: MockRedis): Promise<FastifyInstance> {
// in app.ts so that both Authorization header and token cookie are accepted.
await app.register(jwtPlugin as any, {
secret: TEST_JWT_SECRET,
cookie: { cookieName: 'token', signed: false },
cookie: { cookieName: 'access_Token', signed: false },
});

// Minimal Prisma stub. The logout route does not touch the database, but
Expand Down Expand Up @@ -264,7 +264,7 @@ describe('DELETE /auth/logout', () => {
const res = await app.inject({
method: 'DELETE',
url: '/auth/logout',
headers: { Cookie: `token=${token}` },
headers: { Cookie: `access_Token=${token}` },
});

expect(res.statusCode).toBe(200);
Expand All @@ -284,7 +284,7 @@ describe('DELETE /auth/logout', () => {
url: '/auth/logout',
headers: {
Authorization: `Bearer ${headerToken}`,
Cookie: `token=${cookieToken}`,
Cookie: `access_Token=${cookieToken}`,
},
});

Expand All @@ -309,7 +309,7 @@ describe('DELETE /auth/logout', () => {
const raw = res.headers['set-cookie'] as string | string[];
const cookieStr = Array.isArray(raw) ? raw.join('; ') : (raw ?? '');
// Value must be emptied.
expect(cookieStr).toMatch(/token=;/);
expect(cookieStr).toMatch(/access_Token=;/);
// Path must be explicit so the browser clears the cookie on all routes.
expect(cookieStr).toMatch(/Path=\//i);
// Browser must be told to delete the cookie immediately.
Expand Down Expand Up @@ -350,7 +350,7 @@ describe('DELETE /auth/logout', () => {
expect(mockRedis.set).not.toHaveBeenCalled();
expect(warnMock).toHaveBeenCalledOnce();
// Verify the message identifies the root cause clearly.
const [, message] = warnMock.mock.calls[0] as [unknown, string];
const [message] = warnMock.mock.calls[0] as [string];
expect(message).toMatch(/missing exp/i);
});

Expand Down Expand Up @@ -471,7 +471,7 @@ describe('authenticate middleware', () => {
const res = await app.inject({
method: 'GET',
url: '/protected',
headers: { Cookie: `token=${token}` },
headers: { Cookie: `access_Token=${token}` },
});

expect(res.statusCode).toBe(200);
Expand Down Expand Up @@ -574,7 +574,7 @@ describe('revocation flow — end-to-end', () => {
const logout = await app.inject({
method: 'DELETE',
url: '/auth/logout',
headers: { Cookie: `token=${token}` },
headers: { Cookie: `access_Token=${token}` },
});
expect(logout.statusCode).toBe(200);
expect(mockRedis.set).toHaveBeenCalledOnce();
Expand All @@ -588,7 +588,7 @@ describe('revocation flow — end-to-end', () => {
const after = await app.inject({
method: 'GET',
url: '/protected',
headers: { Cookie: `token=${token}` },
headers: { Cookie: `access_Token=${token}` },
});
expect(after.statusCode).toBe(401);
expect(after.json().error).toBe('Token has been revoked');
Expand Down Expand Up @@ -637,14 +637,14 @@ describe('extractRawJwt', () => {
});

it('returns token from cookie when no Authorization header', () => {
const req = makeRequest({ cookies: { token: 'cookie.jwt.token' } });
const req = makeRequest({ cookies: { access_Token: 'cookie.jwt.token' } });
expect(extractRawJwt(req)).toBe('cookie.jwt.token');
});

it('prefers Authorization header over cookie', () => {
const req = makeRequest({
authorization: 'Bearer header.jwt.token',
cookies: { token: 'cookie.jwt.token' },
cookies: { access_Token: 'cookie.jwt.token' },
});
expect(extractRawJwt(req)).toBe('header.jwt.token');
});
Expand All @@ -666,7 +666,7 @@ describe('extractRawJwt', () => {
});

it('returns null when the token cookie value is empty', () => {
const req = makeRequest({ cookies: { token: '' } });
const req = makeRequest({ cookies: { access_Token: '' } });
// || null normalises the empty string to null, matching the return type.
expect(extractRawJwt(req)).toBeNull();
});
Expand Down
Loading