diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index ba5837d..8e3494a 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -1,4 +1,5 @@ -import { Controller, Post, Body, Headers, Logger, HttpException } from '@nestjs/common'; +import { Controller, Post, Body, Headers, Req, Logger, HttpException } from '@nestjs/common'; +import type { Request } from 'express'; import { ConfigService } from '@nestjs/config'; import axios from 'axios'; import { OzonetelAgentService } from '../ozonetel/ozonetel-agent.service'; @@ -24,7 +25,7 @@ export class AuthController { } @Post('login') - async login(@Body() body: { email: string; password: string }) { + async login(@Body() body: { email: string; password: string }, @Req() req: Request) { this.logger.log(`Login attempt for ${body.email}`); try { @@ -128,13 +129,15 @@ export class AuthController { } // Check for duplicate login — strict: one device only - const existingSession = await this.sessionService.isSessionLocked(agentConfig.ozonetelAgentId); + const clientIp = (req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() ?? req.ip ?? 'unknown'; + const existingSession = await this.sessionService.getSession(agentConfig.ozonetelAgentId); if (existingSession) { - throw new HttpException('You are already logged in on another device. Please log out there first.', 409); + this.logger.warn(`Duplicate login blocked for ${body.email} — session held by IP ${existingSession.ip} since ${existingSession.lockedAt}`); + throw new HttpException(`You are already logged in from another device (${existingSession.ip}). Please log out there first.`, 409); } - // Lock session in Redis - await this.sessionService.lockSession(agentConfig.ozonetelAgentId, memberId); + // Lock session in Redis with IP + await this.sessionService.lockSession(agentConfig.ozonetelAgentId, memberId, clientIp); // Login to Ozonetel with agent-specific credentials const ozAgentPassword = process.env.OZONETEL_AGENT_PASSWORD ?? 'Test123$'; diff --git a/src/auth/session.service.ts b/src/auth/session.service.ts index f1aa198..b695002 100644 --- a/src/auth/session.service.ts +++ b/src/auth/session.service.ts @@ -22,12 +22,25 @@ export class SessionService implements OnModuleInit { return `agent:session:${agentId}`; } - async lockSession(agentId: string, memberId: string): Promise { - await this.redis.set(this.key(agentId), memberId, 'EX', SESSION_TTL); + async lockSession(agentId: string, memberId: string, ip?: string): Promise { + const value = JSON.stringify({ memberId, ip: ip ?? 'unknown', lockedAt: new Date().toISOString() }); + await this.redis.set(this.key(agentId), value, 'EX', SESSION_TTL); + } + + async getSession(agentId: string): Promise<{ memberId: string; ip: string; lockedAt: string } | null> { + const raw = await this.redis.get(this.key(agentId)); + if (!raw) return null; + try { + return JSON.parse(raw); + } catch { + // Legacy format — just memberId string + return { memberId: raw, ip: 'unknown', lockedAt: 'unknown' }; + } } async isSessionLocked(agentId: string): Promise { - return this.redis.get(this.key(agentId)); + const session = await this.getSession(agentId); + return session ? session.memberId : null; } async refreshSession(agentId: string): Promise {