fix logs
This commit is contained in:
parent
5aed7d81ce
commit
6b9ad95811
@ -22,6 +22,7 @@ import { InvitationService } from '../services/invitation.service';
|
|||||||
import { InvitationsController } from '../controllers/invitations.controller';
|
import { InvitationsController } from '../controllers/invitations.controller';
|
||||||
import { EmailModule } from '../../infrastructure/email/email.module';
|
import { EmailModule } from '../../infrastructure/email/email.module';
|
||||||
import { SubscriptionsModule } from '../subscriptions/subscriptions.module';
|
import { SubscriptionsModule } from '../subscriptions/subscriptions.module';
|
||||||
|
import { AuditModule } from '../audit/audit.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -53,6 +54,9 @@ import { SubscriptionsModule } from '../subscriptions/subscriptions.module';
|
|||||||
|
|
||||||
// Subscriptions module for license checks
|
// Subscriptions module for license checks
|
||||||
SubscriptionsModule,
|
SubscriptionsModule,
|
||||||
|
|
||||||
|
// Audit module for login/logout tracking
|
||||||
|
AuditModule,
|
||||||
],
|
],
|
||||||
controllers: [AuthController, InvitationsController],
|
controllers: [AuthController, InvitationsController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@ -235,9 +235,11 @@ export class AuthService {
|
|||||||
* The revocation list lives in Redis with a TTL matching the token's
|
* The revocation list lives in Redis with a TTL matching the token's
|
||||||
* remaining lifetime, so entries clean themselves up.
|
* remaining lifetime, so entries clean themselves up.
|
||||||
*/
|
*/
|
||||||
async logout(refreshToken?: string): Promise<void> {
|
async logout(
|
||||||
|
refreshToken?: string
|
||||||
|
): Promise<{ userId: string; email: string; organizationId: string } | null> {
|
||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -248,9 +250,19 @@ export class AuthService {
|
|||||||
|
|
||||||
await this.cache.set(this.revokedTokenKey(refreshToken), true, remainingSeconds);
|
await this.cache.set(this.revokedTokenKey(refreshToken), true, remainingSeconds);
|
||||||
this.logger.log(`Refresh token revoked for user: ${payload?.email ?? 'unknown'}`);
|
this.logger.log(`Refresh token revoked for user: ${payload?.email ?? 'unknown'}`);
|
||||||
|
|
||||||
|
if (payload?.sub) {
|
||||||
|
return {
|
||||||
|
userId: payload.sub,
|
||||||
|
email: payload.email,
|
||||||
|
organizationId: payload.organizationId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Never block logout on revocation failures — log and continue
|
// Never block logout on revocation failures — log and continue
|
||||||
this.logger.error(`Failed to revoke refresh token: ${error}`);
|
this.logger.error(`Failed to revoke refresh token: ${error}`);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -34,6 +34,8 @@ import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
|||||||
import { UserRepository, USER_REPOSITORY } from '@domain/ports/out/user.repository';
|
import { UserRepository, USER_REPOSITORY } from '@domain/ports/out/user.repository';
|
||||||
import { UserMapper } from '../mappers/user.mapper';
|
import { UserMapper } from '../mappers/user.mapper';
|
||||||
import { InvitationService } from '../services/invitation.service';
|
import { InvitationService } from '../services/invitation.service';
|
||||||
|
import { AuditService } from '../services/audit.service';
|
||||||
|
import { AuditAction } from '@domain/entities/audit-log.entity';
|
||||||
import {
|
import {
|
||||||
AUTH_COOKIE_NAMES,
|
AUTH_COOKIE_NAMES,
|
||||||
authCookieOptions,
|
authCookieOptions,
|
||||||
@ -72,9 +74,26 @@ export class AuthController {
|
|||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
@Inject(USER_REPOSITORY) private readonly userRepository: UserRepository,
|
@Inject(USER_REPOSITORY) private readonly userRepository: UserRepository,
|
||||||
private readonly invitationService: InvitationService,
|
private readonly invitationService: InvitationService,
|
||||||
|
private readonly auditService: AuditService,
|
||||||
@Inject(EMAIL_PORT) private readonly emailService: EmailPort
|
@Inject(EMAIL_PORT) private readonly emailService: EmailPort
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the client IP and user agent from the request so we can record
|
||||||
|
* *who* connected and *from where* in the audit trail.
|
||||||
|
*/
|
||||||
|
private getClientInfo(req: Request): { ipAddress?: string; userAgent?: string } {
|
||||||
|
const forwardedFor = req.headers['x-forwarded-for'];
|
||||||
|
const ipAddress =
|
||||||
|
(Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor?.split(',')[0]?.trim()) ||
|
||||||
|
req.ip ||
|
||||||
|
req.socket?.remoteAddress;
|
||||||
|
return {
|
||||||
|
ipAddress,
|
||||||
|
userAgent: req.headers['user-agent'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deliver tokens as httpOnly cookies so they are out of reach of XSS.
|
* Deliver tokens as httpOnly cookies so they are out of reach of XSS.
|
||||||
* When rememberMe is false the cookies are session-scoped (cleared when
|
* When rememberMe is false the cookies are session-scoped (cleared when
|
||||||
@ -208,18 +227,59 @@ export class AuthController {
|
|||||||
})
|
})
|
||||||
async login(
|
async login(
|
||||||
@Body() dto: LoginDto,
|
@Body() dto: LoginDto,
|
||||||
|
@Req() req: Request,
|
||||||
@Res({ passthrough: true }) res: Response
|
@Res({ passthrough: true }) res: Response
|
||||||
): Promise<AuthResponseDto> {
|
): Promise<AuthResponseDto> {
|
||||||
const rememberMe = dto.rememberMe === true;
|
const rememberMe = dto.rememberMe === true;
|
||||||
const result = await this.authService.login(dto.email, dto.password, rememberMe);
|
const { ipAddress, userAgent } = this.getClientInfo(req);
|
||||||
|
|
||||||
this.setAuthCookies(res, result, rememberMe);
|
try {
|
||||||
|
const result = await this.authService.login(dto.email, dto.password, rememberMe);
|
||||||
|
|
||||||
return {
|
this.setAuthCookies(res, result, rememberMe);
|
||||||
accessToken: result.accessToken,
|
|
||||||
refreshToken: result.refreshToken,
|
// Audit log: record who logged in, when and from where
|
||||||
user: result.user,
|
await this.auditService.logSuccess(
|
||||||
};
|
AuditAction.USER_LOGIN,
|
||||||
|
result.user.id,
|
||||||
|
result.user.email,
|
||||||
|
result.user.organizationId,
|
||||||
|
{
|
||||||
|
resourceType: 'user',
|
||||||
|
resourceId: result.user.id,
|
||||||
|
ipAddress,
|
||||||
|
userAgent,
|
||||||
|
metadata: { rememberMe },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.log(`Login success: ${result.user.email} from ${ipAddress ?? 'unknown IP'}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: result.accessToken,
|
||||||
|
refreshToken: result.refreshToken,
|
||||||
|
user: result.user,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
// Audit log: record failed login attempts (the attempted email is the
|
||||||
|
// only identity we have — the credentials did not match a valid user)
|
||||||
|
await this.auditService.logFailure(
|
||||||
|
AuditAction.USER_LOGIN,
|
||||||
|
'unknown',
|
||||||
|
dto.email,
|
||||||
|
'unknown',
|
||||||
|
error?.message || 'Invalid credentials',
|
||||||
|
{
|
||||||
|
resourceType: 'user',
|
||||||
|
ipAddress,
|
||||||
|
userAgent,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.warn(`Login failed for ${dto.email} from ${ipAddress ?? 'unknown IP'}`);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -302,9 +362,22 @@ export class AuthController {
|
|||||||
): Promise<{ message: string }> {
|
): Promise<{ message: string }> {
|
||||||
const refreshToken = req.cookies?.[AUTH_COOKIE_NAMES.refreshToken];
|
const refreshToken = req.cookies?.[AUTH_COOKIE_NAMES.refreshToken];
|
||||||
|
|
||||||
await this.authService.logout(refreshToken);
|
const loggedOutUser = await this.authService.logout(refreshToken);
|
||||||
this.clearAuthCookies(res);
|
this.clearAuthCookies(res);
|
||||||
|
|
||||||
|
// Audit log: record who logged out and when
|
||||||
|
if (loggedOutUser) {
|
||||||
|
const { ipAddress, userAgent } = this.getClientInfo(req);
|
||||||
|
await this.auditService.logSuccess(
|
||||||
|
AuditAction.USER_LOGOUT,
|
||||||
|
loggedOutUser.userId,
|
||||||
|
loggedOutUser.email,
|
||||||
|
loggedOutUser.organizationId,
|
||||||
|
{ resourceType: 'user', resourceId: loggedOutUser.userId, ipAddress, userAgent }
|
||||||
|
);
|
||||||
|
this.logger.log(`Logout: ${loggedOutUser.email} from ${ipAddress ?? 'unknown IP'}`);
|
||||||
|
}
|
||||||
|
|
||||||
return { message: 'Logout successful' };
|
return { message: 'Logout successful' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user