import { Injectable, BadRequestException, InternalServerErrorException, NotFoundException, } from '@nestjs/common'; import { TraceLogger } from '../common/trace-logger'; import { ConfigService } from '@nestjs/config'; import { chromium } from 'playwright'; import * as fs from 'fs'; import * as path from 'path'; import * as crypto from 'crypto'; import { SessionService } from '../session/session.service'; import { EnvironmentService } from '../environment/environment.service'; interface KeyDescriptor { keyFile?: string; login?: string; password: string; } @Injectable() export class AuthService { private readonly logger = new TraceLogger(AuthService.name); private readonly keysDir: string; constructor( private readonly config: ConfigService, private readonly sessionService: SessionService, private readonly environmentService: EnvironmentService, ) { this.keysDir = path.resolve( this.config.get('KEYS_DIR', 'keys'), ); } listKeys(): string[] { if (!fs.existsSync(this.keysDir)) return []; return fs.readdirSync(this.keysDir) .filter(f => f.endsWith('.json')) .map(f => path.basename(f, '.json')); } async login(keyId: string, environmentName: string, sessionName?: string): Promise<{ token: string; sessionName: string }> { const resolvedSession = sessionName ?? crypto.randomUUID(); const env = await this.environmentService.findAll().then(({ data }) => data.find(e => e.name === environmentName)); if (!env) throw new NotFoundException(`Environment "${environmentName}" not found`); const loginUrl = env.urls.id_url; const cabinetUrl = env.urls.cabinet_url; if (!loginUrl) throw new BadRequestException(`Environment "${environmentName}" is missing id_url`); if (!cabinetUrl) throw new BadRequestException(`Environment "${environmentName}" is missing cabinet_url`); const keyJsonPath = path.join(this.keysDir, `${keyId}.json`); if (!fs.existsSync(keyJsonPath)) { throw new BadRequestException(`Key not found: ${keyId}`); } let descriptor: KeyDescriptor; try { descriptor = JSON.parse(fs.readFileSync(keyJsonPath, 'utf-8')); } catch (err) { throw new BadRequestException(`Cannot read key descriptor: ${keyId}`, { cause: err }); } const useLoginPassword = !!descriptor.login; if (!useLoginPassword) { if (!descriptor.keyFile) { throw new BadRequestException(`Key descriptor for "${keyId}" must have either "login" or "keyFile"`); } const keyFilePath = path.resolve(this.keysDir, descriptor.keyFile); if (!fs.existsSync(keyFilePath)) { throw new BadRequestException(`Key file not found: ${descriptor.keyFile}`); } } const browser = await chromium.launch({ headless: true, executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH }); try { const context = await browser.newContext(); const page = await context.newPage(); this.logger.log(`Navigating to ${loginUrl}`); await page.goto(loginUrl, { waitUntil: 'networkidle' }); if (useLoginPassword) { // Click "Логін і пароль" auth method await page.locator('p[aria-label="Логін і пароль"]').click(); // Fill login and password await page.getByLabel('Електронна пошта').fill(descriptor.login!); await page.getByLabel('Пароль').fill(descriptor.password); // Click "Увійти" await page.locator('button:has-text("Увійти")').click(); } else { const keyFilePath = path.resolve(this.keysDir, descriptor.keyFile!); // Click "Файловий ключ" button await page.getByText('Файловий ключ').click(); // Upload key file via hidden file input const fileInput = page.locator('input[accept=".dat,.pfx,.pk8,.zs2,.jks"][type="file"]'); await fileInput.setInputFiles(keyFilePath); // Enter password await page .locator('#id-app-login-file-key-password') .fill(descriptor.password); // Click "Продовжити" await page.locator('#id-app-login-file-key-sign-button').click(); } // Wait until redirected to cabinet this.logger.log(`Waiting for redirect to ${cabinetUrl}`); await page.waitForURL(cabinetUrl, { timeout: 30000 }); // Extract token from localStorage const token = await page.evaluate(() => localStorage.getItem('token')); if (!token) { throw new InternalServerErrorException( 'Login succeeded but token was not found in localStorage', ); } // Capture all cookies and localStorage from the browser context const cookies = await context.cookies(); const localStorageData = await page.evaluate(() => { const entries: Record = {}; for (let i = 0; i < window.localStorage.length; i++) { const k = window.localStorage.key(i); if (k !== null) entries[k] = window.localStorage.getItem(k) ?? ''; } return entries; }); await this.sessionService.upsert(resolvedSession, token, cookies, localStorageData); this.logger.log(`Login successful for key ${keyId}, session: ${resolvedSession}`); return { token, sessionName: resolvedSession }; } catch (err) { if ( err instanceof BadRequestException || err instanceof InternalServerErrorException ) { throw err; } this.logger.error(`Login failed: ${(err as Error).message}`); throw new InternalServerErrorException( `Login automation failed: ${(err as Error).message}`, { cause: err }, ); } finally { await browser.close(); } } }