Skip to content
Closed
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
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
38 changes: 24 additions & 14 deletions apps/backend/src/app.ts

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will break prod.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Harxhit, thanks for catching this! I've pushed a fix in commit 58a733e:

Reverted the NODE_ENV !== 'test' guards around prisma/redis plugin registration β€” these should always be registered; tests should handle mocking separately
Restored the original health check endpoint with timestamp and service fields
Fixed indentation inconsistencies and removed a leftover debug comment
Please re-review when you get a chance!

Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,21 @@ export async function buildApp():Promise<FastifyInstance> {
});

// ─── 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 Expand Up @@ -84,12 +97,9 @@ export async function buildApp():Promise<FastifyInstance> {
});

// ─── Database & Cache Plugins ───
if (process.env.NODE_ENV !== 'test') {
await app.register(prismaPlugin); //change
}
if (process.env.NODE_ENV !== 'test') {
await app.register(prismaPlugin);
await app.register(redisPlugin);
}

// ─── Auth Decorator ───
app.decorate('authenticate', async function (request: any, reply: any) {
try {
Expand All @@ -107,15 +117,15 @@ export async function buildApp():Promise<FastifyInstance> {
await app.register(followRoutes, { prefix: '/api/follow' });
await app.register(connectRoutes, { prefix: '/api/connect' });
await app.register(analyticsRoutes, { prefix: '/api/analytics' });
await app.register(nfcRoutes, { prefix: '/api/nfc' });
await app.register(eventRoutes, { prefix: '/api/events' });
await app.register(nfcRoutes, { prefix: '/api/nfc' });
await app.register(eventRoutes, { prefix: '/api/events' });

// ─── Health Check ───
type HealthResponse = {
status: 'ok';
};
app.get('/health', async () => ({
status: 'ok',
timestamp: new Date().toISOString(),
service: 'devcard-api',
}));

app.get('/health', async (): Promise<HealthResponse> => {
return { status: 'ok' };
});
return app;
}
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 @@ -54,3 +54,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(99, 102, 241, 0.25);
Expand All @@ -45,6 +50,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);
--shadow-nav: 0 4px 24px -6px rgba(0, 0, 0, 0.45), 0 1px 4px 0 rgba(0, 0, 0, 0.25);
Expand Down Expand Up @@ -138,14 +147,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