feat(security): implement phase 1 core security & build hygiene
Some checks failed
Deploy Canina / deploy (push) Failing after 28s

This commit is contained in:
پارسا آقایی 2026-08-06 21:15:18 +03:30
parent f4cdca4ed1
commit a64291a19a
8 changed files with 35 additions and 9 deletions

View File

@ -56,6 +56,7 @@ async function main() {
const createdProduct = await prisma.product.upsert({
where: { artNo },
update: {
slug: artNo,
nameFa: name,
nameEn: name,
scientificTagline,

View File

@ -33,12 +33,11 @@ export class AuthController {
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'ارسال کد تایید پیامکی (OTP)' })
@ApiOkResponse({
description: 'کد با موفقیت ارسال شد (کد تایید در پاسخ بازگردانده می‌شود)',
description: 'کد با موفقیت به شماره تلفن همراه پیامک شد',
schema: {
example: {
success: true,
message: 'کد تایید ارسال شد',
code: '12345',
message: 'کد تایید با موفقیت به شماره شما پیامک شد.',
},
},
})

View File

@ -11,7 +11,7 @@ import { UsersModule } from '../users/users.module';
UsersModule,
PassportModule,
JwtModule.register({
secret: process.env.JWT_SECRET || 'super-secret-key',
secret: process.env.JWT_ACCESS_SECRET,
signOptions: { expiresIn: '7d' },
}),
],

View File

@ -3,6 +3,7 @@ import { AuthService } from './auth.service';
import { PrismaService } from '../prisma/prisma.service';
import { JwtService } from '@nestjs/jwt';
import { RedisService } from '../redis/redis.service';
import { SmsService } from '../common/services/sms.service';
import { BadRequestException } from '@nestjs/common';
describe('AuthService', () => {
@ -10,6 +11,7 @@ describe('AuthService', () => {
let prisma: PrismaService;
let jwt: JwtService;
let redis: RedisService;
let sms: SmsService;
const mockPrisma = {
user: {
@ -28,6 +30,10 @@ describe('AuthService', () => {
del: jest.fn(),
};
const mockSms = {
sendOtp: jest.fn().mockResolvedValue(true),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
@ -35,6 +41,7 @@ describe('AuthService', () => {
{ provide: PrismaService, useValue: mockPrisma },
{ provide: JwtService, useValue: mockJwt },
{ provide: RedisService, useValue: mockRedis },
{ provide: SmsService, useValue: mockSms },
],
}).compile();
@ -42,6 +49,7 @@ describe('AuthService', () => {
prisma = module.get<PrismaService>(PrismaService);
jwt = module.get<JwtService>(JwtService);
redis = module.get<RedisService>(RedisService);
sms = module.get<SmsService>(SmsService);
});
afterEach(() => {
@ -56,7 +64,7 @@ describe('AuthService', () => {
it('should generate a 5 digit OTP, save it in Redis, and not expose it in response', async () => {
const result = await service.sendOtp({ phoneNumber: '09123456789' });
expect(result.success).toBe(true);
expect(result.message).toBe('کد تایید ارسال شد');
expect(result.message).toBe('کد تایید با موفقیت به شماره شما پیامک شد.');
// Code should NOT be in the response (security)
expect((result as any).code).toBeUndefined();
// But it should have been stored in Redis

View File

@ -6,6 +6,7 @@ import { SendOtpDto } from './dto/send-otp.dto';
import { VerifyOtpDto } from './dto/verify-otp.dto';
import { SmsService } from '../common/services/sms.service';
import * as bcrypt from 'bcryptjs';
import * as crypto from 'crypto';
@Injectable()
export class AuthService {
@ -19,7 +20,7 @@ export class AuthService {
async sendOtp(sendOtpDto: SendOtpDto) {
const { phoneNumber } = sendOtpDto;
const code = Math.floor(10000 + Math.random() * 90000).toString();
const code = crypto.randomInt(10000, 100000).toString();
await this.redisService.set(`otp:${phoneNumber}`, code, 120);

View File

@ -9,7 +9,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET || 'super-secret-key',
secretOrKey: process.env.JWT_ACCESS_SECRET!,
});
}

View File

@ -1,7 +1,7 @@
import { Controller, Get, Res } from '@nestjs/common';
import { ApiExcludeController } from '@nestjs/swagger';
import { PrismaService } from '../prisma/prisma.service';
import * as express from 'express';
import type { Response } from 'express';
@ApiExcludeController()
@Controller('metrics')
@ -15,7 +15,7 @@ export class MetricsController {
}
@Get()
async getMetrics(@Res() res: express.Response) {
async getMetrics(@Res() res: Response) {
const memory = process.memoryUsage();
const cpu = process.cpuUsage();

View File

@ -9,6 +9,23 @@ import { PrismaExceptionFilter } from './common/filters/prisma-exception.filter'
import helmet from 'helmet';
async function bootstrap() {
const jwtAccessSecret = process.env.JWT_ACCESS_SECRET;
const jwtRefreshSecret = process.env.JWT_REFRESH_SECRET;
if (!jwtAccessSecret || jwtAccessSecret.trim().length < 32) {
console.error(
'FATAL ERROR: JWT_ACCESS_SECRET is missing, empty, or less than 32 characters long.',
);
process.exit(1);
}
if (!jwtRefreshSecret || jwtRefreshSecret.trim().length < 32) {
console.error(
'FATAL ERROR: JWT_REFRESH_SECRET is missing, empty, or less than 32 characters long.',
);
process.exit(1);
}
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// Serve static uploads folder