feat: add P0/P1/P2 features + Swagger enrichment for MVP completeness
Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 12s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 53s
Deploy / Build API Image (push) Failing after 22s
Deploy / Build Web Image (push) Failing after 14s
Deploy / Build AI Services Image (push) Failing after 12s
E2E Tests / Playwright E2E (push) Failing after 9s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 2s
Security Scanning / Trivy Scan — API Image (push) Failing after 50s
Security Scanning / Trivy Scan — Web Image (push) Failing after 38s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 36s
Security Scanning / Trivy Filesystem Scan (push) Failing after 33s
Security Scanning / Security Gate (push) Failing after 1s
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 12s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 53s
Deploy / Build API Image (push) Failing after 22s
Deploy / Build Web Image (push) Failing after 14s
Deploy / Build AI Services Image (push) Failing after 12s
E2E Tests / Playwright E2E (push) Failing after 9s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 2s
Security Scanning / Trivy Scan — API Image (push) Failing after 50s
Security Scanning / Trivy Scan — Web Image (push) Failing after 38s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 36s
Security Scanning / Trivy Filesystem Scan (push) Failing after 33s
Security Scanning / Security Gate (push) Failing after 1s
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
Closes four gaps the Swagger audit flagged as blocking a full MVP demo,
plus a general documentation pass.
P0 — Forgot/Reset password (auth)
- POST /auth/forgot-password (anti-enumeration: always 200)
- POST /auth/reset-password
- Reuses the Redis-OTP pattern from email/phone change; new key prefix
auth:password_reset_otp with 15-min TTL.
- Emits PasswordResetRequestedEvent; new listener in notifications
dispatches the existing password.reset email template (otp +
expiryMinutes variables already in template.service.ts).
- UserEntity gains changePassword(HashedPassword) domain method; reset
also revokes all refresh tokens for the user.
P0 — Favorites module
- New SavedListing Prisma model (unique(userId, listingId)) with User
and Listing back-relations; schema pushed via db push since the
remote DB was out of sync with migration history.
- New apps/api/src/modules/favorites/ module following the reviews
module's shape (DDD/CQRS: domain repo + Prisma impl + 2 commands
+ 2 queries + controller).
- POST /favorites/:listingId, DELETE /favorites/:listingId,
GET /favorites (paginated), GET /favorites/:listingId/check. All
guarded by JwtAuthGuard.
- FavoritesModule wired into AppModule.
P1 — Resend OTP (auth)
- POST /auth/resend-otp for EMAIL_CHANGE | PHONE_CHANGE. Reads the
pending OTP payload out of Redis and re-emits the original event
without minting a new code, so TTL semantics stay intact. Password
reset resend is done by re-POSTing /auth/forgot-password and is
deliberately not in this enum.
P1 — Agent self-upgrade (agents)
- POST /agents/me/upgrade lets a BUYER/SELLER convert to AGENT. Creates
an Agent row (isVerified=false) and flips User.role in one
$transaction. Rejects if already AGENT/ADMIN or if an Agent row
already exists.
P2 — Swagger enrichment
- @ApiConsumes('multipart/form-data') + body schema on listings media
upload.
- GET /subscriptions/quota/:metric now enumerates the real metric
values from METRIC_TO_PLAN_FIELD.
- POST /avm/batch and /analytics/valuation/batch document the max=50
batch size from their DTO's @ArrayMaxSize.
- GET /admin/dashboard gains a realistic response example schema.
- Admin-gated endpoints in projects/transfer/industrial gain concrete
400/401/403/404 responses.
Swagger endpoint count: 170 → 178. Typecheck clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,11 +17,17 @@ import {
|
||||
EndpointRateLimitGuard,
|
||||
UnauthorizedException,
|
||||
} from '@modules/shared';
|
||||
import { ForgotPasswordCommand } from '../../application/commands/forgot-password/forgot-password.command';
|
||||
import { type ForgotPasswordResultDto } from '../../application/commands/forgot-password/forgot-password.handler';
|
||||
import { GenerateKycUploadUrlsCommand } from '../../application/commands/generate-kyc-upload-urls/generate-kyc-upload-urls.command';
|
||||
import { LoginUserCommand } from '../../application/commands/login-user/login-user.command';
|
||||
import { type LoginResult } from '../../application/commands/login-user/login-user.handler';
|
||||
import { RefreshTokenCommand } from '../../application/commands/refresh-token/refresh-token.command';
|
||||
import { RegisterUserCommand } from '../../application/commands/register-user/register-user.command';
|
||||
import { ResendOtpCommand } from '../../application/commands/resend-otp/resend-otp.command';
|
||||
import { type ResendOtpResultDto } from '../../application/commands/resend-otp/resend-otp.handler';
|
||||
import { ResetPasswordCommand } from '../../application/commands/reset-password/reset-password.command';
|
||||
import { type ResetPasswordResultDto } from '../../application/commands/reset-password/reset-password.handler';
|
||||
import { SubmitKycCommand } from '../../application/commands/submit-kyc/submit-kyc.command';
|
||||
import { UpdateProfileCommand } from '../../application/commands/update-profile/update-profile.command';
|
||||
import { type UpdateProfileResultDto } from '../../application/commands/update-profile/update-profile.handler';
|
||||
@@ -38,10 +44,13 @@ import { TokenService, type JwtPayload, type TokenPair } from '../../infrastruct
|
||||
import { type LocalStrategyResult } from '../../infrastructure/strategies/local.strategy';
|
||||
import { CurrentUser } from '../decorators/current-user.decorator';
|
||||
import { Roles } from '../decorators/roles.decorator';
|
||||
import { ForgotPasswordDto } from '../dto/forgot-password.dto';
|
||||
import { GenerateKycUploadUrlsDto } from '../dto/generate-kyc-upload-urls.dto';
|
||||
import { LoginDto } from '../dto/login.dto';
|
||||
import { RefreshTokenDto } from '../dto/refresh-token.dto';
|
||||
import { RegisterDto } from '../dto/register.dto';
|
||||
import { ResendOtpDto } from '../dto/resend-otp.dto';
|
||||
import { ResetPasswordDto } from '../dto/reset-password.dto';
|
||||
import { SubmitKycDto } from '../dto/submit-kyc.dto';
|
||||
import { UpdateProfileDto } from '../dto/update-profile.dto';
|
||||
import { VerifyEmailChangeDto } from '../dto/verify-email-change.dto';
|
||||
@@ -188,6 +197,39 @@ export class AuthController {
|
||||
return { message: 'Đã đăng xuất' };
|
||||
}
|
||||
|
||||
@Throttle({ default: { ttl: 3_600_000, limit: AUTH_RATE_LIMIT }, auth: { ttl: 3_600_000, limit: AUTH_RATE_LIMIT } })
|
||||
@EndpointRateLimit({ limit: IS_TEST ? 10_000 : 3, windowSeconds: 60, keyStrategy: 'ip' })
|
||||
@UseGuards(EndpointRateLimitGuard)
|
||||
@Post('forgot-password')
|
||||
@ApiOperation({
|
||||
summary: 'Request password reset OTP',
|
||||
description:
|
||||
'Sends a password reset code to the user identified by email or phone. Always returns 200 with { sent: true } to prevent account enumeration.',
|
||||
})
|
||||
@ApiResponse({ status: 201, description: 'Reset code sent (or silently ignored for unknown identifier)' })
|
||||
@ApiResponse({ status: 400, description: 'Validation error' })
|
||||
async forgotPassword(
|
||||
@Body() dto: ForgotPasswordDto,
|
||||
): Promise<ForgotPasswordResultDto> {
|
||||
return this.commandBus.execute(new ForgotPasswordCommand(dto.emailOrPhone));
|
||||
}
|
||||
|
||||
@Throttle({ default: { ttl: 3_600_000, limit: AUTH_RATE_LIMIT }, auth: { ttl: 3_600_000, limit: AUTH_RATE_LIMIT } })
|
||||
@EndpointRateLimit({ limit: IS_TEST ? 10_000 : 5, windowSeconds: 60, keyStrategy: 'ip' })
|
||||
@UseGuards(EndpointRateLimitGuard)
|
||||
@Post('reset-password')
|
||||
@ApiOperation({ summary: 'Reset password using OTP code' })
|
||||
@ApiResponse({ status: 201, description: 'Password reset successfully' })
|
||||
@ApiResponse({ status: 400, description: 'Invalid or expired OTP code' })
|
||||
@ApiResponse({ status: 404, description: 'User not found' })
|
||||
async resetPassword(
|
||||
@Body() dto: ResetPasswordDto,
|
||||
): Promise<ResetPasswordResultDto> {
|
||||
return this.commandBus.execute(
|
||||
new ResetPasswordCommand(dto.emailOrPhone, dto.code, dto.newPassword),
|
||||
);
|
||||
}
|
||||
|
||||
@Post('exchange-token')
|
||||
@ApiOperation({ summary: 'Exchange OAuth token pair for httpOnly cookies' })
|
||||
@ApiResponse({ status: 201, description: 'Auth cookies set' })
|
||||
@@ -272,6 +314,24 @@ export class AuthController {
|
||||
return { message: 'Email đã được cập nhật thành công', data: result };
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('resend-otp')
|
||||
@ApiBearerAuth('JWT')
|
||||
@ApiOperation({
|
||||
summary: 'Resend a pending profile-change OTP',
|
||||
description:
|
||||
'Re-emits the existing OTP for an in-flight email or phone change without generating a new code, so the original TTL is preserved. Returns 400 if no OTP is pending. For password-reset re-sends, call POST /auth/forgot-password again.',
|
||||
})
|
||||
@ApiResponse({ status: 201, description: 'OTP re-sent via the originating channel' })
|
||||
@ApiResponse({ status: 400, description: 'No pending OTP request, or invalid context' })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
async resendOtp(
|
||||
@CurrentUser() user: JwtPayload,
|
||||
@Body() dto: ResendOtpDto,
|
||||
): Promise<ResendOtpResultDto> {
|
||||
return this.commandBus.execute(new ResendOtpCommand(user.sub, dto.context));
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('profile/agent')
|
||||
@ApiBearerAuth('JWT')
|
||||
|
||||
Reference in New Issue
Block a user