feat(backend): Wahl-Phasen, Verantwortliche-Invites, Auth fixes
- New VerantwortlicheInvite model: LT-issued invites so a person can register as Gemeinde Verantwortliche(r) for a specific Gemeinde, skipping the self-registration approval step. - Wahl/Workshop/Teilnehmer gain phase support (phasenAnzahl, beschreibung), mirroring the WP plugin's multi-phase elections. Teilnehmer unique constraint now scoped per phase. - Auth: team login + guest auth adjustments, spec coverage. - sync.service.ts: register VerantwortlicheInvite as a synced model. - wahl.service.ts: submitTeilnehmer updated for the new phase-scoped unique key. - client: login/home screen rework, new theme.dart, FCM web tweaks. - .gitignore: ignore .DS_Store. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -38,19 +38,44 @@ export class TeamAuthService {
|
||||
this.secret = config.getOrThrow<string>('TEAM_JWT_SECRET');
|
||||
}
|
||||
|
||||
async login(email: string, password: string): Promise<{ accessToken: string }> {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { email: email.toLowerCase() },
|
||||
include: { memberships: true },
|
||||
/// Logs a Teamer in by email (legacy) OR by Gemeinde name — the normal
|
||||
/// path, since a Teamer thinks of their login as "meine Gemeinde" rather
|
||||
/// than an email address. A Gemeinde can have several Teamer accounts, so
|
||||
/// a name lookup tries the password against every active GEMEINDE_TEAMER
|
||||
/// membership for that Gemeinde (case-insensitive, trimmed name) until one
|
||||
/// matches, rather than assuming a 1:1 Gemeinde-to-account mapping.
|
||||
async login(
|
||||
credentials: { email?: string; gemeindeName?: string },
|
||||
password: string,
|
||||
): Promise<{ accessToken: string }> {
|
||||
if (credentials.email) {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { email: credentials.email.toLowerCase() },
|
||||
});
|
||||
if (!user || !user.passwordHash || !(await bcrypt.compare(password, user.passwordHash))) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
return { accessToken: this.sign(user.id) };
|
||||
}
|
||||
|
||||
const gemeindeName = credentials.gemeindeName?.trim();
|
||||
if (!gemeindeName) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
const memberships = await this.prisma.membership.findMany({
|
||||
where: {
|
||||
role: Role.GEMEINDE_TEAMER,
|
||||
status: 'ACTIVE',
|
||||
gemeinde: { name: { equals: gemeindeName, mode: 'insensitive' } },
|
||||
},
|
||||
include: { user: true },
|
||||
});
|
||||
if (!user || !user.passwordHash) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
for (const m of memberships) {
|
||||
if (m.user.passwordHash && (await bcrypt.compare(password, m.user.passwordHash))) {
|
||||
return { accessToken: this.sign(m.user.id) };
|
||||
}
|
||||
}
|
||||
const ok = await bcrypt.compare(password, user.passwordHash);
|
||||
if (!ok) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
return { accessToken: this.sign(user.id) };
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
|
||||
/// Redeems an invite token and creates the local Teamer account + its
|
||||
|
||||
Reference in New Issue
Block a user