From 61ca5b6380f2830ae9e869ff1075ddfd39fc381b Mon Sep 17 00:00:00 2001 From: Kaustav Halder Date: Fri, 22 May 2026 12:03:53 +0530 Subject: [PATCH 1/3] Implemented profile integration and QR improvements --- apps/backend/prisma/schema.prisma | 5 +- apps/backend/src/app.ts | 12 +- apps/backend/src/plugins/redis.ts | 4 + apps/backend/src/routes/auth.ts | 52 + apps/web/src/app.css | 15 +- apps/web/src/app.html | 7 + apps/web/src/lib/api.ts | 30 + apps/web/src/routes/+layout.svelte | 10 + apps/web/src/routes/+page.svelte | 68 +- apps/web/src/routes/api/[...path]/+server.ts | 68 + apps/web/src/routes/dashboard/+page.server.ts | 38 + apps/web/src/routes/dashboard/+page.svelte | 1196 +++++++++++++++++ apps/web/src/routes/login/+page.svelte | 335 +++++ apps/web/src/routes/u/[username]/+page.svelte | 33 +- apps/web/svelte.config.js | 2 +- package.json | 1 + packages/shared/src/platforms.ts | 26 + 17 files changed, 1845 insertions(+), 57 deletions(-) create mode 100644 apps/web/src/lib/api.ts create mode 100644 apps/web/src/routes/api/[...path]/+server.ts create mode 100644 apps/web/src/routes/dashboard/+page.server.ts create mode 100644 apps/web/src/routes/dashboard/+page.svelte create mode 100644 apps/web/src/routes/login/+page.svelte diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index f2d3afe..ebf6dbf 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -2,9 +2,8 @@ generator client { provider = "prisma-client-js" } datasource db { - provider = "postgresql" - url = env("DATABASE_URL") - + provider = "sqlite" + url = "file:./dev.db" } model User { diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index 5f45001..f472d42 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -34,8 +34,18 @@ export async function buildApp() { }); // ─── Core Plugins ─── + const allowedOrigins = [ + 'http://localhost:5173', + 'http://localhost:5174', + 'http://127.0.0.1:5173', + 'http://127.0.0.1:5174' + ]; + if (process.env.PUBLIC_APP_URL) { + allowedOrigins.push(process.env.PUBLIC_APP_URL); + } + await app.register(cors, { - origin: process.env.PUBLIC_APP_URL || 'http://localhost:5173', + origin: allowedOrigins, credentials: true, }); diff --git a/apps/backend/src/plugins/redis.ts b/apps/backend/src/plugins/redis.ts index c7b6f94..976fb42 100644 --- a/apps/backend/src/plugins/redis.ts +++ b/apps/backend/src/plugins/redis.ts @@ -14,6 +14,10 @@ export const redisPlugin = fp(async (app: FastifyInstance) => { lazyConnect: true, }); + redis.on('error', (err) => { + app.log.debug(`Redis connection state: offline (${err.message})`); + }); + try { await redis.connect(); app.log.info('🔴 Redis connected'); diff --git a/apps/backend/src/routes/auth.ts b/apps/backend/src/routes/auth.ts index febc41d..29fb6e0 100644 --- a/apps/backend/src/routes/auth.ts +++ b/apps/backend/src/routes/auth.ts @@ -278,6 +278,58 @@ export async function authRoutes(app: FastifyInstance) { }; }); + // ─── Local Developer Bypass Auth ─── + + app.post('/bypass', async (request: FastifyRequest, reply: FastifyReply) => { + const { username } = request.body as { username: string }; + if (!username) { + return reply.status(400).send({ error: 'Missing username' }); + } + + const cleanUsername = username.trim().toLowerCase().replace(/[^a-zA-Z0-9_-]/g, ''); + if (!cleanUsername) { + return reply.status(400).send({ error: 'Invalid username format' }); + } + + try { + const user = await app.prisma.user.upsert({ + where: { + username: cleanUsername, + }, + update: {}, + create: { + email: `${cleanUsername}@devcard.local`, + username: cleanUsername, + displayName: username.trim(), + bio: 'Full-stack developer building outstanding interfaces.', + role: 'Software Engineer', + company: 'DevCard Team', + avatarUrl: `https://api.dicebear.com/7.x/bottts/svg?seed=${cleanUsername}`, + provider: 'dev_bypass', + providerId: `bypass_${cleanUsername}`, + }, + }); + + const token = app.jwt.sign( + { id: user.id, username: user.username }, + { expiresIn: '30d' } + ); + + reply.setCookie('token', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/', + maxAge: 30 * 24 * 60 * 60, + }); + + return { token, user }; + } catch (err) { + app.log.error('Bypass login error:', err); + return reply.status(500).send({ error: 'Authentication bypass failed' }); + } + }); + // ─── Logout ─── app.post('/logout', async (request: FastifyRequest, reply: FastifyReply) => { diff --git a/apps/web/src/app.css b/apps/web/src/app.css index c775623..120b922 100644 --- a/apps/web/src/app.css +++ b/apps/web/src/app.css @@ -19,6 +19,11 @@ --text-secondary: #475569; --text-muted: #64748b; + /* Secondary Button & Inputs */ + --btn-secondary-bg: rgba(15, 23, 42, 0.05); + --btn-secondary-border: rgba(15, 23, 42, 0.08); + --btn-secondary-hover-bg: rgba(15, 23, 42, 0.09); + /* Effects */ --border: rgba(226, 232, 240, 0.9); --border-glass: rgba(255, 255, 255, 0.35); @@ -44,6 +49,10 @@ html.dark { --text-secondary: #cbd5e1; --text-muted: #64748b; + --btn-secondary-bg: rgba(255, 255, 255, 0.08); + --btn-secondary-border: rgba(255, 255, 255, 0.14); + --btn-secondary-hover-bg: rgba(255, 255, 255, 0.14); + --border: rgba(30, 41, 59, 0.85); --border-glass: rgba(255, 255, 255, 0.12); } @@ -131,14 +140,14 @@ button { padding: 0.85rem 1.75rem; border-radius: calc(var(--radius) * 1.2); font-weight: 700; - border: 1px solid rgba(255, 255, 255, 0.14); - background: rgba(255, 255, 255, 0.08); + border: 1px solid var(--btn-secondary-border); + background: var(--btn-secondary-bg); color: var(--text-primary); cursor: pointer; } .btn-secondary:hover { - background: rgba(255, 255, 255, 0.14); + background: var(--btn-secondary-hover-bg); border-color: rgba(99, 102, 241, 0.45); } diff --git a/apps/web/src/app.html b/apps/web/src/app.html index f273cc5..ee910ff 100644 --- a/apps/web/src/app.html +++ b/apps/web/src/app.html @@ -3,6 +3,13 @@ + %sveltekit.head% diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts new file mode 100644 index 0000000..c861162 --- /dev/null +++ b/apps/web/src/lib/api.ts @@ -0,0 +1,30 @@ +export async function apiFetch(path: string, options: RequestInit = {}) { + // Route all browser fetches through the SvelteKit /api/ proxy gateway + const cleanPath = path.startsWith('/') ? path.substring(1) : path; + const url = `/api/${cleanPath}`; + + const headers = new Headers(options.headers); + if (options.body && !(options.body instanceof FormData) && !headers.has('Content-Type')) { + headers.set('Content-Type', 'application/json'); + } + + const response = await fetch(url, { + ...options, + headers, + }); + + if (!response.ok) { + let errorMsg = 'An error occurred'; + try { + const data = await response.json(); + errorMsg = data.error || errorMsg; + } catch {} + throw new Error(errorMsg); + } + + if (response.status === 204) { + return null; + } + + return response.json(); +} diff --git a/apps/web/src/routes/+layout.svelte b/apps/web/src/routes/+layout.svelte index ac6dd12..ea7edec 100644 --- a/apps/web/src/routes/+layout.svelte +++ b/apps/web/src/routes/+layout.svelte @@ -1,6 +1,16 @@ diff --git a/apps/web/src/routes/+page.svelte b/apps/web/src/routes/+page.svelte index 11f8085..cab348b 100644 --- a/apps/web/src/routes/+page.svelte +++ b/apps/web/src/routes/+page.svelte @@ -34,14 +34,17 @@ @@ -52,15 +55,10 @@ The open-source standard for developer networking. Put all your profiles—GitHub, LinkedIn, LeetCode, and more—into a single, high-impact digital card.

