Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ generator client {
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")

}

model User {
Expand Down
6 changes: 6 additions & 0 deletions apps/backend/src/__tests__/event.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ describe('Events API', () => {
prismaMock.event.findUnique.mockResolvedValue({
...MOCK_EVENT,
attendees: attendeeRows,
_count: { attendees: attendeeRows.length },
});

const res = await app.inject({
Expand Down Expand Up @@ -523,6 +524,7 @@ describe('Events API', () => {
prismaMock.event.findUnique.mockResolvedValue({
...MOCK_EVENT,
attendees: [makeAttendeeRow(MOCK_OTHER_USER_PROFILE)],
_count: { attendees: 1 },
});

const res = await app.inject({
Expand All @@ -545,6 +547,7 @@ describe('Events API', () => {
prismaMock.event.findUnique.mockResolvedValue({
...MOCK_EVENT,
attendees: [],
_count: { attendees: 0 },
});

const res = await app.inject({
Expand All @@ -561,6 +564,7 @@ describe('Events API', () => {
prismaMock.event.findUnique.mockResolvedValue({
...MOCK_EVENT,
attendees: [],
_count: { attendees: 0 },
});

const res = await app.inject({
Expand All @@ -577,6 +581,7 @@ describe('Events API', () => {
prismaMock.event.findUnique.mockResolvedValue({
...MOCK_EVENT,
attendees: [],
_count: { attendees: 0 },
});

const res = await app.inject({
Expand All @@ -594,6 +599,7 @@ describe('Events API', () => {
prismaMock.event.findUnique.mockResolvedValue({
...MOCK_EVENT,
attendees: [makeAttendeeRow(MOCK_USER_PROFILE)],
_count: { attendees: 1 },
});

const res = await app.inject({
Expand Down
15 changes: 14 additions & 1 deletion apps/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,21 @@ export async function buildApp() {
});

// ─── Core Plugins ───
const allowedOrigins: string[] = [];
if (process.env.NODE_ENV !== 'production') {
allowedOrigins.push(
'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,
});

Expand Down
4 changes: 4 additions & 0 deletions apps/backend/src/plugins/redis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
51 changes: 51 additions & 0 deletions apps/backend/src/routes/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import { randomBytes } from 'crypto';
import { encrypt } from '../utils/encryption.js';
import { bypassAuthSchema } from '../utils/validators.js';

const GITHUB_AUTH_URL = 'https://github.com/login/oauth/authorize';
const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token';
Expand Down Expand Up @@ -345,6 +346,56 @@ app.get('/github/callback', async (request: FastifyRequest<{ Querystring: OAuthC
};
});

// ─── Local Developer Bypass Auth ───

app.post('/bypass', async (request: FastifyRequest, reply: FastifyReply) => {
Comment thread
Kaustav2706 marked this conversation as resolved.
const parsed = bypassAuthSchema.safeParse(request.body);
if (!parsed.success) {
return reply.status(400).send({ error: 'Validation failed', details: parsed.error.flatten() });
}

const { username } = parsed.data;
const cleanUsername = username.toLowerCase();

try {
const user = await app.prisma.user.upsert({
where: {
username: cleanUsername,
},
update: {},
create: {
email: `${cleanUsername}@devcard.local`,
username: cleanUsername,
displayName: username,
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 { user };
} catch (err) {
app.log.error({ err }, 'Bypass login error');
return reply.status(500).send({ error: 'Authentication bypass failed' });
}
});

// ─── Logout ───

app.post('/logout', async (request: FastifyRequest, reply: FastifyReply) => {
Expand Down
9 changes: 9 additions & 0 deletions apps/backend/src/utils/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,12 @@ export const updateCardSchema = z.object({
title: z.string().min(1).max(100).optional(),
linkIds: z.array(z.string().uuid()).optional(),
});

export const bypassAuthSchema = z.object({
username: z
.string()
.min(1, 'Username is required')
.max(50, 'Username must not exceed 50 characters')
.regex(/^[a-zA-Z0-9_-]+$/, 'Username can only contain letters, numbers, hyphens, and underscores'),
});

15 changes: 12 additions & 3 deletions apps/web/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
}

Expand Down
38 changes: 24 additions & 14 deletions apps/web/src/app.html
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0f0f1a" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="DevCard" />
<meta name="twitter:card" content="summary_large_image" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0f0f1a" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="DevCard" />
<meta name="twitter:card" content="summary_large_image" />
<script>
try {
const saved = localStorage.getItem('devcard-theme');
const theme = saved || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.classList.toggle('dark', theme === 'dark');
} catch (e) { }
</script>
%sveltekit.head%
</head>

<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>

</html>
30 changes: 30 additions & 0 deletions apps/web/src/lib/api.ts
Original file line number Diff line number Diff line change
@@ -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();
}
10 changes: 10 additions & 0 deletions apps/web/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
<script>
import '../app.css';
import { onMount } from 'svelte';
let { children } = $props();

onMount(() => {
try {
const saved = localStorage.getItem('devcard-theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme = saved || (prefersDark ? 'dark' : 'light');
document.documentElement.classList.toggle('dark', theme === 'dark');
} catch (e) {}
});
</script>

<svelte:head>
Expand Down
Loading