feat(auth): add admin login/password form with generic-error fallback
Wires the existing @marketplaces/auth credentials gateway into the admin Telegram login dialog behind a toggle. Targets the documented but not-yet- live POST /api/identity/v1/session contract (BACKEND-INTEGRATION.md §1.2); any failure - 404 today, a real auth error once the backend ships - shows one generic "try another way to sign in" message, never the raw error. Also adds public/favicon.ico (was 404ing; reuses existing app icon).
This commit is contained in:
@@ -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]
|
||||
},
|
||||
|
||||
@@ -64,6 +64,38 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (mode === 'admin') {
|
||||
@if (!credentialsVisible()) {
|
||||
<button class="credentials-toggle" type="button" (click)="toggleCredentials()">
|
||||
{{ 'auth.loginWithCredentials' | translate }}
|
||||
</button>
|
||||
} @else {
|
||||
<form class="credentials-form" (submit)="submitCredentials($event)">
|
||||
<label>
|
||||
{{ 'auth.credentialsLoginLabel' | translate }}
|
||||
<input type="text" autocomplete="username" [value]="credentialsLogin()"
|
||||
(input)="credentialsLogin.set($any($event.target).value)" />
|
||||
</label>
|
||||
<label>
|
||||
{{ 'auth.credentialsPasswordLabel' | translate }}
|
||||
<input type="password" autocomplete="current-password" [value]="credentialsPassword()"
|
||||
(input)="credentialsPassword.set($any($event.target).value)" />
|
||||
</label>
|
||||
@if (credentialsError()) {
|
||||
<p class="credentials-error" role="alert">{{ 'auth.credentialsGenericError' | translate }}</p>
|
||||
}
|
||||
<div class="credentials-actions">
|
||||
<button type="submit" class="credentials-submit" [disabled]="credentialsBusy()">
|
||||
{{ 'auth.credentialsSubmit' | translate }}
|
||||
</button>
|
||||
<button type="button" class="credentials-back" (click)="toggleCredentials()">
|
||||
{{ 'auth.credentialsBack' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
|
||||
<p class="login-note">{{ (mode === 'admin' ? 'auth.adminLoginNote' : 'auth.loginNote') | translate }}</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<QrLoginStatus>;
|
||||
encodedQrUrl!: Signal<string>;
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1164,6 +1164,12 @@ export const hy: Translations = {
|
||||
loginNote: 'Մուտքից հետո դուք կվերաուղղվեք',
|
||||
qrExpired: 'QR կոդը հնացել է։ Սեղմեք՝ թարմացնելու համար',
|
||||
qrError: 'Չհաջողվեց ստեղծել մուտքի սեսիա։ Սեղմեք՝ կրկնելու համար',
|
||||
loginWithCredentials: 'Մուտք գործել լոգինով և գաղտնաբառով',
|
||||
credentialsLoginLabel: 'Լոգին',
|
||||
credentialsPasswordLabel: 'Գաղտնաբառ',
|
||||
credentialsSubmit: 'Մուտք',
|
||||
credentialsBack: 'Հետ',
|
||||
credentialsGenericError: 'Ինչ-որ բան այն չէ։ Փորձեք մուտք գործել այլ եղանակով։',
|
||||
},
|
||||
ux: {
|
||||
items: 'ապրանք',
|
||||
|
||||
@@ -1164,6 +1164,12 @@ export const ru: Translations = {
|
||||
loginNote: 'После входа вы будете перенаправлены обратно',
|
||||
qrExpired: 'QR-код устарел. Нажмите, чтобы обновить',
|
||||
qrError: 'Не удалось создать сессию входа. Нажмите, чтобы повторить',
|
||||
loginWithCredentials: 'Войти по логину и паролю',
|
||||
credentialsLoginLabel: 'Логин',
|
||||
credentialsPasswordLabel: 'Пароль',
|
||||
credentialsSubmit: 'Войти',
|
||||
credentialsBack: 'Назад',
|
||||
credentialsGenericError: 'Что-то пошло не так. Попробуйте войти другим способом.',
|
||||
},
|
||||
ux: {
|
||||
items: 'товаров',
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user