first commit

This commit is contained in:
DerJesen
2025-11-29 12:26:58 +01:00
parent 2fae31f20f
commit fe5bbc1410
142 changed files with 19585 additions and 1 deletions

View File

@@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

12
src/app.controller.ts Normal file
View File

@@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

28
src/app.module.ts Normal file
View File

@@ -0,0 +1,28 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaModule } from './prisma/prisma.module';
import { EventsModule } from './events/events.module';
import { TicketsModule } from './tickets/tickets.module';
import { AuthModule } from './auth/auth.module';
import { MailModule } from './mail/mail.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'client', 'dist')
}),
PrismaModule,
EventsModule,
TicketsModule,
AuthModule,
MailModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

8
src/app.service.ts Normal file
View File

@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -0,0 +1,19 @@
import { Controller, Post, HttpCode, HttpStatus, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller()
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
@HttpCode(HttpStatus.OK)
async login(@Body() body: { password: string }) {
return this.authService.login(body.password);
}
@Post('logout')
@HttpCode(HttpStatus.OK)
async logout() {
return { success: true };
}
}

25
src/auth/auth.module.ts Normal file
View File

@@ -0,0 +1,25 @@
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [
ConfigModule,
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET') || 'secret',
signOptions: { expiresIn: '1d' },
}),
inject: [ConfigService],
}),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
})
export class AuthModule {}

22
src/auth/auth.service.ts Normal file
View File

@@ -0,0 +1,22 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AuthService {
constructor(
private jwtService: JwtService,
private configService: ConfigService,
) {}
async login(password: string) {
const envPassword = this.configService.get<string>('PASSWORD');
if (password !== envPassword) {
throw new UnauthorizedException('Incorrect password');
}
const payload = { username: 'admin', sub: 'admin' };
return {
accessToken: this.jwtService.sign(payload),
};
}
}

19
src/auth/jwt.strategy.ts Normal file
View File

@@ -0,0 +1,19 @@
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET') || 'secret',
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}

View File

@@ -0,0 +1,5 @@
export class CreateEventDto {
name: string;
date: string;
location: string;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateEventDto } from './create-event.dto';
export class UpdateEventDto extends PartialType(CreateEventDto) {}

View File

@@ -0,0 +1 @@
export class Event {}

View File

@@ -0,0 +1,18 @@
import { Controller, Get, Post, Body } from '@nestjs/common';
import { EventsService } from './events.service';
import { CreateEventDto } from './dto/create-event.dto';
@Controller('events')
export class EventsController {
constructor(private readonly eventsService: EventsService) {}
@Post()
create(@Body() createEventDto: CreateEventDto) {
return this.eventsService.create(createEventDto);
}
@Get()
findAll() {
return this.eventsService.findAll();
}
}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { EventsService } from './events.service';
import { EventsController } from './events.controller';
import { PrismaModule } from '../prisma/prisma.module';
@Module({
imports: [PrismaModule],
controllers: [EventsController],
providers: [EventsService],
})
export class EventsModule {}

View File

@@ -0,0 +1,16 @@
import { Injectable } from '@nestjs/common';
import { CreateEventDto } from './dto/create-event.dto';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class EventsService {
constructor(private prisma: PrismaService) {}
create(createEventDto: CreateEventDto) {
return this.prisma.event.create({ data: createEventDto });
}
findAll() {
return this.prisma.event.findMany();
}
}

10
src/mail/mail.module.ts Normal file
View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { MailService } from './mail.service';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule],
providers: [MailService],
exports: [MailService],
})
export class MailModule {}

89
src/mail/mail.service.ts Normal file
View File

@@ -0,0 +1,89 @@
import { Injectable } from '@nestjs/common';
import * as nodemailer from 'nodemailer';
import { ConfigService } from '@nestjs/config';
import * as path from 'path';
@Injectable()
export class MailService {
private transporter: nodemailer.Transporter;
constructor(private configService: ConfigService) {
this.transporter = nodemailer.createTransport({
host: this.configService.get<string>('SMTP_HOST'),
port: this.configService.get<number>('SMTP_PORT'),
secure: this.configService.get<string>('SMTP_SECURE') === 'true', // true for 465, false for other ports
auth: {
user: this.configService.get<string>('SMTP_USER'),
pass: this.configService.get<string>('SMTP_PASS'),
},
});
}
async sendTicket(
email: string,
name: string,
ticketId: string,
eventName: string,
qrCodeBuffer: Buffer, // New parameter for QR code image buffer
) {
const logoPath = path.join(process.cwd(), 'dist/assets/atiw-out-logo.svg');
const firstName = name.split(' ')[0];
await this.transporter.sendMail({
from: this.configService.get<string>('SMTP_FROM') || '"Event Team" <noreply@example.com>',
to: email,
subject: `Dein Ticket fürs ATIW OUT!`,
html: `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background-color: #f4f4f5; margin: 0; padding: 0; }
.container { max-width: 600px; margin: 32px auto; background-color: #ffffff; border-radius: 8px; overflow: hidden; border: 1px solid rgba(0, 0, 0, 0.2); padding: 32px; }
.header { background-color: #ffffff; text-align: center; }
.logo { height: 150px;}
.content {text-align: center; color: #333333; }
.greeting { font-size: 32px; color: #18181b; }
.text { font-size: 16px; line-height: 1.5; color: #52525b;}
.qr-container { background-color: #f4f4f5; padding: 24px; border-radius: 12px; display: inline-block; margin: 32px 0px; }
.footer { background-color: #fafafa; padding: 24px; text-align: center; font-size: 14px; color: #a1a1aa; border-top: 1px solid #e4e4e7; }
.highlight { color: #2563eb; font-weight: 600; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<img src="cid:logo" alt="Logo" class="logo" />
</div>
<div class="content">
<h2 class="greeting">Moin ${firstName},</h2>
<p class="text">Hier ist dein Ticket für das <strong>ATIW OUT</strong>!</p>
<p class="text">Zeig diesen QR Code einfach am <strong>09.12.</strong> am Eingang vor.</p>
<div class="qr-container">
<img src="cid:qrcode" width="200" height="200" alt="Dein Ticket QR Code" style="display: block;"/>
</div>
<p class="text">Wir wünschen dir viel Spaß!</p>
<p class="text">~FI231 & FS231</p>
<p class="text" style="margin-top: 32px;">Bei Fragen oder Problemen komm bitte<br>zum Klassenraum <strong>E.07</strong> und frag nach <strong>Jason</strong></p>
</div>
</div>
</body>
</html>
`,
attachments: [
{
filename: 'qrcode.png',
content: qrCodeBuffer,
cid: 'qrcode', // Content ID to link with img src
},
{
filename: 'logo.svg',
path: logoPath,
cid: 'logo',
},
],
});
}
}

