diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index f2d3afe..5ba4c6f 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -4,7 +4,6 @@ generator client { datasource db { provider = "postgresql" url = env("DATABASE_URL") - } model User { diff --git a/apps/backend/src/__tests__/event.test.ts b/apps/backend/src/__tests__/event.test.ts index 44806af..1e71cb6 100644 --- a/apps/backend/src/__tests__/event.test.ts +++ b/apps/backend/src/__tests__/event.test.ts @@ -495,6 +495,7 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, attendees: attendeeRows, + _count: { attendees: attendeeRows.length }, }); const res = await app.inject({ @@ -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({ @@ -545,6 +547,7 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, attendees: [], + _count: { attendees: 0 }, }); const res = await app.inject({ @@ -561,6 +564,7 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, attendees: [], + _count: { attendees: 0 }, }); const res = await app.inject({ @@ -577,6 +581,7 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, attendees: [], + _count: { attendees: 0 }, }); const res = await app.inject({ @@ -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({ diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index 94767c5..d9c2391 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -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, }); 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 fc90f2e..810ddb5 100644 --- a/apps/backend/src/routes/auth.ts +++ b/apps/backend/src/routes/auth.ts @@ -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'; @@ -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) => { + 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) => { diff --git a/apps/backend/src/utils/validators.ts b/apps/backend/src/utils/validators.ts index 80d2caa..de107cc 100644 --- a/apps/backend/src/utils/validators.ts +++ b/apps/backend/src/utils/validators.ts @@ -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'), +}); + diff --git a/apps/web/src/app.css b/apps/web/src/app.css index c65380f..c4759b6 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 666257e..0f913c7 100644 --- a/apps/web/src/app.html +++ b/apps/web/src/app.html @@ -1,16 +1,26 @@ -
- - - - - - - - %sveltekit.head% - - -