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>
178 lines
4.0 KiB
TypeScript
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);
|
|
}
|
|
}
|