Skip to content

Commit 74888d6

Browse files
author
Amrit
committed
feat(backend): add local auth foundation
1 parent 82a256c commit 74888d6

6 files changed

Lines changed: 576 additions & 95 deletions

File tree

apps/backend/README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,51 @@
11
# DevCard Backend
22

3+
## Authentication API
4+
5+
Local credential authentication is available as a backend foundation for future
6+
profile-sharing features. These endpoints do not add any web or mobile UI.
7+
8+
### POST `/auth/register`
9+
10+
Creates a local user account, stores the password as a salted scrypt hash, and
11+
returns an access token plus refresh token.
12+
13+
Request body:
14+
15+
```json
16+
{
17+
"email": "ada@example.com",
18+
"username": "ada",
19+
"displayName": "Ada Lovelace",
20+
"password": "correct-horse-battery-staple"
21+
}
22+
```
23+
24+
Responses:
25+
26+
- `201` with `{ "user": { "id", "email", "username", "displayName" }, "accessToken", "refreshToken" }`
27+
- `400` when validation fails
28+
- `409` when the email or username is already registered
29+
30+
### POST `/auth/login`
31+
32+
Authenticates an existing local account by email and password.
33+
34+
Request body:
35+
36+
```json
37+
{
38+
"email": "ada@example.com",
39+
"password": "correct-horse-battery-staple"
40+
}
41+
```
42+
43+
Responses:
44+
45+
- `200` with `{ "user": { "id", "email", "username", "displayName" }, "accessToken", "refreshToken" }`
46+
- `400` when validation fails
47+
- `401` when credentials are invalid
48+
349
## Follow Engine Architecture
450

551
DevCard implements a multi-layered Hybrid Follow Engine designed to connect platform professionals seamlessly while maintaining platform policy compliance.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- Add nullable password storage for local credential authentication.
2+
-- OAuth-only accounts can continue to exist without a password hash.
3+
ALTER TABLE "users" ADD COLUMN "password_hash" TEXT;

apps/backend/prisma/schema.prisma

