Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,5 @@ teamd.config.*.map
teamd.config.d.mts
*.gen.ts
.tanstack/
src/mobile/ios/Pods/hermes-engine/destroot/Library/Frameworks/macosx/hermes.framework/Versions/Current
src/mobile/ios/Pods/hermes-engine/destroot/Library/Frameworks/universal/hermes.xcframework/ios-arm64_x86_64-maccatalyst/hermes.framework/Versions/Current
12 changes: 10 additions & 2 deletions src/backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ import { AuthService } from './auth.service';
import { JwtAuthGuard } from './jwt-auth.guard';
import { OnboardingGuard } from './onboarding.guard';

interface RequestWithUser extends Request {
user: { sub: number; email: string };
}

interface RequestWithOnboarding extends Request {
onboardingEmail: string;
}

@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
Expand Down Expand Up @@ -56,14 +64,14 @@ export class AuthController {
password: string;
confirmPassword?: string;
},
@Req() req: any,
@Req() req: RequestWithOnboarding,
) {
return this.authService.registerUser(req.onboardingEmail, body);
}

@Get('me')
@UseGuards(JwtAuthGuard)
me(@Req() req: any) {
me(@Req() req: RequestWithUser) {
return this.authService.getUserInfo(req.user.sub);
}

Expand Down
5 changes: 2 additions & 3 deletions src/backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import {
} from '@nestjs/common';
import { createHash, randomInt } from 'crypto';
import bcrypt from 'bcryptjs';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const nodemailer = require('nodemailer');
type Transporter = { sendMail: (options: any) => Promise<any> };
import * as nodemailer from 'nodemailer';
type Transporter = ReturnType<typeof nodemailer.createTransport>;
import { sign, type SignOptions } from 'jsonwebtoken';
import { and, desc, eq, gt, isNull } from 'drizzle-orm';
import { DatabaseService } from '../database/database.service';
Expand Down
9 changes: 7 additions & 2 deletions src/backend/src/auth/jwt-auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ import {
} from '@nestjs/common';
import { verify } from 'jsonwebtoken';

interface RequestWithHeaders {
headers?: { authorization?: string };
user?: { sub: number; email: string };
}

@Injectable()
export class JwtAuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
// M8: validateToken(token) -> verify JWT before protected routes.
const request = context.switchToHttp().getRequest();
const request = context.switchToHttp().getRequest<RequestWithHeaders>();
const header = request.headers?.authorization ?? '';
const token = typeof header === 'string' && header.startsWith('Bearer ')
? header.slice(7).trim()
Expand All @@ -37,7 +42,7 @@ export class JwtAuthGuard implements CanActivate {

request.user = { sub: Number(sub), email };
return true;
} catch (error) {
} catch {
throw new UnauthorizedException('Invalid or expired token.');
}
}
Expand Down
10 changes: 7 additions & 3 deletions src/backend/src/auth/onboarding.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import {
UnauthorizedException,
} from '@nestjs/common';
import { verify } from 'jsonwebtoken';
import type { OnboardingJwtPayload } from './auth.types';

interface RequestWithHeaders {
headers?: { authorization?: string };
onboardingEmail?: string;
}