@@ -130,8 +128,8 @@ .theme-toggle { width: 46px; height: 46px; - background: rgba(255, 255, 255, 0.08); - border: 1px solid rgba(255, 255, 255, 0.12); + background: var(--btn-secondary-bg); + border: 1px solid var(--btn-secondary-border); border-radius: 50%; cursor: pointer; display: inline-flex; @@ -143,7 +141,7 @@ .theme-toggle:hover { transform: scale(1.05); - background: rgba(255, 255, 255, 0.14); + background: var(--btn-secondary-hover-bg); } .theme-toggle:focus-visible { @@ -203,13 +201,13 @@ padding: 0.92rem 1.75rem; border-radius: calc(var(--radius) * 1.15); font-weight: 700; - border: 1px solid rgba(255, 255, 255, 0.18); - background: rgba(255, 255, 255, 0.08); + border: 1px solid var(--btn-secondary-border); + background: var(--btn-secondary-bg); color: var(--text-primary); } .btn-secondary:hover { - background: rgba(255, 255, 255, 0.14); + background: var(--btn-secondary-hover-bg); border-color: rgba(99, 102, 241, 0.45); } @@ -230,23 +228,25 @@ } .feature-card { - padding: 2.4rem; + padding: 2rem; border-radius: var(--radius-xl); - box-shadow: var(--shadow-lg); - background: linear-gradient(180deg, rgba(15, 23, 42, 0.75), rgba(15, 23, 42, 0.5)); - border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: var(--shadow-md); + background: var(--bg-glass); + backdrop-filter: blur(18px); + -webkit-backdrop-filter: blur(18px); + border: 1px solid var(--border-glass); transition: transform 0.35s ease, border-color 0.35s ease, box-shadow 0.35s ease; + min-height: 180px; + display: flex; + flex-direction: column; } - .feature-card { - min-height: 140px; - padding: 16px; -} -@media (max-width: 640px) { - .feature-card { - margin-bottom: 12px; + @media (max-width: 640px) { + .feature-card { + margin-bottom: 12px; + padding: 1.5rem; + } } -} .feature-card:hover { transform: translateY(-8px); diff --git a/apps/web/src/routes/api/[...path]/+server.ts b/apps/web/src/routes/api/[...path]/+server.ts new file mode 100644 index 0000000..c926bb7 --- /dev/null +++ b/apps/web/src/routes/api/[...path]/+server.ts @@ -0,0 +1,68 @@ +import { error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; + +const API_BASE = process.env.BACKEND_URL || 'http://localhost:3000'; + +export const fallback: RequestHandler = async ({ params, request, cookies, url }) => { + const path = params.path; + const token = cookies.get('token'); + + // Build headers + const headers = new Headers(request.headers); + if (token) { + headers.set('Authorization', `Bearer ${token}`); + } + + // Suppress headers that could conflict during local proxying + headers.delete('host'); + headers.delete('connection'); + + // Fastify route resolution: strip any duplicate leading api/ prefixes + let cleanPath = path; + if (cleanPath.startsWith('api/')) { + cleanPath = cleanPath.substring(4); + } + const targetPath = cleanPath.startsWith('auth/') ? cleanPath : `api/${cleanPath}`; + const targetUrl = new URL(`${API_BASE}/${targetPath}${url.search}`); + + try { + let requestBody: any = undefined; + if (request.method !== 'GET' && request.method !== 'HEAD') { + const blob = await request.blob(); + if (blob.size > 0) { + requestBody = blob; + } else { + headers.delete('content-type'); + } + } + + const res = await fetch(targetUrl.toString(), { + method: request.method, + headers, + body: requestBody, + }); + + const responseHeaders = new Headers(); + const contentType = res.headers.get('Content-Type'); + if (contentType) { + responseHeaders.set('Content-Type', contentType); + } + + // Forward cookies set by the backend (like token set on login) + const setCookie = res.headers.get('Set-Cookie'); + if (setCookie) { + responseHeaders.set('Set-Cookie', setCookie); + } + + return new Response(res.body, { + status: res.status, + headers: responseHeaders, + }); + } catch (err: any) { + console.error('DevCard Proxy Error:', err); + return new Response(JSON.stringify({ error: 'Backend API is currently offline.' }), { + status: 502, + headers: { 'Content-Type': 'application/json' }, + }); + } +}; diff --git a/apps/web/src/routes/dashboard/+page.server.ts b/apps/web/src/routes/dashboard/+page.server.ts new file mode 100644 index 0000000..40e5d90 --- /dev/null +++ b/apps/web/src/routes/dashboard/+page.server.ts @@ -0,0 +1,38 @@ +import { redirect } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; + +const API_BASE = process.env.BACKEND_URL || 'http://localhost:3000'; + +export const load: PageServerLoad = async ({ cookies, fetch }) => { + const token = cookies.get('token'); + if (!token) { + throw redirect(302, '/login'); + } + + try { + const res = await fetch(`${API_BASE}/api/profiles/me`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!res.ok) { + // Token is invalid or expired, clear and redirect + cookies.delete('token', { path: '/' }); + throw redirect(302, '/login'); + } + + const user = await res.json(); + return { user }; + } catch (err: any) { + // If the error is a SvelteKit redirect instruction, forward it + if (err.status && err.status >= 300 && err.status < 400) { + throw err; + } + + return { + user: null, + error: 'Backend API is currently unreachable. Please make sure the backend server is running.', + }; + } +}; diff --git a/apps/web/src/routes/dashboard/+page.svelte b/apps/web/src/routes/dashboard/+page.svelte new file mode 100644 index 0000000..87a6db0 --- /dev/null +++ b/apps/web/src/routes/dashboard/+page.svelte @@ -0,0 +1,1196 @@ + + +
+ +
+ {#each toasts as toast (toast.id)} +
+ {toast.type === 'success' ? '⚡' : '⚠️'} + {toast.message} +
+ {/each} +
+ +
+ + + + +
+ + +
+
+ 🎨 +

Card Customizer

+
+

Fine-tune your brand colors and professional details.

+ +
+
+
+ + +
+
+ +
+ + +
+
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ + +
+
+ + + + + +
+
+ 📱 +

Dynamic Card Live View

+
+

Real-time preview of your public identity page.

+ + +
+
+
+ +
+ {#if user.avatarUrl} + {user.displayName} + {:else} +
+ {user.displayName.charAt(0).toUpperCase()} +
+ {/if} +

{user.displayName}

+ {#if user.role} + {user.role}{user.company ? ` @ ${user.company}` : ''} + {/if} + {#if user.bio} +

{user.bio}

+ {/if} +
+ + +
+
+ + + +
+ +
+
+ + +{#if isModalOpen} + +{/if} + + diff --git a/apps/web/src/routes/login/+page.svelte b/apps/web/src/routes/login/+page.svelte new file mode 100644 index 0000000..6b63775 --- /dev/null +++ b/apps/web/src/routes/login/+page.svelte @@ -0,0 +1,335 @@ + + + + Login | DevCard Studio ⚡ + + +
+ +
+ +
+ + diff --git a/apps/web/src/routes/u/[username]/+page.svelte b/apps/web/src/routes/u/[username]/+page.svelte index bb23cca..000c4c8 100644 --- a/apps/web/src/routes/u/[username]/+page.svelte +++ b/apps/web/src/routes/u/[username]/+page.svelte @@ -154,7 +154,7 @@ right: 0; bottom: 0; background: radial-gradient(circle at 50% 0%, var(--accent), transparent 50%), - #020617; + var(--bg-page); opacity: 0.18; z-index: -1; } @@ -180,11 +180,13 @@ max-width: 540px; border-radius: var(--radius-xl); padding: 2.5rem 2rem; - box-shadow: 0 26px 60px -20px rgba(0, 0, 0, 0.55); + box-shadow: var(--shadow-lg); position: relative; overflow: hidden; - border: 1px solid rgba(255, 255, 255, 0.08); - background: rgba(15, 23, 42, 0.96); + border: 1px solid var(--border-glass); + background: var(--bg-glass); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); } .profile-header { @@ -233,8 +235,8 @@ align-items: center; justify-content: center; padding: 0.45rem 1rem; - background: rgba(255, 255, 255, 0.08); - border: 1px solid rgba(255, 255, 255, 0.12); + background: var(--btn-secondary-bg); + border: 1px solid var(--btn-secondary-border); border-radius: 999px; font-size: 0.9rem; font-weight: 700; @@ -261,9 +263,9 @@ align-items: center; padding: 1rem; border-radius: calc(var(--radius) * 1.1); - border: 1px solid rgba(255, 255, 255, 0.1); - background: rgba(255, 255, 255, 0.06); - box-shadow: 0 12px 30px -18px rgba(0, 0, 0, 0.35); + border: 1px solid var(--border); + background: var(--bg-secondary); + box-shadow: var(--shadow-sm); transition: transform 0.25s ease, background 0.25s ease, border-color 0.25s ease; animation: slideIn 0.5s ease-out forwards; animation-delay: var(--delay); @@ -272,9 +274,10 @@ .link-tile:hover, .link-tile:focus-visible { - background: rgba(255, 255, 255, 0.13); + background: var(--bg-primary); transform: translateY(-2px); - border-color: rgba(99, 102, 241, 0.35); + border-color: var(--primary); + box-shadow: var(--shadow-md); } .link-tile:focus-visible { @@ -332,7 +335,7 @@ .card-footer { margin-top: 2.5rem; padding-top: 1.75rem; - border-top: 1px solid rgba(255,255,255,0.08); + border-top: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; @@ -373,9 +376,9 @@ } .copy-link-button { - border: 1px solid var(--border-glass); + border: 1px solid var(--btn-secondary-border); border-radius: var(--radius); - background: rgba(255, 255, 255, 0.08); + background: var(--btn-secondary-bg); color: var(--text-primary); cursor: pointer; font: inherit; @@ -385,7 +388,7 @@ } .copy-link-button:hover { - background: rgba(255, 255, 255, 0.15); + background: var(--btn-secondary-hover-bg); transform: translateY(-1px); } diff --git a/apps/web/svelte.config.js b/apps/web/svelte.config.js index 55c3bd2..38b2fca 100644 --- a/apps/web/svelte.config.js +++ b/apps/web/svelte.config.js @@ -14,7 +14,7 @@ const config = { 'script-src': ['self', 'unsafe-inline'], 'style-src': ['self', 'unsafe-inline', 'https://fonts.googleapis.com'], 'img-src': ['self', 'data:', 'https:'], - 'connect-src': ['self'], + 'connect-src': ['self', 'http://localhost:3000', 'ws://localhost:5173'], 'font-src': ['self', 'data:', 'https:', 'https://fonts.gstatic.com'], 'object-src': ['none'], 'base-uri': ['self'], diff --git a/package.json b/package.json index bbe44f7..77d8e64 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "description": "One Tap. Every Profile. Every Platform. Open Source Developer Profile Exchange Platform.", "license": "Apache-2.0", "scripts": { + "dev": "concurrently \"pnpm dev:backend\" \"pnpm dev:web\"", "dev:backend": "pnpm --filter @devcard/backend dev", "dev:mobile": "pnpm --filter @devcard/mobile start", "dev:web": "pnpm --filter @devcard/web dev", diff --git a/packages/shared/src/platforms.ts b/packages/shared/src/platforms.ts index a218957..c0de90f 100644 --- a/packages/shared/src/platforms.ts +++ b/packages/shared/src/platforms.ts @@ -162,6 +162,32 @@ export const PLATFORMS: Record = { usernamePlaceholder: 'e.g. coder', usesFullUrl: false, }, + geeksforgeeks: { + id: 'geeksforgeeks', + name: 'GeeksforGeeks', + icon: 'code', + color: '#2F8D46', + urlPattern: 'https://www.geeksforgeeks.org/user/{username}', + deepLinkPattern: null, + webViewUrlPattern: null, + followStrategy: 'link', + oauthScopes: [], + usernamePlaceholder: 'e.g. gfg_user', + usesFullUrl: false, + }, + codeforces: { + id: 'codeforces', + name: 'Codeforces', + icon: 'award', + color: '#1A8FFF', + urlPattern: 'https://codeforces.com/profile/{username}', + deepLinkPattern: null, + webViewUrlPattern: null, + followStrategy: 'link', + oauthScopes: [], + usernamePlaceholder: 'e.g. tourist', + usesFullUrl: false, + }, hackerrank: { id: 'hackerrank', name: 'HackerRank', From 5e2ad94e458396a3ece676de65f5ee09f7d56b8a Mon Sep 17 00:00:00 2001 From: Kaustav Halder Date: Sat, 23 May 2026 22:33:07 +0530 Subject: [PATCH 2/3] Resolved merge conflicts --- apps/web/src/app.html | 5 ++++ apps/web/src/routes/+page.svelte | 45 ++++++++++++++++++-------------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/apps/web/src/app.html b/apps/web/src/app.html index ee910ff..2165b44 100644 --- a/apps/web/src/app.html +++ b/apps/web/src/app.html @@ -3,6 +3,11 @@ + + + + +