Step8.4 Rbac And Session Control
✅ Step 8.4 — RBAC and Session Control
What you’ll have after this step
Auth-gateway configured with global guards:
- All routes require JWT by default
- Routes marked
@Public()bypass JWT - Routes marked
@Roles(...)additionally enforce role checks - Gateway session validation (tokens only work while the session exists) via
JwtSessionGuardandauth.session.validateRPC. - Public product endpoints: list / get by id / get by slug require no JWT.
- Per-tenant product DB (no
tenantIdin the product schema).
⚙️ 1) Auth-Service — add TCP auth.session.validate
apps/auth-service/src/auth-service.service.ts (add this validateSession method)
// ───────────────────────────────
// Validate a session
// ───────────────────────────────
async validateSession(req: any) {
try {
// ============================================================
// 1️⃣ Extract Token (from Header, Cookie, or Custom Header)
// ============================================================
const extractToken = () => {
const auth = req.headers?.authorization || req.headers?.Authorization;
if (auth && typeof auth === 'string') {
const [scheme, value] = auth.trim().split(/\s+/);
if (/^Bearer$/i.test(scheme) && value) return value;
if (!/\s/.test(auth)) return auth;
}
const xToken = req.headers?.['x-access-token'];
if (xToken && typeof xToken === 'string') return xToken.trim();
const cookies = req.cookies || {};
if (cookies.AccessToken) return String(cookies.AccessToken);
if (cookies.authorization) return String(cookies.authorization);
return null;
};
const token = extractToken();
if (!token) {
return apiResponse('Access token not provided.', null, {
status: 'error',
code: 'ACCESS_TOKEN_MISSING',
});
}
// ============================================================
// 2️⃣ Verify JWT Token
// ============================================================
let payload: any;
try {
payload = this.jwt.verify(token);
} catch (err) {
this.logger.warn(`⚠️ Invalid or expired access token`);
return apiResponse('Invalid or expired access token.', null, {
status: 'error',
code: 'INVALID_ACCESS_TOKEN',
});
}
const { sub: userId, tenantId, sid } = payload;
if (!userId || !tenantId) {
return apiResponse('Malformed token payload.', null, {
status: 'error',
code: 'MALFORMED_TOKEN',
details: { payload },
});
}
// ============================================================
// 3️⃣ Ensure Tenant Connection Exists
// ============================================================
const conn = req.tenantConnection;
if (!conn) {
this.logger.warn(`❌ Tenant connection missing (tenantId=${tenantId})`);
return apiResponse('Tenant environment not initialized.', null, {
status: 'error',
code: 'TENANT_CONNECTION_MISSING',
details: { tenantId },
});
}
// ============================================================
// 4️⃣ Find User and Validate Session
// ============================================================
const User = conn.model('User', UserSchema);
const user = await User.findById(userId).select('-password -sessions.refreshHash');
if (!user) {
this.logger.warn(`⚠️ User not found (userId=${userId})`);
return apiResponse('User not found.', null, {
status: 'error',
code: 'USER_NOT_FOUND',
});
}
if (sid) {
const activeSession = user.sessions?.find((s: any) => s.sessionId === sid);
if (!activeSession) {
this.logger.warn(`⚠️ Session not found or expired (sid=${sid})`);
return apiResponse('Session not found or expired.', null, {
status: 'error',
code: 'SESSION_NOT_FOUND',
sessionId: sid,
});
}
// Optionally check if session is marked "loggedOut" or "disabled"
if (activeSession.loggedOutAt || activeSession.status === 'LOGGED_OUT') {
this.logger.warn(`⚠️ Session already logged out (sid=${sid})`);
return apiResponse('Session already logged out.', null, {
status: 'error',
code: 'SESSION_LOGGED_OUT',
sessionId: sid,
});
}
}
// ============================================================
// 5️⃣ Construct Safe User Object
// ============================================================
const safeUser = {
id: user._id,
username: user.username,
name: user.name,
email: user.email,
mobile: user.mobile,
role: user.role,
sessionId: sid ?? null,
lastLoginAt: user.lastLoginAt,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
};
this.logger.log(`✅ Token validated (user=${userId}, tenant=${tenantId})`);
return apiResponse('Token validated successfully.', safeUser, {
status: 'success',
code: 'ACCESS_TOKEN_VALID',
});
} catch (err: any) {
this.logger.error('❌ Error validating access token', err.stack || err);
return apiResponse('Failed to validate access token.', null, {
status: 'error',
code: 'INTERNAL_ERROR',
error: err.message || 'Unknown error',
});
}
}
// ───────────────────────────────
// Get current user details from access token
// ───────────────────────────────
async getCurrentUserFromAccessToken(req: any) {
try {
// -------------------- 1. Extract access token --------------------
const token = this.extractAccessToken(req);
if (!token) {
return apiResponse(
'Access token not provided.',
null,
{ status: 'error', code: 'ACCESS_TOKEN_MISSING' },
);
}
// -------------------- 2. Verify token --------------------
let payload: any;
try {
payload = this.jwt.verify(token);
} catch (e) {
return apiResponse(
'Invalid or expired access token.',
null,
{ status: 'error', code: 'INVALID_ACCESS_TOKEN' },
);
}
const { sub: userId, tenantId } = payload;
if (!userId || !tenantId) {
return apiResponse(
'Invalid token payload: missing userId or tenantId.',
null,
{ status: 'error', code: 'MALFORMED_TOKEN' },
);
}
// -------------------- 3. Resolve tenant connection --------------------
const conn = req.tenantConnection;
if (!conn) {
return apiResponse(
'Tenant environment is not initialized.',
null,
{ status: 'error', code: 'TENANT_CONNECTION_MISSING', details: { tenantId } },
);
}
// -------------------- 4. Load user from DB --------------------
const User = conn.model('User', UserSchema);
const user = await User.findById(userId).select('-password -sessions.refreshHash');
if (!user) {
return apiResponse(
'User not found.',
null,
{ status: 'error', code: 'USER_NOT_FOUND', userId },
);
}
// Optional: if token includes a sessionId, validate it exists
if (payload.sid) {
const sessionExists = user.sessions?.some(
(s: any) => s.sessionId === payload.sid,
);
if (!sessionExists) {
return apiResponse(
'Session not found or expired.',
null,
{ status: 'error', code: 'SESSION_NOT_FOUND', sessionId: payload.sid },
);
}
}
// -------------------- 5. Prepare safe response object --------------------
const safeUser = {
id: user._id,
username: user.username,
name: user.name,
email: user.email,
mobile: user.mobile,
role: user.role,
sessionId: payload.sid ?? null,
lastLoginAt: user.lastLoginAt,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
};
return apiResponse(
'Current user retrieved successfully.',
safeUser,
{ status: 'success', code: 'CURRENT_USER_OK' },
);
} catch (err: any) {
this.logger.error('❌ Error in getCurrentUserFromAccessToken', err.stack || err);
return apiResponse(
'Failed to retrieve current user from token.',
null,
{ status: 'error', code: 'INTERNAL_ERROR', error: err.message },
);
}
}
// ───────────────────────────────
// Change User Role
// ───────────────────────────────
async changeUserRole(
req: any,
{ userId, newRole }: { userId: string; newRole: string },
) {
try {
const conn = req.tenantConnection;
if (!conn) {
this.logger.warn(
`❌ Tenant connection missing (tenantId=${req.tenantId})`,
);
return apiResponse(
'Role update failed because tenant environment is not initialized.',
null,
{
status: 'error',
code: 'TENANT_CONNECTION_MISSING',
details: { tenantId: req.tenantId },
},
);
}
const User = conn.model('User', UserSchema);
const user = await User.findById(userId);
if (!user) {
this.logger.warn(
`❌ Role change failed: user not found (userId=${userId}, tenantId=${req.tenantId})`,
);
return apiResponse(
'No user found with the provided identifier. Please verify and try again.',
null,
{ status: 'error', code: 'USER_NOT_FOUND', userId },
);
}
const prevRole = user.role;
user.role = newRole;
await user.save();
this.logger.log(
`✅ User ${user.email} role changed from ${prevRole} → ${newRole} (tenantId=${req.tenantId})`,
);
return apiResponse(
'User role has been updated successfully.',
{
id: user._id,
username: user.username,
email: user.email,
previousRole: prevRole,
newRole: user.role,
},
{ status: 'success', code: 'USER_ROLE_UPDATED' },
);
} catch (err: any) {
this.logger.error(
`❌ Unexpected error while changing user role (tenantId=${req.tenantId}, userId=${userId})`,
err?.stack || err,
);
return apiResponse(
'Role update failed due to a system error. Please try again later.',
null,
{
status: 'error',
code: 'INTERNAL_ERROR',
error: err.message || 'Unknown error',
},
);
}
}
// ───────────────────────────────
// HELPERS
// ───────────────────────────────
private maskEmail(email: string) {
const [name, domain] = email.split('@');
return name[0] + '***@' + domain;
}
private extractAccessToken(req: any): string | null {
const auth = req.headers?.authorization || req.headers?.Authorization;
if (auth && typeof auth === 'string') {
const [scheme, value] = auth.trim().split(/\s+/);
if (/^Bearer$/i.test(scheme) && value) return value;
if (!/\s/.test(auth)) return auth; // raw token (no "Bearer ")
}
const xToken = req.headers?.['x-access-token'];
if (xToken && typeof xToken === 'string') return xToken.trim();
const cookies = req.cookies || {};
if (cookies.AccessToken) return String(cookies.AccessToken);
if (cookies.authorization) return String(cookies.authorization);
return null;
}
apps/auth-service/src/auth-service.controller.ts
// ───────────────────────────────
// TCP: Validate Access Token
// ───────────────────────────────
@MessagePattern({ cmd: 'auth.session.validate' })
async validateAccessTokenTcp(
@Payload()
payload: {
req?: any;
tenantId: string;
ip?: string;
'user-agent'?: string;
token?: string;
},
) {
const conn = await this.databaseLibService.getTenantConnection(
payload.tenantId,
);
const req = {
tenantId: payload.tenantId,
tenantConnection: conn,
headers: {
authorization: payload.token
? `Bearer ${payload.token}`
: payload?.req?.headers?.authorization,
'x-tenant-id': payload.tenantId,
'user-agent': payload['user-agent'] || 'tcp-client',
},
cookies: payload?.req?.cookies || {},
ip: payload.ip || '0.0.0.0',
};
const result = await this.service.validateSession(req);
return result;
}
// ───────────────────────────────
// TCP: Get current user details
// ───────────────────────────────
@MessagePattern({ cmd: 'auth.get-current-user' })
async getCurrentUserTcp(
@Payload()
payload: {
token: string;
tenantId: string;
'user-agent'?: string;
ip?: string;
},
) {
const conn = await this.databaseLibService.getTenantConnection(
payload.tenantId,
);
const fakeReq = {
tenantId: payload.tenantId,
tenantConnection: conn,
headers: {
authorization: `Bearer ${payload.token}`,
'user-agent': payload['user-agent'] || 'tcp-client',
},
ip: payload.ip || '0.0.0.0',
};
const result = await this.service.getCurrentUserFromAccessToken(fakeReq);
return result;
}
// ───────────────────────────────
// TCP: Change User Role
// ───────────────────────────────
@MessagePattern({ cmd: 'auth.changeUserRole' })
async changeUserRoleTcp(
@Payload() payload: { tenantId: string; userId: string; newRole: string },
) {
const conn = await this.databaseLibService.getTenantConnection(
payload.tenantId,
);
const req = {
tenantId: payload.tenantId,
tenantConnection: conn,
headers: { 'user-agent': payload['user-agent'] || 'tcp-client' },
ip: '0.0.0.0',
};
const result = await this.service.changeUserRole(req, {
userId: payload.userId,
newRole: payload.newRole,
});
return result;
}
⚙️ 2) JwtSessionGuard
We’ll add JwtSessionGuard here and wire it in the gateway. Place this into your libs/auth-lib package and export it from the lib’s index.ts.
libs/auth-lib/src/guards/jwt-session.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
Inject,
Logger,
} from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { firstValueFrom } from 'rxjs';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
@Injectable()
export class JwtSessionGuard implements CanActivate {
private readonly logger = new Logger(JwtSessionGuard.name);
constructor(
private readonly reflector: Reflector,
@Inject('AUTH_SERVICE') private readonly authClient: ClientProxy,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
if (context.getType() !== 'http') return true;
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
const req = context.switchToHttp().getRequest();
const payload = req.user;
const authHeader = req.headers?.authorization || req.headers?.Authorization;
let token: string | null = null;
if (authHeader && typeof authHeader === 'string') {
const [scheme, value] = authHeader.trim().split(/\s+/);
if (/^Bearer$/i.test(scheme) && value) token = value;
else if (!/\s/.test(authHeader)) token = authHeader; // raw token (no "Bearer ")
}
if (!payload) throw new UnauthorizedException('Missing JWT payload');
const tenantId = req.headers['x-tenant-id'] as string;
const { sub: userId, sid, username } = payload || {};
if (!tenantId || !userId || !sid)
throw new UnauthorizedException('Invalid or malformed token payload');
// 🔐 Validate live session via auth-service microservice
try {
const result = await firstValueFrom(
this.authClient.send(
{ cmd: 'auth.session.validate' },
{ token, tenantId, userId, sid },
),
);
if (
!result ||
result?.meta?.code !== 'ACCESS_TOKEN_VALID' ||
(result.data.id !== userId && result.data.sessionId !== sid)
) {
this.logger.warn(
`Session expired or revoked (user=${userId}, sid=${sid})`,
);
throw new UnauthorizedException('Session expired or revoked');
}
// Attach normalized actor info for downstream services
req.actor = { id: userId, username: username ?? result.username };
return true;
} catch (err) {
this.logger.error(`❌ Session validation failed`, err);
throw new UnauthorizedException('Session validation failed');
}
}
}
libs/auth-lib/src/index.ts (ensure exports)
export * from './guards/jwt-session.guard';
⚙️ 3) API-Gateway — register global guards & ensure AUTH_SERVICE client
Order matters: JwtAuthGuard → JwtSessionGuard → RolesGuard.
apps/api-gateway/src/api-gateway.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { ApiGatewayController } from './api-gateway.controller';
import { ApiGatewayService } from './api-gateway.service';
import { TenantGatewayController } from './tenant-gateway.controller';
import {
JwtStrategy,
JwtAuthGuard,
RolesGuard,
JwtSessionGuard,
} from '@app/auth-lib';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { APP_GUARD } from '@nestjs/core';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.registerAsync({
inject: [ConfigService],
useFactory: (cfg: ConfigService) => {
const expiresIn = cfg.get<string>('JWT_EXPIRES_IN', '15m');
return {
secret: cfg.get<string>('JWT_SECRET'),
signOptions: { expiresIn: expiresIn as any },
};
},
}),
ClientsModule.registerAsync([
{
name: 'AUTH_SERVICE',
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (cfg: ConfigService) => ({
transport: Transport.TCP,
options: {
host: '0.0.0.0',
port: Number(cfg.get('AUTH_SERVICE_TCP_PORT') || 4502),
},
}),
},
{
name: 'TENANT_SERVICE',
inject: [ConfigService],
useFactory: (cfg: ConfigService) => ({
transport: Transport.TCP,
options: {
host: '0.0.0.0',
port: cfg.get<number>('TENANT_SERVICE_TCP_PORT', 4503),
},
}),
},
{
name: 'USER_SERVICE',
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (cfg: ConfigService) => ({
transport: Transport.TCP,
options: {
host: '0.0.0.0',
port: Number(cfg.get('USER_SERVICE_TCP_PORT') || 4504),
},
}),
},
{
name: 'PRODUCT_SERVICE',
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
transport: Transport.TCP,
options: {
host: '0.0.0.0',
port: Number(configService.get('PRODUCT_SERVICE_TCP_PORT') || 4505),
},
}),
},
]),
],
controllers: [ApiGatewayController, TenantGatewayController],
providers: [
ApiGatewayService,
JwtStrategy,
// Global guards: order matters (JWT first, then Session, then Roles)
{ provide: APP_GUARD, useClass: JwtAuthGuard },
{ provide: APP_GUARD, useClass: JwtSessionGuard },
{ provide: APP_GUARD, useClass: RolesGuard },
],
})
export class ApiGatewayModule {}
Using APP_GUARD means you don’t need to put @UseGuards(JwtAuthGuard, RolesGuard) on every controller. Just add @Public() where you want a route open, and @Roles(...) where you want role checks.
⚙️ 4) Auth-Gateway-Controller — full file (RBAC + Public + actor)
We’ll register global guards so every controller benefits automatically:
JwtAuthGuard(global): protects all routes unless@Public()RolesGuard(global): checks roles when@Roles()is present
apps/api-gateway/src/auth-gateway.controller.ts
// apps/api-gateway/src/controllers/auth-gateway.controller.ts
import {
BadRequestException,
Body,
Controller,
Get,
Headers,
Inject,
Post,
Query,
Req,
UseGuards,
} from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { firstValueFrom, timeout, catchError, throwError } from 'rxjs';
import { SignupDto } from '../../auth-service/src/dto/signup.dto';
import { LoginDto } from '../../auth-service/src/dto/login.dto';
import { VerifyOtpDto } from '../../auth-service/src/dto/verify-otp.dto';
import { JwtAuthGuard, JwtSessionGuard, Public, Roles } from '@app/auth-lib';
import { LogoutSessionDto } from '../../auth-service/src/dto/logout-session.dto';
import { RefreshDto } from '../../auth-service/src/dto/refresh.dto';
@Controller('gateway/auth')
export class AuthGatewayController {
constructor(
@Inject('AUTH_SERVICE') private readonly authClient: ClientProxy,
) {}
private async sendSafe<T>(cmd: string, payload: any): Promise<T> {
try {
return await firstValueFrom(
this.authClient.send<T>({ cmd }, payload).pipe(
timeout(10000),
catchError((error) => {
const message =
error?.message ||
error?.response?.message ||
'Auth service error';
const errors = error?.response?.errors;
return throwError(
() => new BadRequestException({ message, errors }),
);
}),
),
);
} catch (unexpected: any) {
throw new BadRequestException(
unexpected?.response || {
message: unexpected?.message || 'Auth service error',
},
);
}
}
// ───────────────────────────────
// Signup
// ───────────────────────────────
@Public()
@Post('signup')
async signup(
@Headers('x-tenant-id') tenantId: string,
@Body() dto: SignupDto,
) {
if (!tenantId) throw new BadRequestException('x-tenant-id is required');
const result = await this.sendSafe<any>('auth.signup', {
...dto,
tenantId,
});
return result;
}
// ───────────────────────────────
// Login (send OTP)
// ───────────────────────────────
@Public()
@Post('login')
async login(@Headers('x-tenant-id') tenantId: string, @Body() dto: LoginDto) {
if (!tenantId) throw new BadRequestException('x-tenant-id is required');
const result = await this.sendSafe<any>('auth.login', { ...dto, tenantId });
return result;
}
// ───────────────────────────────
// Verify OTP → Tokens
// ───────────────────────────────
@Public()
@Post('login/verify')
async verifyOtp(
@Headers('x-tenant-id') tenantId: string,
@Body() dto: VerifyOtpDto,
) {
if (!tenantId) throw new BadRequestException('x-tenant-id is required');
const result = await this.sendSafe<any>('auth.verifyOtp', {
...dto,
tenantId,
});
return result;
}
// ───────────────────────────────
// Unlock account
// ───────────────────────────────
@Public()
@Get('unlock')
async unlock(
@Headers('x-tenant-id') tenantId: string,
@Query('token') token: string,
) {
if (!tenantId) throw new BadRequestException('x-tenant-id is required');
const result = await this.sendSafe<any>('auth.unlock', { tenantId, token });
return result;
}
// ───────────────────────────────
// List sessions
// ───────────────────────────────
@UseGuards(JwtAuthGuard, JwtSessionGuard)
@Get('sessions')
async sessions(@Headers('x-tenant-id') tenantId: string, @Req() req: any) {
if (!tenantId) throw new BadRequestException('x-tenant-id is required');
const safeReq = {
user: req.user,
headers: { 'user-agent': req.headers['user-agent'] || 'unknown' },
ip: req.ip || req.connection?.remoteAddress || '0.0.0.0',
};
const result = await this.sendSafe<any>('auth.sessions', {
tenantId,
req: safeReq,
});
return result;
}
// ───────────────────────────────
// Logout single session
// ───────────────────────────────
@UseGuards(JwtAuthGuard, JwtSessionGuard)
@Post('logout/session')
async logoutSession(
@Headers('x-tenant-id') tenantId: string,
@Req() req: any,
@Body() dto: LogoutSessionDto,
) {
if (!tenantId) throw new BadRequestException('x-tenant-id is required');
const safeReq = {
user: req.user,
headers: { 'user-agent': req.headers['user-agent'] },
ip: req.ip || req.connection?.remoteAddress || '0.0.0.0',
};
const result = await this.sendSafe<any>('auth.logoutSession', {
tenantId,
req: safeReq,
sessionId: dto.sessionId,
});
return result;
}
// ───────────────────────────────
// Logout all sessions
// ───────────────────────────────
@UseGuards(JwtAuthGuard, JwtSessionGuard)
@Post('logout/all')
async logoutAll(@Headers('x-tenant-id') tenantId: string, @Req() req: any) {
if (!tenantId) throw new BadRequestException('x-tenant-id is required');
const safeReq = {
user: req.user,
headers: { 'user-agent': req.headers['user-agent'] },
ip: req.ip || req.connection?.remoteAddress || '0.0.0.0',
};
const result = await this.sendSafe<any>('auth.logoutAll', {
tenantId,
req: safeReq,
});
return result;
}
// ───────────────────────────────
// Refresh tokens
// ───────────────────────────────
@UseGuards(JwtAuthGuard, JwtSessionGuard)
@Post('refresh')
async refresh(
@Headers('x-tenant-id') tenantId: string,
@Req() req: any,
@Body() dto: RefreshDto,
) {
if (!tenantId) throw new BadRequestException('x-tenant-id is required');
const safeReq = {
user: req.user,
headers: { 'user-agent': req.headers['user-agent'] },
ip: req.ip || req.connection?.remoteAddress || '0.0.0.0',
};
const result = await this.sendSafe<any>('auth.refresh', {
tenantId,
req: safeReq,
refreshToken: dto.refreshToken,
});
return result;
}
// ───────────────────────────────
// Change role
// ───────────────────────────────
@Roles('admin')
@Post('change-role')
async changeUserRole(
@Headers('x-tenant-id') tenantId: string,
@Body('userId') userId: string,
@Body('newRole') newRole: string,
) {
if (!tenantId) throw new BadRequestException('x-tenant-id is required');
const result = await this.sendSafe<any>('auth.changeUserRole', {
tenantId,
userId,
newRole,
});
return result;
}
// ───────────────────────────────
// Get Current User (from Access Token)
// ───────────────────────────────
@UseGuards(JwtAuthGuard)
@Get('me')
async getCurrentUser(
@Headers('x-tenant-id') tenantId: string,
@Req() req: any,
) {
if (!tenantId) throw new BadRequestException('x-tenant-id is required');
const accessToken =
req.headers['authorization'] || req.headers['Authorization'];
if (!accessToken) {
throw new BadRequestException('Authorization header is required');
}
const token = accessToken.replace(/^Bearer\s+/i, '');
const result = await this.sendSafe<any>('auth.get-current-user', {
tenantId,
token,
'user-agent': req.headers['user-agent'],
ip: req.ip || req.connection?.remoteAddress || '0.0.0.0',
});
return result;
}
}
Update api-gateway.module to include all api endpoints from AuthGatewayController.
apps/api-gateway/src/api-gateway.module.ts
controllers: [
ApiGatewayController,
AuthGatewayController,
TenantGatewayController,
],
⚙️ 5) cURL Tests
a) Signup
curl -X POST http://localhost:3501/gateway/auth/signup \
-H "Content-Type: application/json" \
-H "x-tenant-id: darmist1" \
-d '{
"name": "Test User",
"username": "testuser",
"email": "testuser@darmist.com",
"mobile": "01710000000",
"password": "secret123"
}' | jq
Expected (201):
{
"message": "Signup successful. Please verify your account if required.",
"data": {
"id": "690c30f4b16e95915801e4e5",
"username": "testuser",
"email": "testuser@darmist.com",
"role": "user"
},
"meta": {
"status": "success",
"code": "SIGNUP_SUCCESS"
},
"ts": "2025-11-06T05:20:12.814Z"
}
b) Request OTP (Login)
curl -X POST http://localhost:3501/gateway/auth/login \
-H "Content-Type: application/json" \
-H "x-tenant-id: darmist1" \
-d '{"usernameOrEmailOrMobile":"testuser","password":"secret123"}' | jq
Expected (200):
{
"message": "A verification OTP has been sent to your registered email address. Please check your inbox.",
"data": {
"loginId": "c1e0d2ba-237e-47e1-b18b-d86a092c4058",
"channel": "email",
"maskedEmail": "t***@darmist.com"
},
"meta": {
"status": "success",
"code": "OTP_SENT"
},
"ts": "2025-11-06T05:24:58.249Z"
}
c) Verify OTP → Get Tokens
curl -X POST http://localhost:3501/gateway/auth/login/verify \
-H "Content-Type: application/json" \
-H "x-tenant-id: darmist1" \
-d '{
"loginId": "c1e0d2ba-237e-47e1-b18b-d86a092c4058",
"otp": "903335"
}' | jq
Expected (200):
{
"message": "You have successfully logged in to DARMIST Lab.",
"data": {
"accessToken": "<ACCESS_TOKEN>",
"refreshToken": "<REFRESH_TOKEN>",
"sessionId": "e8c7fa5f-166b-4de2-b2e4-2d290c57cf58",
"user": {
"id": "690c30f4b16e95915801e4e5",
"username": "testuser",
"role": "user"
}
},
"meta": {
"status": "success",
"code": "LOGIN_SUCCESS"
},
"ts": "2025-11-06T05:26:25.829Z"
}
d) Get Current User
curl http://localhost:3501/gateway/auth/me \
-H "Content-Type: application/json" \
-H "x-tenant-id: darmist1" \
-H "Authorization: Bearer <ACCESS_TOKEN>" | jq
Expected (200):
{
"message": "Authenticated user retrieved successfully.",
"data": {
"id": "690c30f4b16e95915801e4e5",
"username": "testuser",
"email": "testuser@darmist.com",
"role": "user",
"sessionId": "e8c7fa5f-166b-4de2-b2e4-2d290c57cf58"
},
"meta": {
"status": "success",
"code": "USER_INFO"
},
"ts": "2025-11-06T05:35:22.890Z"
}
e) List Active Sessions
curl http://localhost:3501/gateway/auth/sessions \
-H "x-tenant-id: darmist1" \
-H "Authorization: Bearer <ACCESS_TOKEN>" | jq
Expected (200):
{
"message": "Active sessions retrieved successfully.",
"data": [
{
"sessionId": "e8c7fa5f-166b-4de2-b2e4-2d290c57cf58",
"device": "Chrome on macOS",
"ip": "127.0.0.1",
"current": true,
"lastActive": "2025-11-06T05:35:10.002Z"
}
],
"meta": {
"status": "success",
"code": "SESSIONS_LISTED"
},
"ts": "2025-11-06T05:37:48.444Z"
}
f) Logout a Single Session
curl -X POST http://localhost:3501/gateway/auth/logout/session \
-H "Content-Type: application/json" \
-H "x-tenant-id: darmist1" \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-d '{"sessionId":"e8c7fa5f-166b-4de2-b2e4-2d290c57cf58"}' | jq
Expected (200):
{
"message": "The selected session has been successfully logged out.",
"meta": {
"status": "success",
"code": "LOGOUT_SESSION_SUCCESS"
},
"ts": "2025-11-06T05:39:12.771Z"
}
g) Logout All Sessions
curl -X POST http://localhost:3501/gateway/auth/logout/all \
-H "x-tenant-id: darmist1" \
-H "Authorization: Bearer <ACCESS_TOKEN>" | jq
Expected (200):
{
"message": "All sessions have been successfully terminated.",
"meta": {
"status": "success",
"code": "LOGOUT_ALL_SUCCESS"
},
"ts": "2025-11-06T05:40:05.621Z"
}
🔒 After this, retrying /gateway/auth/me should yield:
{
"message": "Session expired or revoked.",
"meta": {
"status": "error",
"code": "SESSION_EXPIRED"
}
}
h) Refresh Tokens
curl -X POST http://localhost:3501/gateway/auth/refresh \
-H "Content-Type: application/json" \
-H "x-tenant-id: darmist1" \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-d '{"refreshToken":"<REFRESH_TOKEN>"}' | jq
Expected (200):
{
"message": "Access token refreshed successfully.",
"data": {
"accessToken": "<NEW_ACCESS_TOKEN>",
"refreshToken": "<NEW_REFRESH_TOKEN>"
},
"meta": {
"status": "success",
"code": "TOKEN_REFRESHED"
},
"ts": "2025-11-06T05:42:11.508Z"
}
i) 🔴 Change User Role (Admin Only)
curl -X POST http://localhost:3501/gateway/auth/change-role \
-H "Content-Type: application/json" \
-H "x-tenant-id: darmist1" \
-H "Authorization: Bearer <ADMIN_ACCESS_TOKEN>" \
-d '{
"userId": "690c30f4b16e95915801e4e5",
"newRole": "admin"
}' | jq
Expected (200):
{
"message": "User role has been updated successfully.",
"data": {
"id": "690c30f4b16e95915801e4e5",
"username": "testuser",
"email": "testuser@darmist.com",
"previousRole": "user",
"newRole": "admin"
},
"meta": {
"status": "success",
"code": "USER_ROLE_UPDATED"
},
"ts": "2025-11-06T11:33:24.431Z"
}
Expected (403) if non-admin:
{
"message": "Forbidden resource. Admin privileges required.",
"meta": {
"status": "error",
"code": "FORBIDDEN"
},
"ts": "2025-11-06T05:46:11.233Z"
}