diff --git a/.github/prompts/autonomy-version.json b/.github/prompts/autonomy-version.json new file mode 100644 index 00000000..39afc297 --- /dev/null +++ b/.github/prompts/autonomy-version.json @@ -0,0 +1,3 @@ +{ + "version": "2025.11" +} diff --git a/.github/prompts/autonomy.manifest.json b/.github/prompts/autonomy.manifest.json new file mode 100644 index 00000000..25f7f880 --- /dev/null +++ b/.github/prompts/autonomy.manifest.json @@ -0,0 +1,8 @@ +{ + "version": "2025.11", + "consent": { + "phrase": "", + "expiresMinutes": 0 + }, + "actions": [] +} \ No newline at end of file diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index bb607980..f052ab8a 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -196,4 +196,11 @@ export class AuthController { const userAgent = request.headers['user-agent']; return this.authService.verifyInitialEmail(verifyEmailDto.token, ipAddress, userAgent); } + + @Post('email/resend') + resendVerification(@Body() data: { email: string }, @Req() request: Request) { + const ipAddress = request.ip || request.socket.remoteAddress; + const userAgent = request.headers['user-agent']; + return this.authService.resendEmailVerification(data.email, ipAddress, userAgent); + } } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index eb1ae0fd..9455aaea 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -49,14 +49,14 @@ import { UserRole } from '../types/prisma.types'; import { FraudService } from '../fraud/fraud.service'; type JwtPayload = { - sub: string; - email: string; - role: UserRole; - type: 'access' | 'refresh'; - jti: string; - family?: string; // Token rotation family ID - exp?: number; -}; + sub: string; + email: string; + role: UserRole; + type: 'access' | 'refresh'; + jti: string; + family?: string; + exp?: number; + }; @Injectable() export class AuthService { @@ -150,7 +150,7 @@ export class AuthService { }, }); - // Send verification email + // Send verification email and await result await this.emailService .sendEmail({ to: user.email, @@ -164,9 +164,12 @@ export class AuthService { this.logger.error('Failed to queue verification email:', err?.message || err); }); + // Only issue short-lived verification token after email is sent + // Full token pair is issued in verifyInitialEmail after verification succeeds return { user: sanitizeUser(user), message: 'Registration successful. Please check your email to verify your account.', + verificationToken, }; } @@ -1538,4 +1541,52 @@ export class AuthService { ...tokens, }; } + + async resendEmailVerification(email: string, ipAddress?: string, userAgent?: string) { + const user = await this.usersService.findByEmail(email); + if (!user) { + return; + } + + if (user.isBlocked) { + return; + } + + if (user.isVerified) { + throw new BadRequestException('Email already verified'); + } + + const verificationToken = randomToken(32); + const verificationExpiresAt = new Date( + Date.now() + + parseDuration( + this.configService.get('EMAIL_VERIFICATION_EXPIRES_IN') ?? '24h', + 24 * 60 * 60, + ) * + 1000, + ); + + await this.prisma.user.update({ + where: { id: user.id }, + data: { + emailVerificationToken: verificationToken, + emailVerificationExpires: verificationExpiresAt, + }, + }); + + await this.emailService + .sendEmail({ + to: user.email, + subject: 'Verify your email - PropChain', + template: 'email-verification', + context: { token: verificationToken }, + userId: user.id, + emailType: 'email_verification', + }) + .catch((err) => { + this.logger.error('Failed to queue verification email:', err?.message || err); + }); + + this.logger.log(`Verification email resent for user ${user.id}`); + } } diff --git a/src/auth/rate-limit.config.ts b/src/auth/rate-limit.config.ts index bd1b0e24..aa007bc2 100644 --- a/src/auth/rate-limit.config.ts +++ b/src/auth/rate-limit.config.ts @@ -47,6 +47,10 @@ export const ENDPOINT_RATE_LIMITS: Record