Skip to content

Commit 15af719

Browse files
committed
feat: implement authentication and developer profile system
1 parent db94586 commit 15af719

13 files changed

Lines changed: 972 additions & 48 deletions

File tree

apps/backend/src/app.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,13 @@ export async function buildApp():Promise<FastifyInstance> {
4747
done();
4848
});
4949

50+
await app.register(cookie);
51+
5052
// ─── Core Plugins ───
51-
await app.register(cors, {
52-
origin: process.env.PUBLIC_APP_URL || 'http://localhost:5173',
53-
credentials: true,
54-
});
53+
app.register(cors, {
54+
origin: 'http://localhost:5174',
55+
credentials: true,
56+
});
5557

5658
await app.register(helmet, {
5759
contentSecurityPolicy: {
@@ -71,11 +73,16 @@ export async function buildApp():Promise<FastifyInstance> {
7173
});
7274

7375
await app.register(jwt, {
74-
// validateEnv() above guarantees JWT_SECRET is present and safe.
75-
secret: process.env.JWT_SECRET!,
76-
});
77-
78-
await app.register(cookie);
76+
secret: process.env.JWT_SECRET!,
77+
sign: {
78+
expiresIn: '30d',
79+
},
80+
cookie: {
81+
cookieName: 'token',
82+
signed: false,
83+
},
84+
});
85+
7986
await app.register(multipart, { limits: { fileSize: 5 * 1024 * 1024 } }); // 5MB
8087
await app.register(rateLimit, {
8188
max: 100,

apps/backend/src/routes/auth.ts

Lines changed: 68 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -98,24 +98,38 @@ export async function authRoutes(app: FastifyInstance) {
9898
email = primary?.email || emails[0]?.email;
9999
}
100100

101-
const user = await app.prisma.user.upsert({
102-
where: { provider_providerId: { provider: 'github', providerId: String(githubUser.id) } },
103-
update: {
104-
email: email || `${githubUser.login}@github.local`,
105-
displayName: githubUser.name || githubUser.login,
106-
avatarUrl: githubUser.avatar_url,
107-
},
108-
create: {
109-
email: email || `${githubUser.login}@github.local`,
110-
username: githubUser.login,
111-
displayName: githubUser.name || githubUser.login,
112-
bio: githubUser.bio,
113-
company: githubUser.company,
114-
avatarUrl: githubUser.avatar_url,
115-
provider: 'github',
116-
providerId: String(githubUser.id),
117-
},
118-
});
101+
let user = await app.prisma.user.findUnique({
102+
where: {
103+
email: email || `${githubUser.login}@github.local`,
104+
},
105+
});
106+
107+
if (user) {
108+
user = await app.prisma.user.update({
109+
where: {
110+
email: email || `${githubUser.login}@github.local`,
111+
},
112+
data: {
113+
provider: 'github',
114+
providerId: String(githubUser.id),
115+
displayName: githubUser.name || githubUser.login,
116+
avatarUrl: githubUser.avatar_url,
117+
},
118+
});
119+
} else {
120+
user = await app.prisma.user.create({
121+
data: {
122+
email: email || `${githubUser.login}@github.local`,
123+
username: githubUser.login,
124+
displayName: githubUser.name || githubUser.login,
125+
bio: githubUser.bio,
126+
company: githubUser.company,
127+
avatarUrl: githubUser.avatar_url,
128+
provider: 'github',
129+
providerId: String(githubUser.id),
130+
},
131+
});
132+
}
119133

120134
try {
121135
const encryptedToken = encrypt(tokenData.access_token);
@@ -215,20 +229,42 @@ export async function authRoutes(app: FastifyInstance) {
215229
const userRes = await fetch(GOOGLE_USER_URL, { headers: { Authorization: `Bearer ${tokenData.access_token}` } });
216230
const googleUser = (await userRes.json()) as any;
217231

218-
const baseUsername = googleUser.email.split('@')[0].replace(/[^a-zA-Z0-9_-]/g, '');
219-
220-
const user = await app.prisma.user.upsert({
221-
where: { provider_providerId: { provider: 'google', providerId: googleUser.id } },
222-
update: { email: googleUser.email, displayName: googleUser.name || baseUsername, avatarUrl: googleUser.picture },
223-
create: {
224-
email: googleUser.email,
225-
username: `${baseUsername}_${Date.now().toString(36)}`,
226-
displayName: googleUser.name || baseUsername,
227-
avatarUrl: googleUser.picture,
228-
provider: 'google',
229-
providerId: googleUser.id,
230-
},
231-
});
232+
const baseUsername = googleUser.email
233+
.split('@')[0]
234+
.replace(/[^a-zA-Z0-9_-]/g, '');
235+
236+
const existingUser = await app.prisma.user.findUnique({
237+
where: {
238+
email: googleUser.email,
239+
},
240+
});
241+
242+
let user;
243+
244+
if (existingUser) {
245+
user = await app.prisma.user.update({
246+
where: {
247+
email: googleUser.email,
248+
},
249+
data: {
250+
provider: 'google',
251+
providerId: googleUser.id,
252+
displayName: googleUser.name || baseUsername,
253+
avatarUrl: googleUser.picture,
254+
},
255+
});
256+
} else {
257+
user = await app.prisma.user.create({
258+
data: {
259+
email: googleUser.email,
260+
username: `${baseUsername}_${Date.now().toString(36)}`,
261+
displayName: googleUser.name || baseUsername,
262+
avatarUrl: googleUser.picture,
263+
provider: 'google',
264+
providerId: googleUser.id,
265+
},
266+
});
267+
}
232268

233269
const token = app.jwt.sign({ id: user.id, username: user.username }, { expiresIn: '30d' });
234270

apps/web/src/App.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,23 @@ import LandingPage from './pages/LandingPage';
33
import ProfilePage from './pages/ProfilePage';
44
import CardPage from './pages/CardPage';
55
import NotFound from './pages/NotFound';
6-
6+
import CreatePage from './pages/CreatePage';
7+
import LoginPage from './pages/LoginPage';
8+
import DashboardPage from './pages/DashboardPage';
9+
import EditProfilePage from './pages/EditProfilePage';
10+
import PublicProfilePage from './pages/PublicProfilePage';
711
export default function App() {
812
return (
913
<Routes>
1014
<Route path="/" element={<LandingPage />} />
1115
<Route path="/u/:username" element={<ProfilePage />} />
1216
<Route path="/devcard/:id" element={<CardPage />} />
1317
<Route path="*" element={<NotFound />} />
18+
<Route path="/create" element={<CreatePage />} />
19+
<Route path="/login" element={<LoginPage />} />
20+
<Route path="/dashboard" element={<DashboardPage />} />
21+
<Route path="/edit-profile" element={<EditProfilePage />} />
22+
<Route path="/u/:username" element={<PublicProfilePage />} />
1423
</Routes>
1524
);
1625
}

apps/web/src/lib/api.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
11
const API_BASE_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:3000';
22

3-
export async function apiFetch<T>(endpoint: string): Promise<T> {
3+
export async function apiFetch<T>(
4+
endpoint: string,
5+
options: RequestInit = {}
6+
): Promise<T> {
47
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
5-
headers: { 'Content-Type': 'application/json' },
8+
...options,
9+
credentials: 'include', // IMPORTANT
10+
headers: {
11+
'Content-Type': 'application/json',
12+
...(options.headers || {}),
13+
},
614
});
715

816
if (!response.ok) {
917
const error = await response.json().catch(() => ({}));
18+
1019
throw new Error(
11-
(error as Record<string, string>)?.message ?? `Request failed: ${response.status}`
20+
(error as Record<string, string>)?.message ??
21+
`Request failed: ${response.status}`
1222
);
1323
}
1424

1525
return response.json() as Promise<T>;
16-
}
26+
}

apps/web/src/pages/CreatePage.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export default function CreatePage() {
2+
return (
3+
<div
4+
style={{
5+
minHeight: '100vh',
6+
display: 'flex',
7+
justifyContent: 'center',
8+
alignItems: 'center',
9+
background: '#0f172a',
10+
color: 'white',
11+
flexDirection: 'column',
12+
gap: '1rem',
13+
}}
14+
>
15+
<h1>Create Your DevCard 🚀</h1>
16+
<p>This page is under development.</p>
17+
</div>
18+
);
19+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
.dashboard-page {
2+
min-height: 100vh;
3+
background: #0f172a;
4+
display: flex;
5+
justify-content: center;
6+
align-items: center;
7+
padding: 2rem;
8+
}
9+
10+
.dashboard-card {
11+
width: 100%;
12+
max-width: 500px;
13+
background: rgba(255,255,255,0.06);
14+
border: 1px solid rgba(255,255,255,0.08);
15+
backdrop-filter: blur(20px);
16+
border-radius: 24px;
17+
padding: 2.5rem;
18+
color: white;
19+
text-align: center;
20+
}
21+
22+
.dashboard-avatar {
23+
width: 110px;
24+
height: 110px;
25+
border-radius: 50%;
26+
object-fit: cover;
27+
margin-bottom: 1rem;
28+
}
29+
30+
.username {
31+
color: #a5b4fc;
32+
margin-top: 0.5rem;
33+
}
34+
35+
.email {
36+
color: #cbd5e1;
37+
margin-top: 0.5rem;
38+
}
39+
40+
.bio {
41+
margin-top: 1rem;
42+
color: #e2e8f0;
43+
line-height: 1.6;
44+
}
45+
46+
.logout-btn {
47+
margin-top: 2rem;
48+
border: none;
49+
background: #ef4444;
50+
color: white;
51+
padding: 0.9rem 1.4rem;
52+
border-radius: 12px;
53+
cursor: pointer;
54+
font-weight: 600;
55+
}

0 commit comments

Comments
 (0)