12
src/main.ts Normal file
View File

@@ -0,0 +1,12 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api');
app.enableCors();
app.useGlobalPipes(new ValidationPipe());
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

View File

@@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}

View File

@@ -0,0 +1,13 @@
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}

View File

@@ -0,0 +1,15 @@
import { IsString, IsEmail, IsIn } from 'class-validator';
export class CreateTicketDto {
@IsString()
eventId: string;
@IsString()
attendeeName: string;
@IsEmail()
attendeeEmail: string;
@IsIn(['Klassenbester', '1er Schüler', 'Partyborner'])
ticketType: string;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateTicketDto } from './create-ticket.dto';
export class UpdateTicketDto extends PartialType(CreateTicketDto) {}

View File

@@ -0,0 +1 @@
export class Ticket {}

View File

@@ -0,0 +1,33 @@
import { Controller, Get, Post, Body, Param, Delete, Put, Query } from '@nestjs/common';
import { TicketsService } from './tickets.service';
import { CreateTicketDto } from './dto/create-ticket.dto';
@Controller('tickets')
export class TicketsController {
constructor(private readonly ticketsService: TicketsService) {}
@Post()
create(@Body() createTicketDto: CreateTicketDto) {
return this.ticketsService.create(createTicketDto);
}
@Get()
findAll(@Query('eventId') eventId: string) {
return this.ticketsService.findAll(eventId);
}
@Post(':id/resend')
resendEmail(@Param('id') id: string) {
return this.ticketsService.resendEmail(id);
}
@Put(':id/status')
updateStatus(@Param('id') id: string, @Body('status') status: string) {
return this.ticketsService.updateStatus(id, status);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.ticketsService.remove(id);
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TicketsService } from './tickets.service';
import { TicketsController } from './tickets.controller';
import { PrismaModule } from '../prisma/prisma.module';
import { MailModule } from '../mail/mail.module';
@Module({
imports: [PrismaModule, MailModule],
controllers: [TicketsController],
providers: [TicketsService],
})
export class TicketsModule {}

View File

@@ -0,0 +1,101 @@
import { Injectable } from '@nestjs/common';
import { CreateTicketDto } from './dto/create-ticket.dto';
import { PrismaService } from '../prisma/prisma.service';
import { MailService } from '../mail/mail.service';
import * as QRCode from 'qrcode'; // Import qrcode library
@Injectable()
export class TicketsService {
constructor(
private prisma: PrismaService,
private mailService: MailService,
) {}
async create(createTicketDto: CreateTicketDto) {
const ticket = await this.prisma.ticket.create({
data: createTicketDto,
include: { event: true },
});
try {
// Generate QR code buffer from ticket ID
const qrCodeBuffer = await QRCode.toBuffer(ticket.id, {
errorCorrectionLevel: 'H', // High error correction
type: 'png', // Output as PNG image
width: 250, // QR code width
color: {
dark: '#000000', // Black dots
light: '#FFFFFF', // White background
},
});
await this.mailService.sendTicket(
ticket.attendeeEmail,
ticket.attendeeName,
ticket.id,
ticket.event.name,
qrCodeBuffer, // Pass the QR code buffer
);
} catch (error) {
console.error('Failed to send ticket email:', error);
// Optionally throw error or just log it.
// For now, we log it so the ticket creation doesn't fail if email fails (unless strict requirement).
}
return ticket;
}
findAll(eventId: string) {
return this.prisma.ticket.findMany({
where: { eventId },
});
}
async resendEmail(id: string) {
const ticket = await this.prisma.ticket.findUnique({
where: { id },
include: { event: true },
});
if (!ticket) {
throw new Error('Ticket not found');
}
try {
const qrCodeBuffer = await QRCode.toBuffer(ticket.id, {
errorCorrectionLevel: 'H',
type: 'png',
width: 250,
color: {
dark: '#000000',
light: '#FFFFFF',
},
});
await this.mailService.sendTicket(
ticket.attendeeEmail,
ticket.attendeeName,
ticket.id,
ticket.event.name,
qrCodeBuffer,
);
return { success: true, message: 'Email resent successfully' };
} catch (error) {
console.error('Failed to resend ticket email:', error);
throw new Error('Failed to send email');
}
}
updateStatus(id: string, status: string) {
return this.prisma.ticket.update({
where: { id },
data: { status },
});
}
remove(id: string) {
return this.prisma.ticket.delete({
where: { id },
});
}
}