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:
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 547 B |
@@ -43,6 +43,11 @@ export const appConfig: ApplicationConfig = {
|
|||||||
useFactory: (tenantResolver: TenantResolverService): MarketplacesAuthConfig => ({
|
useFactory: (tenantResolver: TenantResolverService): MarketplacesAuthConfig => ({
|
||||||
apiUrl: environment.authApiUrl,
|
apiUrl: environment.authApiUrl,
|
||||||
marketplaceDomain: () => tenantResolver.getBaseDomain(),
|
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]
|
deps: [TenantResolverService]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -64,6 +64,38 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</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>
|
<p class="login-note">{{ (mode === 'admin' ? 'auth.adminLoginNote' : 'auth.loginNote') | translate }}</p>
|
||||||
}
|
}
|
||||||
</div>
|
</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 {
|
.login-note {
|
||||||
margin: 16px 0 0;
|
margin: 16px 0 0;
|
||||||
font-size: var(--font-size-xs, 0.75rem);
|
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 { 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 { LanguageService } from '../../services/language.service';
|
||||||
import { TranslatePipe } from '../../i18n/translate.pipe';
|
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||||
import { QrLoginEngine } from '../../shared/qr-login/qr-login.engine';
|
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 customerAuth = inject(AuthService);
|
||||||
private readonly adminAuth = inject(AdminAuthService);
|
private readonly adminAuth = inject(AdminAuthService);
|
||||||
|
private readonly authGateway = inject(MARKETPLACES_AUTH_GATEWAY);
|
||||||
private readonly injector = inject(Injector);
|
private readonly injector = inject(Injector);
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
private readonly languageService = inject(LanguageService);
|
private readonly languageService = inject(LanguageService);
|
||||||
@@ -41,6 +42,17 @@ export class TelegramLoginComponent implements OnInit, OnDestroy {
|
|||||||
qrStatus!: Signal<QrLoginStatus>;
|
qrStatus!: Signal<QrLoginStatus>;
|
||||||
encodedQrUrl!: Signal<string>;
|
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 {
|
ngOnInit(): void {
|
||||||
const service = this.mode === 'admin' ? this.adminAuth : this.customerAuth;
|
const service = this.mode === 'admin' ? this.adminAuth : this.customerAuth;
|
||||||
this.showDialog = service.showLoginDialog;
|
this.showDialog = service.showLoginDialog;
|
||||||
@@ -81,6 +93,7 @@ export class TelegramLoginComponent implements OnInit, OnDestroy {
|
|||||||
close(): void {
|
close(): void {
|
||||||
this.engine.setActive(false);
|
this.engine.setActive(false);
|
||||||
(this.mode === 'admin' ? this.adminAuth : this.customerAuth).hideLogin();
|
(this.mode === 'admin' ? this.adminAuth : this.customerAuth).hideLogin();
|
||||||
|
this.resetCredentials();
|
||||||
}
|
}
|
||||||
|
|
||||||
openTelegramLogin(): void {
|
openTelegramLogin(): void {
|
||||||
@@ -90,4 +103,38 @@ export class TelegramLoginComponent implements OnInit, OnDestroy {
|
|||||||
refreshQr(): void {
|
refreshQr(): void {
|
||||||
this.engine.refresh();
|
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',
|
loginNote: 'You will be redirected back after login',
|
||||||
qrExpired: 'QR code expired. Click to refresh',
|
qrExpired: 'QR code expired. Click to refresh',
|
||||||
qrError: 'Could not create login session. Click to retry',
|
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: {
|
ux: {
|
||||||
items: 'items',
|
items: 'items',
|
||||||
|
|||||||
@@ -1164,6 +1164,12 @@ export const hy: Translations = {
|
|||||||
loginNote: 'Մուտքից հետո դուք կվերաուղղվեք',
|
loginNote: 'Մուտքից հետո դուք կվերաուղղվեք',
|
||||||
qrExpired: 'QR կոդը հնացել է։ Սեղմեք՝ թարմացնելու համար',
|
qrExpired: 'QR կոդը հնացել է։ Սեղմեք՝ թարմացնելու համար',
|
||||||
qrError: 'Չհաջողվեց ստեղծել մուտքի սեսիա։ Սեղմեք՝ կրկնելու համար',
|
qrError: 'Չհաջողվեց ստեղծել մուտքի սեսիա։ Սեղմեք՝ կրկնելու համար',
|
||||||
|
loginWithCredentials: 'Մուտք գործել լոգինով և գաղտնաբառով',
|
||||||
|
credentialsLoginLabel: 'Լոգին',
|
||||||
|
credentialsPasswordLabel: 'Գաղտնաբառ',
|
||||||
|
credentialsSubmit: 'Մուտք',
|
||||||
|
credentialsBack: 'Հետ',
|
||||||
|
credentialsGenericError: 'Ինչ-որ բան այն չէ։ Փորձեք մուտք գործել այլ եղանակով։',
|
||||||
},
|
},
|
||||||
ux: {
|
ux: {
|
||||||
items: 'ապրանք',
|
items: 'ապրանք',
|
||||||
|
|||||||
@@ -1164,6 +1164,12 @@ export const ru: Translations = {
|
|||||||
loginNote: 'После входа вы будете перенаправлены обратно',
|
loginNote: 'После входа вы будете перенаправлены обратно',
|
||||||
qrExpired: 'QR-код устарел. Нажмите, чтобы обновить',
|
qrExpired: 'QR-код устарел. Нажмите, чтобы обновить',
|
||||||
qrError: 'Не удалось создать сессию входа. Нажмите, чтобы повторить',
|
qrError: 'Не удалось создать сессию входа. Нажмите, чтобы повторить',
|
||||||
|
loginWithCredentials: 'Войти по логину и паролю',
|
||||||
|
credentialsLoginLabel: 'Логин',
|
||||||
|
credentialsPasswordLabel: 'Пароль',
|
||||||
|
credentialsSubmit: 'Войти',
|
||||||
|
credentialsBack: 'Назад',
|
||||||
|
credentialsGenericError: 'Что-то пошло не так. Попробуйте войти другим способом.',
|
||||||
},
|
},
|
||||||
ux: {
|
ux: {
|
||||||
items: 'товаров',
|
items: 'товаров',
|
||||||
|
|||||||
@@ -1163,6 +1163,12 @@ export interface Translations {
|
|||||||
loginNote: string;
|
loginNote: string;
|
||||||
qrExpired: string;
|
qrExpired: string;
|
||||||
qrError: string;
|
qrError: string;
|
||||||
|
loginWithCredentials: string;
|
||||||
|
credentialsLoginLabel: string;
|
||||||
|
credentialsPasswordLabel: string;
|
||||||
|
credentialsSubmit: string;
|
||||||
|
credentialsBack: string;
|
||||||
|
credentialsGenericError: string;
|
||||||
};
|
};
|
||||||
ux: {
|
ux: {
|
||||||
items: string;
|
items: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user