Lines changed: 95 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,59 @@
11
generator client {
22
provider = "prisma-client-js"
33
}
4+
45
datasource db {
56
provider = "postgresql"
67
url = env("DATABASE_URL")
7-
88
}
9-
enum Role{
9+
10+
enum Role {
1011
SUPERADMIN
1112
ADMIN
1213
USER
13-
1414
}
15+
1516
model User {
16-
id String @id @default(uuid())
17-
email String @unique
18-
username String @unique
19-
displayName String @map("display_name")
20-
bio String?
21-
pronouns String?
22-
role String?
23-
authRole Role @default(USER)
24-
company String?
25-
avatarUrl String? @map("avatar_url")
26-
accentColor String @default("#6366f1") @map("accent_color")
27-
emailVerified Boolean @default(false) @map("email_verified")
28-
phoneNumber String? @unique @map("phone_number")
29-
lastSignInAt DateTime? @map("last_sign_in_at")
30-
createdAt DateTime @default(now()) @map("created_at")
31-
updatedAt DateTime @updatedAt @map("updated_at")
32-
isActive Boolean @default(false)
33-
34-
identities UserIdentity[]
35-
refreshTokens RefreshToken[]
36-
platformLinks PlatformLink[]
37-
cards Card[]
38-
oauthTokens OAuthToken[]
39-
ownedViews CardView[] @relation("cardOwner")
40-
viewedCards CardView[] @relation("cardViewer")
41-
followLogs FollowLog[]
42-
organizer Event[]
43-
attendedEvents EventAttendee[]
44-
ownedTeams Team[] @relation("TeamOwner")
45-
teamMemberships TeamMember[] @relation("TeamMember")
17+
id String @id @default(uuid())
18+
email String @unique
19+
username String @unique
20+
displayName String @map("display_name")
21+
bio String?
22+
pronouns String?
23+
role String?
24+
authRole Role @default(USER)
25+
company String?
26+
avatarUrl String? @map("avatar_url")
27+
passwordHash String? @map("password_hash")
28+
accentColor String @default("#6366f1") @map("accent_color")
29+
emailVerified Boolean @default(false) @map("email_verified")
30+
phoneNumber String? @unique @map("phone_number")
31+
lastSignInAt DateTime? @map("last_sign_in_at")
32+
createdAt DateTime @default(now()) @map("created_at")
33+
updatedAt DateTime @updatedAt @map("updated_at")
34+
isActive Boolean @default(false)
35+
36+
identities UserIdentity[]
37+
refreshTokens RefreshToken[]
38+
platformLinks PlatformLink[]
39+
cards Card[]
40+
oauthTokens OAuthToken[]
41+
ownedViews CardView[] @relation("cardOwner")
42+
viewedCards CardView[] @relation("cardViewer")
43+
followLogs FollowLog[]
44+
organizer Event[]
45+
attendedEvents EventAttendee[]
46+
ownedTeams Team[] @relation("TeamOwner")
47+
teamMemberships TeamMember[] @relation("TeamMember")
4648
4749
@@map("users")
4850
}
4951

5052
model UserIdentity {
5153
id String @id @default(uuid())
5254
userId String @map("user_id")
53-
provider String // "google.com" | "apple.com" | "firebase" | "phone"
54-
providerId String @map("provider_id") // Google sub / Apple sub / Firebase UID
55+
provider String // "google.com" | "apple.com" | "firebase" | "phone"
56+
providerId String @map("provider_id") // Google sub / Apple sub / Firebase UID
5557
createdAt DateTime @default(now()) @map("created_at")
5658
5759
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@ -61,17 +63,16 @@ model UserIdentity {
6163
@@map("user_identities")
6264
}
6365

64-
6566
model RefreshToken {
6667
id String @id @default(uuid())
6768
userId String @map("user_id")
6869
tokenHash String @unique @map("token_hash") //SHA-256 hash
69-
family String // token rotation
70+
family String // token rotation
7071
expiresAt DateTime @map("expires_at")
71-
revokedAt DateTime? @map("revoked_at") // null = still valid
72+
revokedAt DateTime? @map("revoked_at") // null = still valid
7273
createdAt DateTime @default(now()) @map("created_at")
73-
userAgent String? @map("user_agent")
74-
ip String? //hash
74+
userAgent String? @map("user_agent")
75+
ip String? //hash
7576
7677
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
7778
@@ -81,13 +82,13 @@ model RefreshToken {
8182
}
8283

8384
model PlatformLink {
84-
id String @id @default(uuid())
85-
userId String @map("user_id")
85+
id String @id @default(uuid())
86+
userId String @map("user_id")
8687
platform String
8788
username String
8889
url String
89-
displayOrder Int @default(0) @map("display_order")
90-
createdAt DateTime @default(now()) @map("created_at")
90+
displayOrder Int @default(0) @map("display_order")
91+
createdAt DateTime @default(now()) @map("created_at")
9192
9293
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
9394
cardLinks CardLink[]
@@ -96,12 +97,12 @@ model PlatformLink {
9697
}
9798

9899
model Card {
99-
id String @id @default(uuid())
100-
userId String @map("user_id")
100+
id String @id @default(uuid())
101+
userId String @map("user_id")
101102
title String
102-
isDefault Boolean @default(false) @map("is_default")
103-
createdAt DateTime @default(now()) @map("created_at")
104-
updatedAt DateTime @updatedAt @map("updated_at")
103+
isDefault Boolean @default(false) @map("is_default")
104+
createdAt DateTime @default(now()) @map("created_at")
105+
updatedAt DateTime @updatedAt @map("updated_at")
105106
106107
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
107108
cardLinks CardLink[]
@@ -142,17 +143,17 @@ model OAuthToken {
142143

143144
model CardView {
144145
id String @id @default(uuid())
145-
cardId String? @map("card_id") // null = default profile view
146-
ownerId String @map("owner_id") // card/profile owner
147-
viewerId String? @map("viewer_id") // null = anonymous web viewer
146+
cardId String? @map("card_id") // null = default profile view
147+
ownerId String @map("owner_id") // card/profile owner
148+
viewerId String? @map("viewer_id") // null = anonymous web viewer
148149
viewerIp String? @map("viewer_ip")
149150
viewerAgent String? @map("viewer_agent")
150-
source String @default("qr") // "qr" | "link" | "web" | "app"
151+
source String @default("qr") // "qr" | "link" | "web" | "app"
151152
createdAt DateTime @default(now()) @map("created_at")
152153
153-
card Card? @relation(fields: [cardId], references: [id], onDelete: SetNull)
154-
owner User @relation("cardOwner", fields: [ownerId], references: [id], onDelete: Cascade)
155-
viewer User? @relation("cardViewer", fields: [viewerId], references: [id], onDelete: SetNull)
154+
card Card? @relation(fields: [cardId], references: [id], onDelete: SetNull)
155+
owner User @relation("cardOwner", fields: [ownerId], references: [id], onDelete: Cascade)
156+
viewer User? @relation("cardViewer", fields: [viewerId], references: [id], onDelete: SetNull)
156157
157158
@@map("card_views")
158159
}
@@ -162,8 +163,8 @@ model FollowLog {
162163
followerId String @map("follower_id")
163164
targetUsername String @map("target_username")
164165
platform String
165-
status String @default("success") // "success" | "error"
166-
layer String // "api" | "webview" | "link"
166+
status String @default("success") // "success" | "error"
167+
layer String // "api" | "webview" | "link"
167168
createdAt DateTime @default(now()) @map("created_at")
168169
169170
follower User @relation(fields: [followerId], references: [id], onDelete: Cascade)
@@ -172,29 +173,29 @@ model FollowLog {
172173
}
173174

174175
model Event {
175-
id String @id @default(uuid())
176-
name String
177-
slug String @unique
178-
location String
176+
id String @id @default(uuid())
177+
name String
178+
slug String @unique
179+
location String
179180
description String?
180-
organizerId String
181-
startDate DateTime
182-
endDate DateTime
183-
isPublic Boolean @default(true)
184-
createdAt DateTime @default(now()) @map("created_at")
185-
attendees EventAttendee[]
181+
organizerId String
182+
startDate DateTime
183+
endDate DateTime
184+
isPublic Boolean @default(true)
185+
createdAt DateTime @default(now()) @map("created_at")
186+
attendees EventAttendee[]
186187
187188
organizer User @relation(fields: [organizerId], references: [id])
188189
}
189190

190191
model EventAttendee {
191-
id String @id @default(uuid())
192-
userId String
193-
eventId String
194-
joinedAt DateTime
192+
id String @id @default(uuid())
193+
userId String
194+
eventId String
195+
joinedAt DateTime
195196
196-
event Event @relation(fields: [eventId] , references: [id])
197-
user User @relation(fields: [userId],references: [id])
197+
event Event @relation(fields: [eventId], references: [id])
198+
user User @relation(fields: [userId], references: [id])
198199
199200
@@unique([userId, eventId])
200201
}
@@ -205,34 +206,34 @@ enum TeamRole {
205206
MEMBER
206207
}
207208

208-
model Team{
209-
id String @id @default(uuid())
210-
name String
211-
slug String @unique
212-
description String?
213-
avatarUrl String?
214-
ownerId String
215-
createdAt DateTime @default(now())
216-
updatedAt DateTime @updatedAt
209+
model Team {
210+
id String @id @default(uuid())
211+
name String
212+
slug String @unique
213+
description String?
214+
avatarUrl String?
215+
ownerId String
216+
createdAt DateTime @default(now())
217+
updatedAt DateTime @updatedAt
217218
218-
owner User @relation("TeamOwner", fields: [ownerId], references: [id], onDelete: Restrict)
219+
owner User @relation("TeamOwner", fields: [ownerId], references: [id], onDelete: Restrict)
219220
members TeamMember[] @relation("TeamMember")
220221
221-
@@map("teams")
222222
@@index([slug])
223+
@@map("teams")
223224
}
224225

225-
model TeamMember{
226-
id String @id @default(uuid())
227-
teamId String
228-
userId String
229-
role TeamRole
230-
joinedAt DateTime
226+
model TeamMember {
227+
id String @id @default(uuid())
228+
teamId String
229+
userId String
230+
role TeamRole
231+
joinedAt DateTime
231232
232-
team Team @relation("TeamMember",fields: [teamId] , references: [id], onDelete: Cascade)
233-
user User @relation("TeamMember",fields: [userId] , references: [id])
233+
team Team @relation("TeamMember", fields: [teamId], references: [id], onDelete: Cascade)
234+
user User @relation("TeamMember", fields: [userId], references: [id])
234235
235236
@@unique([userId, teamId])
236237
@@index([userId])
237238
@@map("team_members")
238-
}
239+
}

0 commit comments

Comments
 (0)