From 15af7199fd74641dedfdfb21664d7ff988b2dd60 Mon Sep 17 00:00:00 2001 From: Kishor0009 Date: Mon, 8 Jun 2026 23:43:39 +0530 Subject: [PATCH 1/3] feat: implement authentication and developer profile system --- apps/backend/src/app.ts | 25 +- apps/backend/src/routes/auth.ts | 100 ++++--- apps/web/src/App.tsx | 11 +- apps/web/src/lib/api.ts | 18 +- apps/web/src/pages/CreatePage.tsx | 19 ++ apps/web/src/pages/DashboardPage.css | 55 ++++ apps/web/src/pages/DashboardPage.tsx | 115 ++++++++ apps/web/src/pages/EditProfilePage.tsx | 332 +++++++++++++++++++++++ apps/web/src/pages/LandingPage.tsx | 2 +- apps/web/src/pages/LoginPage.css | 77 ++++++ apps/web/src/pages/LoginPage.tsx | 47 ++++ apps/web/src/pages/ProfilePage.tsx | 2 +- apps/web/src/pages/PublicProfilePage.tsx | 217 +++++++++++++++ 13 files changed, 972 insertions(+), 48 deletions(-) create mode 100644 apps/web/src/pages/CreatePage.tsx create mode 100644 apps/web/src/pages/DashboardPage.css create mode 100644 apps/web/src/pages/DashboardPage.tsx create mode 100644 apps/web/src/pages/EditProfilePage.tsx create mode 100644 apps/web/src/pages/LoginPage.css create mode 100644 apps/web/src/pages/LoginPage.tsx create mode 100644 apps/web/src/pages/PublicProfilePage.tsx diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index be0b27e9..ad890a51 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -47,11 +47,13 @@ export async function buildApp():Promise { done(); }); + await app.register(cookie); + // ─── Core Plugins ─── - await app.register(cors, { - origin: process.env.PUBLIC_APP_URL || 'http://localhost:5173', - credentials: true, - }); + app.register(cors, { + origin: 'http://localhost:5174', + credentials: true, +}); await app.register(helmet, { contentSecurityPolicy: { @@ -71,11 +73,16 @@ export async function buildApp():Promise { }); await app.register(jwt, { - // validateEnv() above guarantees JWT_SECRET is present and safe. - secret: process.env.JWT_SECRET!, - }); - - await app.register(cookie); + secret: process.env.JWT_SECRET!, + sign: { + expiresIn: '30d', + }, + cookie: { + cookieName: 'token', + signed: false, + }, +}); + await app.register(multipart, { limits: { fileSize: 5 * 1024 * 1024 } }); // 5MB await app.register(rateLimit, { max: 100, diff --git a/apps/backend/src/routes/auth.ts b/apps/backend/src/routes/auth.ts index c14949e1..70aa28ba 100644 --- a/apps/backend/src/routes/auth.ts +++ b/apps/backend/src/routes/auth.ts @@ -98,24 +98,38 @@ export async function authRoutes(app: FastifyInstance) { 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`, - displayName: githubUser.name || githubUser.login, - avatarUrl: githubUser.avatar_url, - }, - create: { - email: email || `${githubUser.login}@github.local`, - username: githubUser.login, - displayName: githubUser.name || githubUser.login, - bio: githubUser.bio, - company: githubUser.company, - avatarUrl: githubUser.avatar_url, - provider: 'github', - providerId: String(githubUser.id), - }, - }); + let user = await app.prisma.user.findUnique({ + where: { + email: email || `${githubUser.login}@github.local`, + }, +}); + +if (user) { + user = await app.prisma.user.update({ + where: { + email: email || `${githubUser.login}@github.local`, + }, + data: { + provider: 'github', + providerId: String(githubUser.id), + displayName: githubUser.name || githubUser.login, + avatarUrl: githubUser.avatar_url, + }, + }); +} else { + user = await app.prisma.user.create({ + data: { + email: email || `${githubUser.login}@github.local`, + username: githubUser.login, + displayName: githubUser.name || githubUser.login, + bio: githubUser.bio, + company: githubUser.company, + avatarUrl: githubUser.avatar_url, + provider: 'github', + providerId: String(githubUser.id), + }, + }); +} try { const encryptedToken = encrypt(tokenData.access_token); @@ -215,20 +229,42 @@ export async function authRoutes(app: FastifyInstance) { const userRes = await fetch(GOOGLE_USER_URL, { headers: { Authorization: `Bearer ${tokenData.access_token}` } }); const googleUser = (await userRes.json()) as any; - const baseUsername = googleUser.email.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 }, - create: { - email: googleUser.email, - username: `${baseUsername}_${Date.now().toString(36)}`, - displayName: googleUser.name || baseUsername, - avatarUrl: googleUser.picture, - provider: 'google', - providerId: googleUser.id, - }, - }); + const baseUsername = googleUser.email + .split('@')[0] + .replace(/[^a-zA-Z0-9_-]/g, ''); + +const existingUser = await app.prisma.user.findUnique({ + where: { + email: googleUser.email, + }, +}); + +let user; + +if (existingUser) { + user = await app.prisma.user.update({ + where: { + email: googleUser.email, + }, + data: { + provider: 'google', + providerId: googleUser.id, + displayName: googleUser.name || baseUsername, + avatarUrl: googleUser.picture, + }, + }); +} else { + user = await app.prisma.user.create({ + data: { + email: googleUser.email, + username: `${baseUsername}_${Date.now().toString(36)}`, + displayName: googleUser.name || baseUsername, + avatarUrl: googleUser.picture, + provider: 'google', + providerId: googleUser.id, + }, + }); +} const token = app.jwt.sign({ id: user.id, username: user.username }, { expiresIn: '30d' }); diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 49c29037..e2072bfc 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -3,7 +3,11 @@ import LandingPage from './pages/LandingPage'; import ProfilePage from './pages/ProfilePage'; import CardPage from './pages/CardPage'; import NotFound from './pages/NotFound'; - +import CreatePage from './pages/CreatePage'; +import LoginPage from './pages/LoginPage'; +import DashboardPage from './pages/DashboardPage'; +import EditProfilePage from './pages/EditProfilePage'; +import PublicProfilePage from './pages/PublicProfilePage'; export default function App() { return ( @@ -11,6 +15,11 @@ export default function App() { } /> } /> } /> + } /> + } /> + } /> + } /> + } /> ); } diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts index be6afdd8..b85056c1 100644 --- a/apps/web/src/lib/api.ts +++ b/apps/web/src/lib/api.ts @@ -1,16 +1,26 @@ const API_BASE_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:3000'; -export async function apiFetch(endpoint: string): Promise { +export async function apiFetch( + endpoint: string, + options: RequestInit = {} +): Promise { const response = await fetch(`${API_BASE_URL}${endpoint}`, { - headers: { 'Content-Type': 'application/json' }, + ...options, + credentials: 'include', // IMPORTANT + headers: { + 'Content-Type': 'application/json', + ...(options.headers || {}), + }, }); if (!response.ok) { const error = await response.json().catch(() => ({})); + throw new Error( - (error as Record)?.message ?? `Request failed: ${response.status}` + (error as Record)?.message ?? + `Request failed: ${response.status}` ); } return response.json() as Promise; -} +} \ No newline at end of file diff --git a/apps/web/src/pages/CreatePage.tsx b/apps/web/src/pages/CreatePage.tsx new file mode 100644 index 00000000..4b0e7f59 --- /dev/null +++ b/apps/web/src/pages/CreatePage.tsx @@ -0,0 +1,19 @@ +export default function CreatePage() { + return ( +
+

