Files
Dimension-47/server/src/modules/auth/auth.service.ts
Alexander Zielonka 94335ecd12 feat: Charaktere-Modul mit Pathbuilder Import
Backend:
- Characters-Modul (CRUD, HP-Tracking, Conditions)
- Pathbuilder 2e JSON Import Service
- Claude API Integration für automatische Übersetzungen
- Translations-Modul mit Datenbank-Caching
- Prisma Schema erweitert (Character, Abilities, Skills, Feats, Items, Resources)

Frontend:
- Kampagnen-Detailseite mit Mitglieder- und Charakterverwaltung
- Charakter erstellen Modal
- Pathbuilder Import Modal (Datei-Upload + JSON-Paste)
- Logo-Integration (Dimension 47 + Zeasy)
- Cinzel Font für Branding

Weitere Änderungen:
- Auth 401 Redirect Fix für Login-Seite
- PROGRESS.md mit Projektfortschritt

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 20:36:44 +01:00

178 lines
4.0 KiB
TypeScript

import {
Injectable,
ConflictException,
UnauthorizedException,
NotFoundException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { PrismaService } from '../../prisma/prisma.service';
import { RegisterDto, LoginDto } from './dto';
import { UserRole } from '../../generated/prisma/client.js';
@Injectable()
export class AuthService {
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
) {}
async register(dto: RegisterDto) {
// Check if username or email already exists
const existingUser = await this.prisma.user.findFirst({
where: {
OR: [
{ username: dto.username.toLowerCase() },
{ email: dto.email.toLowerCase() },
],
},
});
if (existingUser) {
if (existingUser.username.toLowerCase() === dto.username.toLowerCase()) {
throw new ConflictException('Username already taken');
}
throw new ConflictException('Email already registered');
}
// Hash password
const saltRounds = 10;
const passwordHash = await bcrypt.hash(dto.password, saltRounds);
// Create user
const user = await this.prisma.user.create({
data: {
username: dto.username.toLowerCase(),
email: dto.email.toLowerCase(),
passwordHash,
role: UserRole.PLAYER,
},
select: {
id: true,
username: true,
email: true,
role: true,
avatarUrl: true,
createdAt: true,
},
});
// Generate JWT
const token = this.generateToken(user.id, user.username, user.role);
return {
user,
token,
};
}
async login(dto: LoginDto) {
// Find user by username or email
const user = await this.prisma.user.findFirst({
where: {
OR: [
{ username: dto.identifier.toLowerCase() },
{ email: dto.identifier.toLowerCase() },
],
},
});
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
// Verify password
const isPasswordValid = await bcrypt.compare(dto.password, user.passwordHash);
if (!isPasswordValid) {
throw new UnauthorizedException('Invalid credentials');
}
// Generate JWT
const token = this.generateToken(user.id, user.username, user.role);
return {
user: {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
avatarUrl: user.avatarUrl,
},
token,
};
}
async getProfile(userId: string) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
username: true,
email: true,
role: true,
avatarUrl: true,
createdAt: true,
updatedAt: true,
},
});
if (!user) {
throw new NotFoundException('User not found');
}
return user;
}
async updateProfile(userId: string, data: { avatarUrl?: string }) {
const user = await this.prisma.user.update({
where: { id: userId },
data,
select: {
id: true,
username: true,
email: true,
role: true,
avatarUrl: true,
},
});
return user;
}
async searchUsers(query: string, currentUserId: string) {
const users = await this.prisma.user.findMany({
where: {
AND: [
{ id: { not: currentUserId } },
{
OR: [
{ username: { contains: query, mode: 'insensitive' } },
{ email: { contains: query, mode: 'insensitive' } },
],
},
],
},
select: {
id: true,
username: true,
email: true,
avatarUrl: true,
},
take: 10,
});
return users;
}
private generateToken(userId: string, username: string, role: UserRole): string {
const payload = {
sub: userId,
username,
role,
};
return this.jwtService.sign(payload);
}
}