Skip to content
Closed
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
46 changes: 46 additions & 0 deletions apps/backend/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,51 @@
# DevCard Backend

## Authentication API

Local credential authentication is available as a backend foundation for future
profile-sharing features. These endpoints do not add any web or mobile UI.

### POST `/auth/register`

Creates a local user account, stores the password as a salted scrypt hash, and
returns an access token plus refresh token.

Request body:

```json
{
"email": "ada@example.com",
"username": "ada",
"displayName": "Ada Lovelace",
"password": "correct-horse-battery-staple"
}
```

Responses:

- `201` with `{ "user": { "id", "email", "username", "displayName" }, "accessToken", "refreshToken" }`
- `400` when validation fails
- `409` when the email or username is already registered

### POST `/auth/login`

Authenticates an existing local account by email and password.

Request body:

```json
{
"email": "ada@example.com",
"password": "correct-horse-battery-staple"
}
```

Responses:

- `200` with `{ "user": { "id", "email", "username", "displayName" }, "accessToken", "refreshToken" }`
- `400` when validation fails
- `401` when credentials are invalid

## Follow Engine Architecture

DevCard implements a multi-layered Hybrid Follow Engine designed to connect platform professionals seamlessly while maintaining platform policy compliance.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Add nullable password storage for local credential authentication.
-- OAuth-only accounts can continue to exist without a password hash.
ALTER TABLE "users" ADD COLUMN "password_hash" TEXT;
189 changes: 95 additions & 94 deletions apps/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -1,57 +1,59 @@
generator client {
provider = "prisma-client-js"
}

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

}
enum Role{

enum Role {
SUPERADMIN
ADMIN
USER

}

model User {
id String @id @default(uuid())
email String @unique
username String @unique
displayName String @map("display_name")
bio String?
pronouns String?
role String?
authRole Role @default(USER)
company String?
avatarUrl String? @map("avatar_url")
accentColor String @default("#6366f1") @map("accent_color")
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[]
ownedTeams Team[] @relation("TeamOwner")
teamMemberships TeamMember[] @relation("TeamMember")
id String @id @default(uuid())
email String @unique
username String @unique
displayName String @map("display_name")
bio String?
pronouns String?
role String?
authRole Role @default(USER)
company String?
avatarUrl String? @map("avatar_url")
passwordHash String? @map("password_hash")
accentColor String @default("#6366f1") @map("accent_color")
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[]
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
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)
Expand All @@ -61,17 +63,16 @@ model UserIdentity {
@@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
family String // token rotation
expiresAt DateTime @map("expires_at")
revokedAt DateTime? @map("revoked_at") // null = still valid
revokedAt DateTime? @map("revoked_at") // null = still valid
createdAt DateTime @default(now()) @map("created_at")
userAgent String? @map("user_agent")
ip String? //hash
userAgent String? @map("user_agent")
ip String? //hash

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

Expand All @@ -81,13 +82,13 @@ model RefreshToken {
}

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 @@ -96,12 +97,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 @@ -142,17 +143,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
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")
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 @@ -162,8 +163,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 @@ -172,29 +173,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 @@ -205,34 +206,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
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)
owner User @relation("TeamOwner", fields: [ownerId], references: [id], onDelete: Restrict)
members TeamMember[] @relation("TeamMember")

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

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