feat: Wahl-Phasen, Verantwortliche-Invites, Auth fixes, GRUPPE chat #1
@@ -36,6 +36,10 @@ client's host - no separate web server is needed.
|
||||
- `auth/` — Authentik resource-server strategy (`AuthGuard('authentik')`) +
|
||||
guest invite-code login issuing a locally-signed JWT (`AuthGuard('guest')`).
|
||||
- `kc/` — KC (event) creation/listing, Leitungsteam-only.
|
||||
- `gemeinde/` — Gemeinde (congregation) CRUD per KC (`POST /gemeinde`,
|
||||
`GET /gemeinde?kcId=`, `GET/PATCH/DELETE /gemeinde/:id`), Leitungsteam-only.
|
||||
Gemeinde Verantwortliche/Teamer get their own Gemeinde from their
|
||||
`Membership`, not from this endpoint.
|
||||
- `wahl/` — Wahl/Workshop administration (Leitungsteam-only), guest
|
||||
Teilnehmer submission, Force-Zuteilung overrides, and `ZuteilungService`:
|
||||
a faithful port of the WP plugin's `kc_run_zuteilung` (force-assignments →
|
||||
|
||||
@@ -5,6 +5,7 @@ import { join } from 'path';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { KcModule } from './kc/kc.module';
|
||||
import { GemeindeModule } from './gemeinde/gemeinde.module';
|
||||
import { WahlModule } from './wahl/wahl.module';
|
||||
import { FilesModule } from './files/files.module';
|
||||
import { ChatModule } from './chat/chat.module';
|
||||
@@ -23,6 +24,7 @@ import { SyncModule } from './sync/sync.module';
|
||||
SyncModule,
|
||||
AuthModule,
|
||||
KcModule,
|
||||
GemeindeModule,
|
||||
WahlModule,
|
||||
FilesModule,
|
||||
ChatModule,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class CreateGemeindeDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
kcId!: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name!: string;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class UpdateGemeindeDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name!: string;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { GemeindeService } from './gemeinde.service';
|
||||
import { CreateGemeindeDto } from './dto/create-gemeinde.dto';
|
||||
import { UpdateGemeindeDto } from './dto/update-gemeinde.dto';
|
||||
import { Roles } from '../common/roles.decorator';
|
||||
import { RolesGuard } from '../common/roles.guard';
|
||||
import { Role } from '../common/role.enum';
|
||||
|
||||
/// Gemeinde (congregation) management. Reserved for the Leitungsteam, which is
|
||||
/// global across all KCs (see RolesGuard); Gemeinde Verantwortliche/Teamer
|
||||
/// learn their own Gemeinde from their Membership, not from this endpoint.
|
||||
@Controller('gemeinde')
|
||||
@UseGuards(AuthGuard('authentik'), RolesGuard)
|
||||
@Roles(Role.LEITUNGSTEAM)
|
||||
export class GemeindeController {
|
||||
constructor(private readonly gemeinde: GemeindeService) {}
|
||||
|
||||
@Post()
|
||||
create(@Body() dto: CreateGemeindeDto) {
|
||||
return this.gemeinde.create(dto.kcId, dto.name);
|
||||
}
|
||||
|
||||
@Get()
|
||||
list(@Query('kcId') kcId: string) {
|
||||
return this.gemeinde.list(kcId);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
get(@Param('id') id: string) {
|
||||
return this.gemeinde.get(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Body() dto: UpdateGemeindeDto) {
|
||||
return this.gemeinde.update(id, dto.name);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.gemeinde.remove(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { GemeindeService } from './gemeinde.service';
|
||||
import { GemeindeController } from './gemeinde.controller';
|
||||
|
||||
@Module({
|
||||
providers: [GemeindeService],
|
||||
controllers: [GemeindeController],
|
||||
})
|
||||
export class GemeindeModule {}
|
||||
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
ConflictException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { Prisma, SyncOperation } from '@prisma/client';
|
||||
import { PrismaClient } from '../prisma/prisma.module';
|
||||
import { SyncService } from '../sync/sync.service';
|
||||
|
||||
/// CRUD for Gemeinden (congregations) within a KC. Creating/renaming/deleting
|
||||
/// is Leitungsteam-only (see GemeindeController); other team roles may list
|
||||
/// and read the Gemeinden of their KC for onboarding/assignment UIs.
|
||||
@Injectable()
|
||||
export class GemeindeService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaClient,
|
||||
private readonly sync: SyncService,
|
||||
) {}
|
||||
|
||||
async create(kcId: string, name: string) {
|
||||
const kc = await this.prisma.kc.findUnique({ where: { id: kcId } });
|
||||
if (!kc) {
|
||||
throw new NotFoundException('KC not found');
|
||||
}
|
||||
try {
|
||||
const gemeinde = await this.prisma.gemeinde.create({ data: { kcId, name } });
|
||||
await this.sync.capture('Gemeinde', SyncOperation.CREATE, gemeinde.id, gemeinde);
|
||||
return gemeinde;
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof Prisma.PrismaClientKnownRequestError &&
|
||||
err.code === 'P2002'
|
||||
) {
|
||||
throw new ConflictException('A Gemeinde with this name already exists in this KC');
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
list(kcId: string) {
|
||||
return this.prisma.gemeinde.findMany({
|
||||
where: { kcId },
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
async get(id: string) {
|
||||
const gemeinde = await this.prisma.gemeinde.findUnique({ where: { id } });
|
||||
if (!gemeinde) {
|
||||
throw new NotFoundException('Gemeinde not found');
|
||||
}
|
||||
return gemeinde;
|
||||
}
|
||||
|
||||
async update(id: string, name: string) {
|
||||
await this.get(id);
|
||||
try {
|
||||
const gemeinde = await this.prisma.gemeinde.update({
|
||||
where: { id },
|
||||
data: { name },
|
||||
});
|
||||
await this.sync.capture('Gemeinde', SyncOperation.UPDATE, gemeinde.id, gemeinde);
|
||||
return gemeinde;
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof Prisma.PrismaClientKnownRequestError &&
|
||||
err.code === 'P2002'
|
||||
) {
|
||||
throw new ConflictException('A Gemeinde with this name already exists in this KC');
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
await this.get(id);
|
||||
const gemeinde = await this.prisma.gemeinde.delete({ where: { id } });
|
||||
await this.sync.capture('Gemeinde', SyncOperation.DELETE, gemeinde.id, gemeinde);
|
||||
return gemeinde;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user