diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..459b484 Binary files /dev/null and b/public/favicon.ico differ diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 00fc11a..5763c2d 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -43,6 +43,11 @@ export const appConfig: ApplicationConfig = { useFactory: (tenantResolver: TenantResolverService): MarketplacesAuthConfig => ({ apiUrl: environment.authApiUrl, marketplaceDomain: () => tenantResolver.getBaseDomain(), + // Admin login/password contract per docs/backend/BACKEND-INTEGRATION.md §1.2. + // Not live yet - backend endpoint doesn't exist, so every call here 404s + // until it ships. That's fine: the UI treats any failure as "something + // went wrong, try Telegram instead" and never surfaces the raw error. + credentialsPath: '/api/identity/v1/session', }), deps: [TenantResolverService] }, diff --git a/src/app/components/telegram-login/telegram-login.component.html b/src/app/components/telegram-login/telegram-login.component.html index 2cbef88..bf21ee8 100644 --- a/src/app/components/telegram-login/telegram-login.component.html +++ b/src/app/components/telegram-login/telegram-login.component.html @@ -64,6 +64,38 @@ } + @if (mode === 'admin') { + @if (!credentialsVisible()) { + + } @else { +
+ + + @if (credentialsError()) { + + } +
+ + +
+
+ } + } +

{{ (mode === 'admin' ? 'auth.adminLoginNote' : 'auth.loginNote') | translate }}

} diff --git a/src/app/components/telegram-login/telegram-login.component.scss b/src/app/components/telegram-login/telegram-login.component.scss index 8162c1e..88afb64 100644 --- a/src/app/components/telegram-login/telegram-login.component.scss +++ b/src/app/components/telegram-login/telegram-login.component.scss @@ -195,6 +195,91 @@ h2 { } } +.credentials-toggle { + display: block; + width: 100%; + margin-top: 16px; + padding: 10px; + border: none; + background: none; + color: var(--accent-color, #497671); + font-size: var(--font-size-sm, 0.8125rem); + cursor: pointer; + + &:hover { + text-decoration: underline; + } +} + +.credentials-form { + margin-top: 16px; + text-align: left; + + label { + display: block; + margin-bottom: 12px; + font-size: var(--font-size-sm, 0.8125rem); + color: var(--text-secondary, #666); + + input { + display: block; + width: 100%; + margin-top: 6px; + padding: 10px 12px; + border: 1px solid #e0e0e0; + border-radius: var(--radius-md, 12px); + font-size: var(--font-size-base, 0.875rem); + color: var(--text-primary, #1a1a1a); + background: var(--bg-card, #fff); + + &:focus { + outline: none; + border-color: var(--accent-color, #497671); + } + } + } + + .credentials-error { + margin: 0 0 12px; + font-size: var(--font-size-xs, 0.75rem); + color: #d32f2f; + line-height: 1.4; + } + + .credentials-actions { + display: flex; + gap: 10px; + } + + .credentials-submit { + flex: 1; + padding: 12px 20px; + border: none; + border-radius: var(--radius-md, 12px); + background: var(--accent-color, #497671); + color: #fff; + font-size: var(--font-size-base, 0.875rem); + font-weight: var(--font-weight-semibold, 600); + cursor: pointer; + transition: opacity 0.2s ease; + + &:disabled { + opacity: 0.6; + cursor: wait; + } + } + + .credentials-back { + padding: 12px 16px; + border: 1px solid #e0e0e0; + border-radius: var(--radius-md, 12px); + background: none; + color: var(--text-secondary, #666); + font-size: var(--font-size-base, 0.875rem); + cursor: pointer; + } +} + .login-note { margin: 16px 0 0; font-size: var(--font-size-xs, 0.75rem); diff --git a/src/app/components/telegram-login/telegram-login.component.ts b/src/app/components/telegram-login/telegram-login.component.ts index 02e624b..02f8697 100644 --- a/src/app/components/telegram-login/telegram-login.component.ts +++ b/src/app/components/telegram-login/telegram-login.component.ts @@ -1,6 +1,6 @@ -import { Component, ChangeDetectionStrategy, Input, Injector, Signal, inject, effect, OnDestroy, OnInit } from '@angular/core'; +import { Component, ChangeDetectionStrategy, Input, Injector, Signal, signal, inject, effect, OnDestroy, OnInit } from '@angular/core'; import { Router } from '@angular/router'; -import { AuthService, AdminAuthService, AuthSession } from '@marketplaces/auth'; +import { AuthService, AdminAuthService, AuthSession, MARKETPLACES_AUTH_GATEWAY } from '@marketplaces/auth'; import { LanguageService } from '../../services/language.service'; import { TranslatePipe } from '../../i18n/translate.pipe'; import { QrLoginEngine } from '../../shared/qr-login/qr-login.engine'; @@ -27,6 +27,7 @@ export class TelegramLoginComponent implements OnInit, OnDestroy { private readonly customerAuth = inject(AuthService); private readonly adminAuth = inject(AdminAuthService); + private readonly authGateway = inject(MARKETPLACES_AUTH_GATEWAY); private readonly injector = inject(Injector); private readonly router = inject(Router); private readonly languageService = inject(LanguageService); @@ -41,6 +42,17 @@ export class TelegramLoginComponent implements OnInit, OnDestroy { qrStatus!: Signal; encodedQrUrl!: Signal; + // Admin-only: password login per docs/backend/BACKEND-INTEGRATION.md §1.2. + // Backend endpoint isn't live yet, so any failure here (404 today, or a + // real auth failure once it ships) surfaces as one generic message - + // never the raw error - per product decision to keep Telegram as the + // fallback path rather than leak backend state. + credentialsVisible = signal(false); + credentialsLogin = signal(''); + credentialsPassword = signal(''); + credentialsBusy = signal(false); + credentialsError = signal(false); + ngOnInit(): void { const service = this.mode === 'admin' ? this.adminAuth : this.customerAuth; this.showDialog = service.showLoginDialog; @@ -81,6 +93,7 @@ export class TelegramLoginComponent implements OnInit, OnDestroy { close(): void { this.engine.setActive(false); (this.mode === 'admin' ? this.adminAuth : this.customerAuth).hideLogin(); + this.resetCredentials(); } openTelegramLogin(): void { @@ -90,4 +103,38 @@ export class TelegramLoginComponent implements OnInit, OnDestroy { refreshQr(): void { this.engine.refresh(); } + + toggleCredentials(): void { + this.credentialsVisible.update(visible => !visible); + this.credentialsError.set(false); + } + + submitCredentials(event: Event): void { + event.preventDefault(); + const login = this.credentialsLogin().trim(); + const password = this.credentialsPassword(); + if (!login || !password || this.credentialsBusy()) return; + + this.credentialsBusy.set(true); + this.credentialsError.set(false); + this.authGateway.loginWithCredentials('admin', { login, password }).subscribe({ + next: () => { + this.credentialsBusy.set(false); + this.adminAuth.onLoginComplete(); + void this.router.navigate([this.languageService.currentLanguage(), 'backoffice', 'dashboard']); + }, + error: () => { + this.credentialsBusy.set(false); + this.credentialsError.set(true); + }, + }); + } + + private resetCredentials(): void { + this.credentialsVisible.set(false); + this.credentialsLogin.set(''); + this.credentialsPassword.set(''); + this.credentialsBusy.set(false); + this.credentialsError.set(false); + } } diff --git a/src/app/i18n/en.ts b/src/app/i18n/en.ts index 8ac5f7a..a8177d8 100644 --- a/src/app/i18n/en.ts +++ b/src/app/i18n/en.ts @@ -1164,6 +1164,12 @@ export const en: Translations = { loginNote: 'You will be redirected back after login', qrExpired: 'QR code expired. Click to refresh', qrError: 'Could not create login session. Click to retry', + loginWithCredentials: 'Sign in with login and password', + credentialsLoginLabel: 'Login', + credentialsPasswordLabel: 'Password', + credentialsSubmit: 'Sign in', + credentialsBack: 'Back', + credentialsGenericError: 'Something went wrong. Try another way to sign in.', }, ux: { items: 'items', diff --git a/src/app/i18n/hy.ts b/src/app/i18n/hy.ts index 09a48cb..6eda9d5 100644 --- a/src/app/i18n/hy.ts +++ b/src/app/i18n/hy.ts @@ -1164,6 +1164,12 @@ export const hy: Translations = { loginNote: 'Մուտքից հետո դուք կվերաուղղվեք', qrExpired: 'QR կոդը հնացել է։ Սեղմեք՝ թարմացնելու համար', qrError: 'Չհաջողվեց ստեղծել մուտքի սեսիա։ Սեղմեք՝ կրկնելու համար', + loginWithCredentials: 'Մուտք գործել լոգինով և գաղտնաբառով', + credentialsLoginLabel: 'Լոգին', + credentialsPasswordLabel: 'Գաղտնաբառ', + credentialsSubmit: 'Մուտք', + credentialsBack: 'Հետ', + credentialsGenericError: 'Ինչ-որ բան այն չէ։ Փորձեք մուտք գործել այլ եղանակով։', }, ux: { items: 'ապրանք', diff --git a/src/app/i18n/ru.ts b/src/app/i18n/ru.ts index 382898b..055e585 100644 --- a/src/app/i18n/ru.ts +++ b/src/app/i18n/ru.ts @@ -1164,6 +1164,12 @@ export const ru: Translations = { loginNote: 'После входа вы будете перенаправлены обратно', qrExpired: 'QR-код устарел. Нажмите, чтобы обновить', qrError: 'Не удалось создать сессию входа. Нажмите, чтобы повторить', + loginWithCredentials: 'Войти по логину и паролю', + credentialsLoginLabel: 'Логин', + credentialsPasswordLabel: 'Пароль', + credentialsSubmit: 'Войти', + credentialsBack: 'Назад', + credentialsGenericError: 'Что-то пошло не так. Попробуйте войти другим способом.', }, ux: { items: 'товаров', diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts index ba0dac4..a06c716 100644 --- a/src/app/i18n/translations.ts +++ b/src/app/i18n/translations.ts @@ -1163,6 +1163,12 @@ export interface Translations { loginNote: string; qrExpired: string; qrError: string; + loginWithCredentials: string; + credentialsLoginLabel: string; + credentialsPasswordLabel: string; + credentialsSubmit: string; + credentialsBack: string; + credentialsGenericError: string; }; ux: { items: string;