Create Your DevCard 🚀

+

This page is under development.

+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/pages/DashboardPage.css b/apps/web/src/pages/DashboardPage.css new file mode 100644 index 00000000..4d20b695 --- /dev/null +++ b/apps/web/src/pages/DashboardPage.css @@ -0,0 +1,55 @@ +.dashboard-page { + min-height: 100vh; + background: #0f172a; + display: flex; + justify-content: center; + align-items: center; + padding: 2rem; +} + +.dashboard-card { + width: 100%; + max-width: 500px; + background: rgba(255,255,255,0.06); + border: 1px solid rgba(255,255,255,0.08); + backdrop-filter: blur(20px); + border-radius: 24px; + padding: 2.5rem; + color: white; + text-align: center; +} + +.dashboard-avatar { + width: 110px; + height: 110px; + border-radius: 50%; + object-fit: cover; + margin-bottom: 1rem; +} + +.username { + color: #a5b4fc; + margin-top: 0.5rem; +} + +.email { + color: #cbd5e1; + margin-top: 0.5rem; +} + +.bio { + margin-top: 1rem; + color: #e2e8f0; + line-height: 1.6; +} + +.logout-btn { + margin-top: 2rem; + border: none; + background: #ef4444; + color: white; + padding: 0.9rem 1.4rem; + border-radius: 12px; + cursor: pointer; + font-weight: 600; +} \ No newline at end of file diff --git a/apps/web/src/pages/DashboardPage.tsx b/apps/web/src/pages/DashboardPage.tsx new file mode 100644 index 00000000..b0a40533 --- /dev/null +++ b/apps/web/src/pages/DashboardPage.tsx @@ -0,0 +1,115 @@ +import { useEffect, useState } from 'react'; +import './DashboardPage.css'; +import { useNavigate } from 'react-router-dom'; + +interface User { + id: string; + username: string; + displayName: string; + email: string; + bio?: string; + avatarUrl?: string; +} + +const BACKEND_URL = 'http://localhost:3000'; + +export default function DashboardPage() { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const navigate = useNavigate(); + + useEffect(() => { + fetch(`${BACKEND_URL}/auth/me`, { + credentials: 'include', + }) + .then(async (res) => { + if (!res.ok) { + throw new Error('Unauthorized'); + } + + const data = await res.json(); + setUser(data); + }) + .catch(() => { + setUser(null); + }) + .finally(() => { + setLoading(false); + }); + }, []); + + async function handleLogout() { + await fetch(`${BACKEND_URL}/auth/logout`, { + method: 'POST', + credentials: 'include', + }); + + window.location.href = '/login'; + } + + if (loading) { + return ( +
+

Loading dashboard...

+
+ ); + } + + if (!user) { + return ( +
+
+

Unauthorized

+

Please login first.

+
+
+ ); + } + + return ( +
+
+ {user.avatarUrl && ( + {user.displayName} + )} + +

{user.displayName}

+ +

@{user.username}

+ +

{user.email}

+ + {user.bio && ( +

{user.bio}

+ )} + + + + +
+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/pages/EditProfilePage.tsx b/apps/web/src/pages/EditProfilePage.tsx new file mode 100644 index 00000000..8e41856e --- /dev/null +++ b/apps/web/src/pages/EditProfilePage.tsx @@ -0,0 +1,332 @@ +import { useEffect, useState } from 'react'; + +const API_BASE_URL = + import.meta.env.VITE_API_URL ?? 'http://localhost:3000'; + +type UserProfile = { + displayName: string; + username: string; + email: string; + bio: string; + role: string; + company: string; + accentColor: string; + avatarUrl: string; + github: string; +linkedin: string; +twitter: string; +website: string; +}; + +export default function EditProfilePage() { + const [profile, setProfile] = useState({ + displayName: '', + username: '', + email: '', + bio: '', + role: '', + company: '', + accentColor: '#6366f1', + avatarUrl: '', + github: '', +linkedin: '', +twitter: '', +website: '', + }); + + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [message, setMessage] = useState(''); + + // Fetch logged-in user + useEffect(() => { + async function fetchProfile() { + try { + const response = await fetch(`${API_BASE_URL}/auth/me`, { + credentials: 'include', + }); + + const data = await response.json(); + + setProfile({ + displayName: data.displayName || '', + username: data.username || '', + email: data.email || '', + bio: data.bio || '', + role: data.role || '', + company: data.company || '', + accentColor: data.accentColor || '#6366f1', + avatarUrl: data.avatarUrl || '', + github: data.github || '', +linkedin: data.linkedin || '', +twitter: data.twitter || '', +website: data.website || '', + }); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + } + + fetchProfile(); + }, []); + + // Handle form input + const handleChange = ( + e: React.ChangeEvent + ) => { + setProfile({ + ...profile, + [e.target.name]: e.target.value, + }); + }; + + // Save profile + const handleSave = async () => { + try { + setSaving(true); + setMessage(''); + + const response = await fetch( + `${API_BASE_URL}/api/profiles/me`, + { + method: 'PUT', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(profile), + } + ); + + if (!response.ok) { + throw new Error('Failed to update profile'); + } + + setMessage('Profile updated successfully!'); + } catch (error) { + console.error(error); + setMessage('Failed to update profile'); + } finally { + setSaving(false); + } + }; + + if (loading) { + return ( +
+ Loading... +
+ ); + } + + return ( +
+
+

+ Edit Profile +

+ +
+ + + + + + +