77import { JwtService } from '@nestjs/jwt' ;
88import { ConfigService } from '@nestjs/config' ;
99import * as bcrypt from 'bcrypt' ;
10+ import * as speakeasy from 'speakeasy' ;
11+ import * as qrcode from 'qrcode' ;
1012import { UsersService } from '../users/users.service' ;
1113import { RegisterDto } from './dto/register.dto' ;
1214import { LoginDto } from './dto/login.dto' ;
@@ -49,7 +51,12 @@ export class AuthService {
4951 return { user, tokens } ;
5052 }
5153
52- async login ( dto : LoginDto ) : Promise < { user : User ; tokens : AuthTokens } > {
54+ async login (
55+ dto : LoginDto ,
56+ ) : Promise <
57+ | { user : User ; tokens : AuthTokens }
58+ | { requiresTwoFactor : true ; tempToken : string }
59+ > {
5360 const user = await this . usersService . findByEmail ( dto . email . toLowerCase ( ) ) ;
5461 if ( ! user ) {
5562 throw new UnauthorizedException ( 'Invalid email or password' ) ;
@@ -60,6 +67,17 @@ export class AuthService {
6067 throw new UnauthorizedException ( 'Invalid email or password' ) ;
6168 }
6269
70+ if ( user . twoFactorEnabled ) {
71+ const tempToken = await this . jwtService . signAsync (
72+ { sub : user . id , twoFactorPending : true } ,
73+ {
74+ secret : this . configService . get < string > ( 'JWT_SECRET' , 'change-me-in-env' ) ,
75+ expiresIn : '5m' ,
76+ } ,
77+ ) ;
78+ return { requiresTwoFactor : true , tempToken } ;
79+ }
80+
6381 const tokens = await this . signTokens ( user ) ;
6482 await this . storeRefreshToken ( user . id , tokens . refreshToken ) ;
6583
@@ -136,4 +154,84 @@ export class AuthService {
136154 const hashed = await bcrypt . hash ( token , 10 ) ;
137155 await this . usersService . updateRefreshToken ( userId , hashed ) ;
138156 }
157+
158+ // ── 2FA ──────────────────────────────────────────────────────────
159+
160+ async twoFactorSetup ( userId : string ) : Promise < { otpauthUrl : string ; qrCodeDataUrl : string } > {
161+ const user = await this . usersService . findById ( userId ) ;
162+ const secret = speakeasy . generateSecret ( {
163+ name : `ManageAssets (${ user . email } )` ,
164+ } ) ;
165+
166+ await this . usersService . updateTwoFactor ( userId , secret . base32 , false ) ;
167+
168+ const qrCodeDataUrl = await qrcode . toDataURL ( secret . otpauth_url ! ) ;
169+ return { otpauthUrl : secret . otpauth_url ! , qrCodeDataUrl } ;
170+ }
171+
172+ async twoFactorEnable ( userId : string , code : string ) : Promise < void > {
173+ const user = await this . usersService . findByIdWithTwoFactor ( userId ) ;
174+ if ( ! user ?. twoFactorSecret ) {
175+ throw new BadRequestException ( '2FA setup not initiated. Call /auth/2fa/setup first.' ) ;
176+ }
177+
178+ const valid = speakeasy . totp . verify ( {
179+ secret : user . twoFactorSecret ,
180+ encoding : 'base32' ,
181+ token : code ,
182+ window : 1 ,
183+ } ) ;
184+ if ( ! valid ) throw new UnauthorizedException ( 'Invalid TOTP code' ) ;
185+
186+ await this . usersService . updateTwoFactor ( userId , user . twoFactorSecret , true ) ;
187+ }
188+
189+ async twoFactorDisable ( userId : string , code : string ) : Promise < void > {
190+ const user = await this . usersService . findByIdWithTwoFactor ( userId ) ;
191+ if ( ! user ?. twoFactorSecret ) {
192+ throw new BadRequestException ( '2FA is not set up for this account.' ) ;
193+ }
194+
195+ const valid = speakeasy . totp . verify ( {
196+ secret : user . twoFactorSecret ,
197+ encoding : 'base32' ,
198+ token : code ,
199+ window : 1 ,
200+ } ) ;
201+ if ( ! valid ) throw new UnauthorizedException ( 'Invalid TOTP code' ) ;
202+
203+ await this . usersService . updateTwoFactor ( userId , null , false ) ;
204+ }
205+
206+ async twoFactorVerify ( tempToken : string , code : string ) : Promise < AuthTokens > {
207+ let payload : { sub : string ; twoFactorPending ?: boolean } ;
208+ try {
209+ payload = await this . jwtService . verifyAsync ( tempToken , {
210+ secret : this . configService . get < string > ( 'JWT_SECRET' , 'change-me-in-env' ) ,
211+ } ) ;
212+ } catch {
213+ throw new UnauthorizedException ( 'Invalid or expired temp token' ) ;
214+ }
215+
216+ if ( ! payload . twoFactorPending ) {
217+ throw new UnauthorizedException ( 'Token is not a 2FA pending token' ) ;
218+ }
219+
220+ const user = await this . usersService . findByIdWithTwoFactor ( payload . sub ) ;
221+ if ( ! user ?. twoFactorSecret ) {
222+ throw new UnauthorizedException ( '2FA not configured' ) ;
223+ }
224+
225+ const valid = speakeasy . totp . verify ( {
226+ secret : user . twoFactorSecret ,
227+ encoding : 'base32' ,
228+ token : code ,
229+ window : 1 ,
230+ } ) ;
231+ if ( ! valid ) throw new UnauthorizedException ( 'Invalid TOTP code' ) ;
232+
233+ const tokens = await this . signTokens ( user ) ;
234+ await this . storeRefreshToken ( user . id , tokens . refreshToken ) ;
235+ return tokens ;
236+ }
139237}
0 commit comments