- NestJS backend with JWT auth, Prisma ORM, Swagger docs - Vite + React 19 frontend with TypeScript - Tailwind CSS v4 with custom dark theme design system - Auth module: Login, Register, Protected routes - Campaigns module: CRUD, Member management - Full Prisma schema for PF2e campaign management - Docker Compose for PostgreSQL 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';
|
|
|
|
@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);
|
|
}
|
|
}
|