feat(auth): add login/password authentication strategy

- KeyDescriptor now accepts optional login field alongside keyFile
- when login is present, clicks login/password auth method, fills email+password fields, and submits
- file-key flow unchanged; validation ensures exactly one strategy is present
This commit is contained in:
2026-04-07 19:33:08 +03:00
parent 02630e4919
commit ae8852d738
+37 -15
View File
@@ -14,7 +14,8 @@ import { SessionService } from '../session/session.service';
import { EnvironmentService } from '../environment/environment.service'; import { EnvironmentService } from '../environment/environment.service';
interface KeyDescriptor { interface KeyDescriptor {
keyFile: string; keyFile?: string;
login?: string;
password: string; password: string;
} }
@@ -64,9 +65,16 @@ export class AuthService {
throw new BadRequestException(`Cannot read key descriptor: ${keyId}`, { cause: err }); throw new BadRequestException(`Cannot read key descriptor: ${keyId}`, { cause: err });
} }
const keyFilePath = path.resolve(this.keysDir, descriptor.keyFile); const useLoginPassword = !!descriptor.login;
if (!fs.existsSync(keyFilePath)) {
throw new BadRequestException(`Key file not found: ${descriptor.keyFile}`); 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 }); const browser = await chromium.launch({ headless: true, executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH });
@@ -77,20 +85,34 @@ export class AuthService {
this.logger.log(`Navigating to ${loginUrl}`); this.logger.log(`Navigating to ${loginUrl}`);
await page.goto(loginUrl, { waitUntil: 'networkidle' }); await page.goto(loginUrl, { waitUntil: 'networkidle' });
// Click "Файловий ключ" button if (useLoginPassword) {
await page.getByText('Файловий ключ').click(); // Click "Логін і пароль" auth method
await page.locator('p[aria-label="Логін і пароль"]').click();
// Upload key file via hidden file input // Fill login and password
const fileInput = page.locator('input[accept=".dat,.pfx,.pk8,.zs2,.jks"][type="file"]'); await page.getByLabel('Електронна пошта').fill(descriptor.login!);
await fileInput.setInputFiles(keyFilePath); await page.getByLabel('Пароль').fill(descriptor.password);
// Enter password // Click "Увійти"
await page await page.locator('button:has-text("Увійти")').click();
.locator('#id-app-login-file-key-password') } else {
.fill(descriptor.password); const keyFilePath = path.resolve(this.keysDir, descriptor.keyFile!);
// Click "Продовжити" // Click "Файловий ключ" button
await page.locator('#id-app-login-file-key-sign-button').click(); 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 // Wait until redirected to cabinet
this.logger.log(`Waiting for redirect to ${cabinetUrl}`); this.logger.log(`Waiting for redirect to ${cabinetUrl}`);