feat(security): helmet + rate limit + env hardening — 8.3
This commit is contained in:
@@ -1,15 +1,13 @@
|
||||
# ReLoop backend env
|
||||
# Copy to .env and fill in.
|
||||
|
||||
DATABASE_URL="mysql://reloop:CHANGE_ME@localhost:3306/reloop"
|
||||
# Database
|
||||
DATABASE_URL="mysql://user:pass@localhost:3306/reloop_v2"
|
||||
|
||||
# JWT
|
||||
JWT_SECRET="change-me-in-prod-really-long-random-string"
|
||||
JWT_EXPIRES_IN="30d"
|
||||
JWT_SECRET="change-me-to-a-long-random-string-32-chars-min"
|
||||
JWT_EXPIRES_IN="7d"
|
||||
|
||||
# CORS
|
||||
CORS_ORIGINS="https://reloop.nabomhalang.co.kr,http://localhost:3000"
|
||||
# CORS (comma-separated origins)
|
||||
CORS_ORIGINS="http://localhost:3000,https://reloop.nabomhalang.co.kr"
|
||||
|
||||
# Server
|
||||
PORT=3001
|
||||
NODE_ENV=production
|
||||
# Node
|
||||
NODE_ENV="production"
|
||||
PORT=4000
|
||||
|
||||
@@ -66,8 +66,7 @@ async function main() {
|
||||
onboardedAt: new Date(),
|
||||
},
|
||||
});
|
||||
// TODO(phase7): 프로덕션 seed 에서는 평문 비밀번호 stdout 출력 제거
|
||||
console.log(` user: ${user.email} (id=${user.id}) password=${DEMO_PASSWORD}`);
|
||||
console.log(` user: ${user.email} (id=${user.id})`);
|
||||
|
||||
for (const s of SUBJECTS) {
|
||||
const subject = await prisma.subject.upsert({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HttpException, HttpStatus, Module } from '@nestjs/common';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { HealthModule } from './health/health.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
@@ -14,10 +15,22 @@ import { DashboardModule } from './dashboard/dashboard.module';
|
||||
import { StatsModule } from './stats/stats.module';
|
||||
import { ProblemSetsModule } from './problem-sets/problem-sets.module';
|
||||
|
||||
class AppThrottlerGuard extends ThrottlerGuard {
|
||||
protected async throwThrottlingException(): Promise<void> {
|
||||
throw new HttpException(
|
||||
{
|
||||
statusCode: HttpStatus.TOO_MANY_REQUESTS,
|
||||
message: '요청이 너무 많아요. 잠시 후 다시 시도해주세요.',
|
||||
},
|
||||
HttpStatus.TOO_MANY_REQUESTS,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
ThrottlerModule.forRoot([{ ttl: 60_000, limit: 120 }]),
|
||||
ThrottlerModule.forRoot([{ ttl: 60_000, limit: 60 }]),
|
||||
PrismaModule,
|
||||
HealthModule,
|
||||
AuthModule,
|
||||
@@ -31,5 +44,11 @@ import { ProblemSetsModule } from './problem-sets/problem-sets.module';
|
||||
StatsModule,
|
||||
ProblemSetsModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: AppThrottlerGuard,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Post,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
import { AuthService } from './auth.service';
|
||||
import { LoginDto, RegisterDto } from './dto';
|
||||
import { JwtAuthGuard } from './jwt-auth.guard';
|
||||
@@ -16,11 +17,13 @@ export class AuthController {
|
||||
constructor(private readonly auth: AuthService) {}
|
||||
|
||||
@Post('register')
|
||||
@Throttle({ default: { ttl: 60_000, limit: 10 } })
|
||||
register(@Body() dto: RegisterDto) {
|
||||
return this.auth.register(dto);
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
@Throttle({ default: { ttl: 60_000, limit: 10 } })
|
||||
login(@Body() dto: LoginDto) {
|
||||
return this.auth.login(dto);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe, Logger } from '@nestjs/common';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
import { join } from 'path';
|
||||
import { AppModule } from './app.module';
|
||||
@@ -10,22 +11,51 @@ async function bootstrap() {
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
|
||||
cors: false,
|
||||
});
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||
|
||||
// CORS — allow the frontend origin explicitly (covers prod + dev)
|
||||
const allowedOrigins = (
|
||||
process.env.CORS_ORIGINS ??
|
||||
'https://reloop.nabomhalang.co.kr,http://localhost:3000'
|
||||
(isDevelopment ? 'http://localhost:3000' : '')
|
||||
)
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const expressApp = app.getHttpAdapter().getInstance();
|
||||
expressApp.use(express.json({ limit: '5mb' }));
|
||||
expressApp.use(express.urlencoded({ extended: true, limit: '5mb' }));
|
||||
|
||||
app.enableCors({
|
||||
origin: allowedOrigins,
|
||||
origin: allowedOrigins.length > 0 ? allowedOrigins : false,
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
app.use(helmet({ crossOriginResourcePolicy: false }));
|
||||
app.use(
|
||||
helmet({
|
||||
contentSecurityPolicy: {
|
||||
useDefaults: true,
|
||||
directives: {
|
||||
"default-src": ["'self'"],
|
||||
"style-src": ["'self'", "'unsafe-inline'"],
|
||||
"img-src": ["'self'", 'data:', 'blob:'],
|
||||
"connect-src": ["'self'", 'https:'],
|
||||
},
|
||||
},
|
||||
crossOriginResourcePolicy: false,
|
||||
hsts: {
|
||||
maxAge: 31_536_000,
|
||||
includeSubDomains: true,
|
||||
preload: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expressApp.use((_req, res, next) => {
|
||||
res.setHeader(
|
||||
'Permissions-Policy',
|
||||
'camera=(), microphone=(), geolocation=()',
|
||||
);
|
||||
next();
|
||||
});
|
||||
app.useStaticAssets(join(__dirname, '..', 'uploads'), {
|
||||
prefix: '/uploads/',
|
||||
});
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
# Build-time env var — baked into the client bundle by Next.js.
|
||||
# Must be set BEFORE running `pnpm build`, not at runtime.
|
||||
NEXT_PUBLIC_API_URL=https://reloop-api.nabomhalang.co.kr/api
|
||||
NEXT_PUBLIC_API_URL="https://reloop-api.nabomhalang.co.kr/api"
|
||||
|
||||
Reference in New Issue
Block a user