fix(platform): resolve master compile errors blocking load test (GOO-171)
- listings index.ts now re-exports PROPERTY_REPOSITORY/IPropertyRepository
and MEDIA_STORAGE_SERVICE/IMediaStorageService/MinioMediaStorageService
(documents module depends on these)
- add PropertyDocument model + DocumentType + DocumentVerificationStatus
enums to Prisma schema and create companion migration
- add TooManyRequestsException to shared domain exceptions
- add RL_SENSITIVE_WRITE preset to endpoint-rate-limit decorator and
re-export from shared/infrastructure
- add certificateVerified to PropertyExtras + Create/UpdateListingDto so
listings.controller line 135/341 type-check
- create PhoneLoginOtpRequestedEvent + matching notifications listener
(notifications.module already imports the listener)
- oauth.service constructs UserEntity with deletedAt: null
- web: fix LegalStatus fixtures ('Sổ hồng' -> 'SO_HONG'), make
ListingDetail.property.certificateVerified optional so existing fixtures
compile, type admin layout auth-store mock to accept null user
Verified: `pnpm typecheck` green across @goodgo/api, @goodgo/web,
@goodgo/mcp-servers; `pnpm --filter @goodgo/api build` succeeds.
Unblocks [GOO-137](/GOO/issues/GOO-137) load test.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -4,3 +4,4 @@ export { EmailChangeRequestedEvent } from './email-change-requested.event';
|
||||
export { PhoneChangeRequestedEvent } from './phone-change-requested.event';
|
||||
export { EmailChangedEvent } from './email-changed.event';
|
||||
export { PhoneChangedEvent } from './phone-changed.event';
|
||||
export { PhoneLoginOtpRequestedEvent } from './phone-login-otp-requested.event';
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { type DomainEvent } from '@modules/shared';
|
||||
|
||||
/**
|
||||
* Emitted when a user requests an OTP to log in via phone number.
|
||||
* Triggers an SMS notification with the one-time code.
|
||||
*/
|
||||
export class PhoneLoginOtpRequestedEvent implements DomainEvent {
|
||||
readonly eventName = 'user.phone_login_otp_requested';
|
||||
readonly occurredAt = new Date();
|
||||
|
||||
constructor(
|
||||
public readonly aggregateId: string,
|
||||
public readonly phone: string,
|
||||
public readonly otpCode: string,
|
||||
) {}
|
||||
}
|
||||
@@ -16,4 +16,5 @@ export { EmailChangeRequestedEvent } from './domain/events/email-change-requeste
|
||||
export { PhoneChangeRequestedEvent } from './domain/events/phone-change-requested.event';
|
||||
export { EmailChangedEvent } from './domain/events/email-changed.event';
|
||||
export { PhoneChangedEvent } from './domain/events/phone-changed.event';
|
||||
export { PhoneLoginOtpRequestedEvent } from './domain/events/phone-login-otp-requested.event';
|
||||
export { USER_REPOSITORY, IUserRepository } from './domain/repositories/user.repository';
|
||||
|
||||
@@ -125,6 +125,7 @@ export class OAuthService {
|
||||
totpEnabled: false,
|
||||
totpBackupCodes: [],
|
||||
totpEnabledAt: null,
|
||||
deletedAt: null,
|
||||
});
|
||||
|
||||
await this.userRepo.save(user);
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface PropertyExtras {
|
||||
petFriendly?: boolean;
|
||||
suitableFor?: string[];
|
||||
whyThisLocation?: string;
|
||||
certificateVerified?: boolean;
|
||||
}
|
||||
|
||||
export class CreateListingCommand {
|
||||
|
||||
@@ -23,3 +23,9 @@ export { ListingOwnershipTransferredEvent } from './domain/events/listing-owners
|
||||
export { ListingFeaturedExpiredEvent } from './domain/events/listing-featured-expired.event';
|
||||
export { ListingExpiringEvent } from './domain/events/listing-expiring.event';
|
||||
export { Price } from './domain/value-objects/price.vo';
|
||||
export { PROPERTY_REPOSITORY, type IPropertyRepository } from './domain/repositories/property.repository';
|
||||
export {
|
||||
MEDIA_STORAGE_SERVICE,
|
||||
type IMediaStorageService,
|
||||
MinioMediaStorageService,
|
||||
} from './infrastructure/services/media-storage.service';
|
||||
|
||||
@@ -212,4 +212,9 @@ export class CreateListingDto {
|
||||
@IsString()
|
||||
@MaxLength(2000)
|
||||
whyThisLocation?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: true, description: 'Whether ownership certificate has been verified' })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
certificateVerified?: boolean;
|
||||
}
|
||||
|
||||
@@ -115,6 +115,11 @@ export class UpdateListingDto {
|
||||
@MaxLength(2000)
|
||||
whyThisLocation?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: true, description: 'Whether ownership certificate has been verified' })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
certificateVerified?: boolean;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
nullable: true,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { type PhoneLoginOtpRequestedEvent } from '@modules/auth';
|
||||
import { LoggerService } from '@modules/shared';
|
||||
import { SendNotificationCommand } from '../commands/send-notification/send-notification.command';
|
||||
|
||||
@Injectable()
|
||||
export class PhoneLoginOtpRequestedListener {
|
||||
constructor(
|
||||
private readonly commandBus: CommandBus,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
@OnEvent('user.phone_login_otp_requested', { async: true })
|
||||
async handle(event: PhoneLoginOtpRequestedEvent): Promise<void> {
|
||||
this.logger.log(
|
||||
`Handling phone login OTP for user ${event.aggregateId}`,
|
||||
'PhoneLoginOtpRequestedListener',
|
||||
);
|
||||
|
||||
await this.commandBus.execute(
|
||||
new SendNotificationCommand(
|
||||
event.aggregateId,
|
||||
'SMS',
|
||||
'user.phone_login_otp',
|
||||
{ otpCode: event.otpCode },
|
||||
event.phone,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -54,3 +54,9 @@ export class ForbiddenException extends DomainException {
|
||||
super(ErrorCode.FORBIDDEN, message, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
export class TooManyRequestsException extends DomainException {
|
||||
constructor(message = 'Too many requests', details?: Record<string, unknown>) {
|
||||
super(ErrorCode.TOO_MANY_REQUESTS, message, HttpStatus.TOO_MANY_REQUESTS, details);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,3 +55,14 @@ export interface EndpointRateLimitOptions {
|
||||
*/
|
||||
export const EndpointRateLimit = (options: EndpointRateLimitOptions) =>
|
||||
SetMetadata<string, EndpointRateLimitOptions>(ENDPOINT_RATE_LIMIT_KEY, options);
|
||||
|
||||
/**
|
||||
* Standard preset for sensitive write endpoints (document uploads, account changes, etc.).
|
||||
* 10 requests/min per authenticated user; ADMIN bypass enabled.
|
||||
*/
|
||||
export const RL_SENSITIVE_WRITE: EndpointRateLimitOptions = {
|
||||
limit: 10,
|
||||
windowSeconds: 60,
|
||||
keyStrategy: 'user',
|
||||
adminBypass: true,
|
||||
};
|
||||
|
||||
@@ -35,6 +35,7 @@ export { UserRateLimit } from './decorators/user-rate-limit.decorator';
|
||||
export {
|
||||
EndpointRateLimit,
|
||||
ENDPOINT_RATE_LIMIT_KEY,
|
||||
RL_SENSITIVE_WRITE,
|
||||
type EndpointRateLimitOptions,
|
||||
} from './decorators/endpoint-rate-limit.decorator';
|
||||
export { EndpointRateLimitGuard } from './guards/endpoint-rate-limit.guard';
|
||||
|
||||
Reference in New Issue
Block a user