feat: bootstrap codexdash app

This commit is contained in:
2026-05-01 01:33:57 +09:00
commit 0ced12cb81
55 changed files with 10481 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
import {
CurrentUser,
type AuthenticatedUser,
} from '../common/current-user.decorator';
import { JwtAuthGuard } from '../common/jwt-auth.guard';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('register')
register(@Body() dto: RegisterDto) {
return this.authService.register(dto);
}
@Post('login')
login(@Body() dto: LoginDto) {
return this.authService.login(dto);
}
@UseGuards(JwtAuthGuard)
@Get('me')
me(@CurrentUser() user: AuthenticatedUser) {
return this.authService.me(user.sub);
}
}

View File

@@ -0,0 +1,23 @@
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from '../common/jwt-auth.guard';
@Module({
imports: [
ConfigModule,
JwtModule.registerAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET') ?? 'change-me',
signOptions: { expiresIn: '30d' },
}),
}),
],
controllers: [AuthController],
providers: [AuthService, JwtAuthGuard],
exports: [JwtModule, JwtAuthGuard],
})
export class AuthModule {}

View File

@@ -0,0 +1,84 @@
import {
ConflictException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as argon2 from 'argon2';
import { AuthResponse, UserProfile } from '@codexdash/shared-types';
import { PrismaService } from '../prisma/prisma.service';
import { LoginDto } from './dto/login.dto';
import { RegisterDto } from './dto/register.dto';
@Injectable()
export class AuthService {
constructor(
private readonly prisma: PrismaService,
private readonly jwtService: JwtService,
) {}
async register(dto: RegisterDto): Promise<AuthResponse> {
const existing = await this.prisma.user.findUnique({
where: { email: dto.email.toLowerCase() },
});
if (existing) {
throw new ConflictException('Email already registered');
}
const user = await this.prisma.user.create({
data: {
email: dto.email.toLowerCase(),
name: dto.name.trim(),
passwordHash: await argon2.hash(dto.password),
},
});
return this.toAuthResponse(user);
}
async login(dto: LoginDto): Promise<AuthResponse> {
const user = await this.prisma.user.findUnique({
where: { email: dto.email.toLowerCase() },
});
if (!user || !(await argon2.verify(user.passwordHash, dto.password))) {
throw new UnauthorizedException('Invalid email or password');
}
return this.toAuthResponse(user);
}
async me(userId: string): Promise<UserProfile> {
const user = await this.prisma.user.findUniqueOrThrow({
where: { id: userId },
});
return {
id: user.id,
email: user.email,
name: user.name,
createdAt: user.createdAt.toISOString(),
};
}
private async toAuthResponse(user: {
id: string;
email: string;
name: string;
createdAt: Date;
}): Promise<AuthResponse> {
return {
token: await this.jwtService.signAsync({
sub: user.id,
email: user.email,
}),
user: {
id: user.id,
email: user.email,
name: user.name,
createdAt: user.createdAt.toISOString(),
},
};
}
}

View File

@@ -0,0 +1,11 @@
import { IsEmail, IsString, MinLength } from 'class-validator';
import { LoginInput } from '@codexdash/shared-types';
export class LoginDto implements LoginInput {
@IsEmail()
email!: string;
@IsString()
@MinLength(8)
password!: string;
}

View File

@@ -0,0 +1,15 @@
import { IsEmail, IsString, MinLength } from 'class-validator';
import { RegisterInput } from '@codexdash/shared-types';
export class RegisterDto implements RegisterInput {
@IsEmail()
email!: string;
@IsString()
@MinLength(8)
password!: string;
@IsString()
@MinLength(2)
name!: string;
}