@@ -7,7 +7,8 @@ import { config } from '../../config/index.js';
77import { OrganizationType } from '../../shared/constants/index.js' ;
88import { UserModel , OrganizationModel , UserRole } from '../users/users.model.js' ;
99import { blockToken , isTokenBlocked } from '../../infra/redis/tokenBlocklist.js' ;
10- import type { SignupInput , LoginInput , RegisterCompanyInput } from './auth.validation.js' ;
10+ import { createSession } from './session.service.js' ;
11+ import type { SignupInput , LoginInput } from './auth.validation.js' ;
1112import { logger } from '../../shared/logger/logger.js' ;
1213import { sendEmail , resetPasswordEmailHtml } from '../../services/email.service.js' ;
1314
@@ -23,22 +24,23 @@ export interface TokenPayload {
2324// SECURITY: [Token Lifecycle Compromise] — This prevents long-term token abuse by enforcing a 7-day Time-To-Live (TTL) limit on authentication tokens, bounding the window of opportunity for stolen credentials.
2425const TOKEN_TTL_SECONDS = 7 * 24 * 60 * 60 ; // 7 days
2526
26- function generateToken ( payload : Omit < TokenPayload , 'jti' > ) : string {
27+ function generateToken ( payload : Omit < TokenPayload , 'jti' > ) : { token : string ; jti : string } {
2728 // SECURITY: [Token Replay Attack] — This prevents reuse of old or intercepted JWTs by attaching a cryptographically random, unique JWT ID (jti) to each token, allowing the middleware to track and revoke individual sessions via Redis.
2829 const jti = randomUUID ( ) ;
29- return jwt . sign ( { ...payload , jti } , env . JWT_SECRET , { expiresIn : TOKEN_TTL_SECONDS } ) ;
30+ const token = jwt . sign ( { ...payload , jti } , env . JWT_SECRET , { expiresIn : TOKEN_TTL_SECONDS } ) ;
31+ return { token, jti } ;
3032}
3133
3234/**
3335 * Determines the safe default role for public signup.
34- *
36+ *
3537 * SECURITY: Always returns VIEWER role to prevent privilege escalation.
3638 * No exceptions or role overrides permitted for unauthenticated signup.
3739 * Admin/privileged roles are assigned exclusively through:
3840 * - POST /api/users (ADMIN only)
3941 * - POST /api/users/team (ADMIN only)
4042 * - Invitation acceptance flow (role determined by inviter)
41- *
43+ *
4244 * @param {string } _email - Email (unused - role is same for all users)
4345 * @returns {UserRole } Always returns VIEWER
4446 */
@@ -54,20 +56,26 @@ function derivePersona(role: string, organizationType?: OrganizationType): 'comp
5456 return 'company' ;
5557}
5658
59+ export interface RequestContext {
60+ ip ?: string ;
61+ userAgent ?: string ;
62+ }
63+
5764/**
5865 * Registers a new user and returns an auth token.
59- *
66+ *
6067 * SECURITY CONTROLS:
6168 * - Public signup ALWAYS assigns VIEWER role (determined via determineUserRole)
6269 * - No role parameter accepted in request body (enforced by SignupBodySchema)
6370 * - Admin/privileged roles only assignable via authenticated admin endpoints
6471 * - Prevents privilege escalation (CWE-284) by unauthenticated users
65- *
72+ *
6673 * @param {SignupInput } input - User signup input payload.
74+ * @param {RequestContext } [ctx] - Optional request context for session tracking.
6775 * @returns {Promise<{user: {id: string; email: string; name: string; role: string}; token: string}> } The created user and JWT token.
6876 * @throws {AppError } When the email is already in use.
6977 */
70- export async function signup ( input : SignupInput ) {
78+ export async function signup ( input : SignupInput , ctx ?: RequestContext ) {
7179 const existing = await UserModel . findOne ( { email : input . email } ) ;
7280 if ( existing ) {
7381 throw new AppError ( 409 , 'Email already in use' , 'EMAIL_TAKEN' ) ;
@@ -90,14 +98,16 @@ export async function signup(input: SignupInput) {
9098 organizationType = organization ?. type ;
9199 }
92100
93- const token = generateToken ( {
101+ const { token, jti } = generateToken ( {
94102 userId : user . _id . toString ( ) ,
95103 role : user . role as string ,
96104 persona : derivePersona ( user . role as string , organizationType ) ,
97105 organizationId : user . organizationId ?. toString ( ) ,
98106 organizationType,
99107 } ) ;
100108
109+ await createSession ( { userId : user . _id . toString ( ) , jti, ip : ctx ?. ip , userAgent : ctx ?. userAgent } ) ;
110+
101111 return {
102112 user : {
103113 id : user . _id ,
@@ -112,10 +122,11 @@ export async function signup(input: SignupInput) {
112122/**
113123 * Authenticates a user and returns a JWT.
114124 * @param {LoginInput } input - User login credentials.
125+ * @param {RequestContext } [ctx] - Optional request context for session tracking.
115126 * @returns {Promise<{user: {id: string; email: string; name: string; role: string}; token: string}> } Authenticated user data and token.
116127 * @throws {AppError } When credentials are invalid.
117128 */
118- export async function login ( input : LoginInput ) {
129+ export async function login ( input : LoginInput , ctx ?: RequestContext ) {
119130 const user = await UserModel . findOne ( { email : input . email } ) ;
120131 if ( ! user ) {
121132 throw new AppError ( 401 , 'Invalid credentials' , 'INVALID_CREDENTIALS' ) ;
@@ -132,14 +143,16 @@ export async function login(input: LoginInput) {
132143 organizationType = organization ?. type ;
133144 }
134145
135- const token = generateToken ( {
146+ const { token, jti } = generateToken ( {
136147 userId : user . _id . toString ( ) ,
137148 role : user . role as string ,
138149 persona : derivePersona ( user . role as string , organizationType ) ,
139150 organizationId : user . organizationId ?. toString ( ) ,
140151 organizationType,
141152 } ) ;
142153
154+ await createSession ( { userId : user . _id . toString ( ) , jti, ip : ctx ?. ip , userAgent : ctx ?. userAgent } ) ;
155+
143156 return {
144157 user : {
145158 id : user . _id ,
@@ -217,7 +230,7 @@ export async function refreshToken(token: string): Promise<{ token: string; expi
217230 organizationType,
218231 } ) ;
219232
220- return { token : newToken , expiresIn : TOKEN_TTL_SECONDS } ;
233+ return { token : newToken . token , expiresIn : TOKEN_TTL_SECONDS } ;
221234}
222235
223236/**
@@ -313,7 +326,7 @@ export async function resetPassword(token: string, newPassword: string): Promise
313326/**
314327 * Self-service company registration: creates both an Organization (type: ENTERPRISE)
315328 * and the first admin user in a single atomic operation.
316- *
329+ *
317330 * @param {RegisterCompanyInput } input - Company and admin user details.
318331 * @returns {Promise<{user: {id: string; email: string; name: string; role: string}; token: string}> } The created admin user and JWT token.
319332 * @throws {AppError } 409 if email or organization name already exists.
@@ -384,7 +397,7 @@ export async function registerCompany(input: {
384397 const createdUser = user [ 0 ] ;
385398
386399 // Generate JWT token with organization context
387- const token = generateToken ( {
400+ const { token } = generateToken ( {
388401 userId : createdUser . _id . toString ( ) ,
389402 role : UserRole . ADMIN ,
390403 persona : derivePersona ( UserRole . ADMIN , OrganizationType . ENTERPRISE ) ,
0 commit comments