@Injectable()
export class OnboardingGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const request = context.switchToHttp().getRequest<RequestWithHeaders>();
const header = request.headers?.authorization ?? '';
const token = typeof header === 'string' && header.startsWith('Bearer ')
? header.slice(7).trim()
Expand Down Expand Up @@ -40,7 +44,7 @@ export class OnboardingGuard implements CanActivate {

request.onboardingEmail = email;
return true;
} catch (error) {
} catch {
throw new UnauthorizedException('Invalid or expired onboarding token.');
}
}
Expand Down
1 change: 1 addition & 0 deletions src/backend/src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export const tickets = pgTable('tickets', {
checkedIn: boolean('checked_in').default(false),
busSeat: varchar('bus_seat', { length: 50 }), // e.g. "Bus 1 - Seat 5" (display)
tableSeat: varchar('table_seat', { length: 50 }), // e.g. "Table 5, Seat 4" (display)
qrCodeData: varchar('qr_code_data', { length: 255 }), // QR code data for ticket verification
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
Expand Down
154 changes: 111 additions & 43 deletions src/backend/src/db/seed-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import {
tickets,
tableSeats,
busSeats,
type Ticket,
type TableSeat,
type BusSeat,
} from './schema';
import { eq } from 'drizzle-orm';

Expand Down Expand Up @@ -137,52 +140,117 @@ export async function runSeedDb(db: SeedDb): Promise<boolean> {
}
if (busSeatRows.length > 0) await db.insert(busSeats).values(busSeatRows);

const [ticket1, ticket2, ticket3] = await db
.insert(tickets)
.values([
{ userId: user1.id, eventId: event1.id },
{ userId: user2.id, eventId: event1.id },
{ userId: user3.id, eventId: event2.id },
])
.returning();
// Check if tickets already exist
const existingTickets = await db.select().from(tickets).limit(1);
let ticket1: Ticket | undefined;
let ticket2: Ticket | undefined;
let ticket3: Ticket | undefined;

if (existingTickets.length > 0) {
// Update existing tickets with QR codes
const allTickets: Ticket[] = await db.select().from(tickets);
for (const ticket of allTickets) {
const qrData = `TICKET:${ticket.userId}:${ticket.eventId}:${ticket.id}:${Date.now()}`;
await db
.update(tickets)
.set({ qrCodeData: qrData, updatedAt: new Date() })
.where(eq(tickets.id, ticket.id));
}
// Get first 3 for seat assignment
ticket1 = allTickets[0];
ticket2 = allTickets[1];
ticket3 = allTickets[2];
} else {
// Create new tickets with QR codes
const timestamp = Date.now();
const [t1, t2, t3] = await db
.insert(tickets)
.values([
{
userId: user1.id,
eventId: event1.id,
qrCodeData: `TICKET:${user1.id}:${event1.id}:temp1:${timestamp}`
},
{
userId: user2.id,
eventId: event1.id,
qrCodeData: `TICKET:${user2.id}:${event1.id}:temp2:${timestamp}`
},
{
userId: user3.id,
eventId: event2.id,
qrCodeData: `TICKET:${user3.id}:${event2.id}:temp3:${timestamp}`
},
])
.returning();

// Update with actual ticket IDs
if (t1) {
await db
.update(tickets)
.set({ qrCodeData: `TICKET:${user1.id}:${event1.id}:${t1.id}:${timestamp}`, updatedAt: new Date() })
.where(eq(tickets.id, t1.id));
}
if (t2) {
await db
.update(tickets)
.set({ qrCodeData: `TICKET:${user2.id}:${event1.id}:${t2.id}:${timestamp}`, updatedAt: new Date() })
.where(eq(tickets.id, t2.id));
}
if (t3) {
await db
.update(tickets)
.set({ qrCodeData: `TICKET:${user3.id}:${event2.id}:${t3.id}:${timestamp}`, updatedAt: new Date() })
.where(eq(tickets.id, t3.id));
}

ticket1 = t1;
ticket2 = t2;
ticket3 = t3;
}

if (!ticket1 || !ticket2 || !ticket3) throw new Error('Ticket insert failed');

const tableSeatToAssign = await db
.select()
.from(tableSeats)
.where(eq(tableSeats.eventId, event1.id))
.limit(1);
if (tableSeatToAssign[0]) {
await db
.update(tableSeats)
.set({ ticketId: ticket1.id, updatedAt: new Date() })
.where(eq(tableSeats.id, tableSeatToAssign[0].id));
await db
.update(tickets)
.set({
tableSeat: `Table ${tableSeatToAssign[0].tableNumber}, Seat ${tableSeatToAssign[0].seatNumber}`,
updatedAt: new Date(),
})
.where(eq(tickets.id, ticket1.id));
}
if (ticket1 && ticket2) {
const tableSeatResults: TableSeat[] = await db
.select()
.from(tableSeats)
.where(eq(tableSeats.eventId, event1.id))
.limit(1);
const tableSeat: TableSeat | undefined = tableSeatResults[0];
if (tableSeat) {
await db
.update(tableSeats)
.set({ ticketId: ticket1.id, updatedAt: new Date() })
.where(eq(tableSeats.id, tableSeat.id));
await db
.update(tickets)
.set({
tableSeat: `Table ${tableSeat.tableNumber}, Seat ${tableSeat.seatNumber}`,
updatedAt: new Date(),
})
.where(eq(tickets.id, ticket1.id));
}

const firstBusSeat = await db
.select()
.from(busSeats)
.where(eq(busSeats.eventId, event1.id))
.limit(1);
if (firstBusSeat[0]) {
await db
.update(busSeats)
.set({ ticketId: ticket2.id, updatedAt: new Date() })
.where(eq(busSeats.id, firstBusSeat[0].id));
await db
.update(tickets)
.set({
busSeat: `Bus ${firstBusSeat[0].busNumber} - Seat ${firstBusSeat[0].seatNumber}`,
updatedAt: new Date(),
})
.where(eq(tickets.id, ticket2.id));
const busSeatResults: BusSeat[] = await db
.select()
.from(busSeats)
.where(eq(busSeats.eventId, event1.id))
.limit(1);
const busSeat: BusSeat | undefined = busSeatResults[0];
if (busSeat) {
await db
.update(busSeats)
.set({ ticketId: ticket2.id, updatedAt: new Date() })
.where(eq(busSeats.id, busSeat.id));
await db
.update(tickets)
.set({
busSeat: `Bus ${busSeat.busNumber} - Seat ${busSeat.seatNumber}`,
updatedAt: new Date(),
})
.where(eq(tickets.id, ticket2.id));
}
}

return true;
Expand Down
14 changes: 9 additions & 5 deletions src/backend/src/events/events.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import { EventsService } from './events.service';
import type { NewEvent } from '../db/schema';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';

interface RequestWithUser extends Request {
user: { sub: number; email: string };
}

@Controller('events')
@UseGuards(JwtAuthGuard)
export class EventsController {
Expand Down Expand Up @@ -49,7 +53,7 @@ export class EventsController {
@Post(':id/signup')
signup(
@Param('id') id: string,
@Req() req: any,
@Req() req: RequestWithUser,
@Body('selectedTable') selectedTable?: number,
) {
return this.eventsService.signup(+id, req.user.sub, selectedTable);
Expand All @@ -58,7 +62,7 @@ export class EventsController {
@Post(':id/checkout-session')
createCheckoutSession(
@Param('id') id: string,
@Req() req: any,
@Req() req: RequestWithUser,
@Body()
body: {
successUrl?: string;
Expand Down Expand Up @@ -109,13 +113,13 @@ export class EventsController {
}

@Post(':id/cancel')
cancelSignup(@Param('id') id: string, @Req() req: any) {
cancelSignup(@Param('id') id: string, @Req() req: RequestWithUser) {
return this.eventsService.cancelSignup(+id, req.user.sub);
}

@Get('user/:userId/tickets')
getUserTickets(@Param('userId') userId: string, @Req() req: any) {
if (req.user?.sub !== +userId) {
getUserTickets(@Param('userId') userId: string, @Req() req: RequestWithUser) {
if (req.user.sub !== +userId) {
throw new ForbiddenException('Access denied.');
}
return this.eventsService.getTicketsForUser(+userId);
Expand Down
Loading