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.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 (
+
+ );
+}
+
+const inputStyle: React.CSSProperties = {
+ padding: '14px',
+ borderRadius: '12px',
+ border: '1px solid #334155',
+ background: '#0f172a',
+ color: 'white',
+ fontSize: '16px',
+ outline: 'none',
+};
+
+const textareaStyle: React.CSSProperties = {
+ ...inputStyle,
+ resize: 'none',
+};
\ No newline at end of file
diff --git a/apps/web/src/pages/LandingPage.tsx b/apps/web/src/pages/LandingPage.tsx
index dd5f3324..9632f6ae 100644
--- a/apps/web/src/pages/LandingPage.tsx
+++ b/apps/web/src/pages/LandingPage.tsx
@@ -41,7 +41,7 @@ export default function LandingPage() {
Twitter, and every other profile with a single NFC tap — beautifully.
-
+
Get Started Free
+
+
🚀 DevCard Auth
+
+
Welcome to DevCard
+
+
+ Connect your developer identity and start sharing your profile instantly.
+
+
+
+
+
+
+
+
+
+ Developer networking made simple ⚡
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web/src/pages/ProfilePage.tsx b/apps/web/src/pages/ProfilePage.tsx
index 94a84f54..eea748c9 100644
--- a/apps/web/src/pages/ProfilePage.tsx
+++ b/apps/web/src/pages/ProfilePage.tsx
@@ -181,7 +181,7 @@ export default function ProfilePage() {
Want a card like this?
-
+
Create your DevCard ⚡