feat(backend): QR-Code-System (Modul 22)
QrModule mit GET /qr/:entityType/:id.svg fuer colonies (Deep-Link direkt zu /inspections/new), apiaries, queens, honey-batches und storage-containers. Nutzt pdfkit-Nachbar qrcode zur SVG-Erzeugung.
This commit is contained in:
1 parent
a7ded02015
commit
dd88b6a92c
9 files changed
+427
-15
No files matched your search
@@ -21,6 +21,7 @@ import { StorageModule } from './storage/storage.module';
|
||||
import { DashboardModule } from './dashboard/dashboard.module';
|
||||
import { SearchModule } from './search/search.module';
|
||||
import { ExportModule } from './export/export.module';
|
||||
import { QrModule } from './qr/qr.module';
|
||||
import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from './auth/guards/roles.guard';
|
||||
|
||||
@@ -47,6 +48,7 @@ import { RolesGuard } from './auth/guards/roles.guard';
|
||||
DashboardModule,
|
||||
SearchModule,
|
||||
ExportModule,
|
||||
QrModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Controller, Get, Param, Res } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import type { Response } from 'express';
|
||||
import { QrService } from './qr.service';
|
||||
|
||||
@ApiTags('qr')
|
||||
@ApiBearerAuth()
|
||||
@Controller('qr')
|
||||
export class QrController {
|
||||
constructor(private readonly qrService: QrService) {}
|
||||
|
||||
@Get(':entityType/:id.svg')
|
||||
async svg(
|
||||
@Param('entityType') entityType: string,
|
||||
@Param('id') id: string,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const svg = await this.qrService.generateSvg(entityType, id);
|
||||
res.setHeader('Content-Type', 'image/svg+xml');
|
||||
res.send(svg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { QrController } from './qr.controller';
|
||||
import { QrService } from './qr.service';
|
||||
|
||||
@Module({
|
||||
controllers: [QrController],
|
||||
providers: [QrService],
|
||||
})
|
||||
export class QrModule {}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { QrService } from './qr.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
describe('QrService', () => {
|
||||
let service: QrService;
|
||||
let prisma: {
|
||||
colony: { findUnique: jest.Mock };
|
||||
apiary: { findUnique: jest.Mock };
|
||||
queen: { findUnique: jest.Mock };
|
||||
honeyBatch: { findUnique: jest.Mock };
|
||||
storageContainer: { findUnique: jest.Mock };
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
prisma = {
|
||||
colony: { findUnique: jest.fn() },
|
||||
apiary: { findUnique: jest.fn() },
|
||||
queen: { findUnique: jest.fn() },
|
||||
honeyBatch: { findUnique: jest.fn() },
|
||||
storageContainer: { findUnique: jest.fn() },
|
||||
};
|
||||
service = new QrService(prisma as unknown as PrismaService);
|
||||
});
|
||||
|
||||
it('rejects an unsupported entity type', async () => {
|
||||
await expect(service.generateSvg('unknown', 'id-1')).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
|
||||
it('throws NotFoundException when the object does not exist', async () => {
|
||||
prisma.colony.findUnique.mockResolvedValueOnce(null);
|
||||
await expect(
|
||||
service.generateSvg('colonies', 'missing'),
|
||||
).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
|
||||
it('generates an SVG QR code for an existing colony', async () => {
|
||||
prisma.colony.findUnique.mockResolvedValueOnce({ id: 'colony-1' });
|
||||
const svg = await service.generateSvg('colonies', 'colony-1');
|
||||
expect(svg).toContain('<svg');
|
||||
});
|
||||
|
||||
it('generates an SVG QR code for an existing storage container', async () => {
|
||||
prisma.storageContainer.findUnique.mockResolvedValueOnce({ id: 'sc-1' });
|
||||
const svg = await service.generateSvg('storage-containers', 'sc-1');
|
||||
expect(svg).toContain('<svg');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
Injectable,
|
||||
BadRequestException,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import * as QRCode from 'qrcode';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
const ENTITY_TYPES = [
|
||||
'colonies',
|
||||
'apiaries',
|
||||
'queens',
|
||||
'honey-batches',
|
||||
'storage-containers',
|
||||
] as const;
|
||||
type EntityType = (typeof ENTITY_TYPES)[number];
|
||||
|
||||
function isEntityType(value: string): value is EntityType {
|
||||
return (ENTITY_TYPES as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class QrService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async generateSvg(entityType: string, id: string): Promise<string> {
|
||||
if (!isEntityType(entityType)) {
|
||||
throw new BadRequestException(
|
||||
`Unsupported QR entity type: ${entityType}`,
|
||||
);
|
||||
}
|
||||
|
||||
const exists = await this.entityExists(entityType, id);
|
||||
if (!exists) {
|
||||
throw new NotFoundException('Object not found');
|
||||
}
|
||||
|
||||
const frontendOrigin =
|
||||
process.env.FRONTEND_ORIGIN ?? 'http://localhost:3000';
|
||||
const url = `${frontendOrigin}${this.deepLink(entityType, id)}`;
|
||||
return QRCode.toString(url, { type: 'svg', margin: 1, width: 256 });
|
||||
}
|
||||
|
||||
private async entityExists(
|
||||
entityType: EntityType,
|
||||
id: string,
|
||||
): Promise<boolean> {
|
||||
switch (entityType) {
|
||||
case 'colonies':
|
||||
return (
|
||||
(await this.prisma.colony.findUnique({ where: { id } })) !== null
|
||||
);
|
||||
case 'apiaries':
|
||||
return (
|
||||
(await this.prisma.apiary.findUnique({ where: { id } })) !== null
|
||||
);
|
||||
case 'queens':
|
||||
return (await this.prisma.queen.findUnique({ where: { id } })) !== null;
|
||||
case 'honey-batches':
|
||||
return (
|
||||
(await this.prisma.honeyBatch.findUnique({ where: { id } })) !== null
|
||||
);
|
||||
case 'storage-containers':
|
||||
return (
|
||||
(await this.prisma.storageContainer.findUnique({ where: { id } })) !==
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private deepLink(entityType: EntityType, id: string): string {
|
||||
switch (entityType) {
|
||||
case 'colonies':
|
||||
return `/inspections/new?colonyId=${id}`;
|
||||
case 'apiaries':
|
||||
return `/apiaries/${id}`;
|
||||
case 'queens':
|
||||
return `/queens/${id}`;
|
||||
case 'honey-batches':
|
||||
return `/honey-batches/${id}`;
|
||||
case 'storage-containers':
|
||||
return `/storage`;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user