feat(auth): add AdminAuthService.loginWithCredentials + rate_limited AuthFailure code
Convenience wrapper around MarketplacesAuthGateway.loginWithCredentials('admin', ...)
that activates the returned session in one call, for apps building a custom admin
login screen. AuthFailure gains a rate_limited code (HTTP 429, parsed Retry-After)
and an optional status field so 401/403/429 surface distinctly without new plumbing.
No credentials, no client-side comparison, no superadmin concept anywhere in this
package - same code path as any other admin credential login. Inert without a
backend implementing POST {credentialsPath} { login, password, mode: 'admin' }.
Additive, backward-compatible. QR/Yandex flows unmodified.
This commit is contained in:
124
packages/auth/test/admin-credentials-login.test.mjs
Normal file
124
packages/auth/test/admin-credentials-login.test.mjs
Normal file
@@ -0,0 +1,124 @@
|
||||
import '@angular/compiler';
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { Injector } from '@angular/core';
|
||||
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
|
||||
import { of, throwError } from 'rxjs';
|
||||
import {
|
||||
AdminAuthService,
|
||||
AuthService,
|
||||
AuthMarketplaceContext,
|
||||
TelegramSessionApiService,
|
||||
HttpMarketplacesAuthGateway,
|
||||
MARKETPLACES_AUTH_GATEWAY,
|
||||
MARKETPLACES_AUTH_CONFIG,
|
||||
AUTH_API_URL,
|
||||
} from '../dist/fesm2022/marketplaces-auth.mjs';
|
||||
|
||||
const API_URL = 'https://api.example.com';
|
||||
const CREDENTIALS_URL = `${API_URL}/auth/credentials/login`;
|
||||
|
||||
/** Wires up the real gateway/context/service classes with a scripted HttpClient - no TestBed, no NgModule compiler needed. */
|
||||
function setup(postResponse) {
|
||||
const calls = [];
|
||||
const fakeHttp = { post: (url, body, opts) => { calls.push({ url, body, opts }); return postResponse; } };
|
||||
|
||||
const injector = Injector.create({
|
||||
providers: [
|
||||
{ provide: HttpClient, useValue: fakeHttp },
|
||||
{ provide: MARKETPLACES_AUTH_CONFIG, useValue: {
|
||||
apiUrl: API_URL,
|
||||
marketplaceDomain: 'admin.tenant-a.example.com',
|
||||
credentialsPath: '/auth/credentials/login',
|
||||
} },
|
||||
{ provide: AUTH_API_URL, useValue: API_URL },
|
||||
AuthMarketplaceContext,
|
||||
TelegramSessionApiService,
|
||||
AuthService,
|
||||
AdminAuthService,
|
||||
HttpMarketplacesAuthGateway,
|
||||
{ provide: MARKETPLACES_AUTH_GATEWAY, useExisting: HttpMarketplacesAuthGateway },
|
||||
],
|
||||
});
|
||||
|
||||
return { admin: injector.get(AdminAuthService), calls };
|
||||
}
|
||||
|
||||
test('loginWithCredentials: success activates the returned session and stores tokens', () => {
|
||||
const backendResponse = {
|
||||
session: {
|
||||
sessionId: 's1', userId: 1, username: 'root', displayName: 'Root',
|
||||
active: true, expires: new Date(Date.now() + 60_000).toISOString(),
|
||||
},
|
||||
accessToken: 'jwt-access',
|
||||
refreshToken: 'jwt-refresh',
|
||||
};
|
||||
const { admin, calls } = setup(of(backendResponse));
|
||||
|
||||
let result;
|
||||
admin.loginWithCredentials({ login: 'root', password: 'secret' }).subscribe(r => (result = r));
|
||||
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].url, CREDENTIALS_URL);
|
||||
assert.deepEqual(calls[0].body, { login: 'root', password: 'secret', mode: 'admin' });
|
||||
assert.equal(calls[0].opts.headers.get('X-Marketplace-Domain'), 'admin.tenant-a.example.com');
|
||||
|
||||
assert.equal(admin.isAuthenticated(), true);
|
||||
assert.equal(admin.session()?.sessionId, 's1');
|
||||
// getAdminToken()/setAdminTokens() are guarded no-ops without a `localStorage`
|
||||
// (e.g. this Node test runner), same as production SSR - token storage
|
||||
// itself isn't under test here, just that acceptSession() was invoked.
|
||||
assert.equal(result.accessToken, 'jwt-access');
|
||||
assert.equal(result.mode, 'admin');
|
||||
assert.equal(result.method, 'credentials');
|
||||
});
|
||||
|
||||
test('loginWithCredentials: 401 surfaces invalid_credentials and does not mutate session state', () => {
|
||||
const error = new HttpErrorResponse({ status: 401, statusText: 'Unauthorized', error: { message: 'Invalid login or password' } });
|
||||
const { admin } = setup(throwError(() => error));
|
||||
|
||||
let failure;
|
||||
admin.loginWithCredentials({ login: 'root', password: 'wrong' }).subscribe({
|
||||
next: () => assert.fail('should not succeed'),
|
||||
error: err => (failure = err),
|
||||
});
|
||||
|
||||
assert.equal(failure.code, 'invalid_credentials');
|
||||
assert.equal(failure.status, 401);
|
||||
assert.equal(admin.isAuthenticated(), false);
|
||||
assert.equal(admin.session(), null);
|
||||
assert.equal(admin.getAdminToken(), null);
|
||||
});
|
||||
|
||||
test('loginWithCredentials: 429 surfaces rate_limited with parsed Retry-After', () => {
|
||||
const error = new HttpErrorResponse({
|
||||
status: 429, statusText: 'Too Many Requests', error: { message: 'Too many attempts' },
|
||||
headers: new HttpHeaders({ 'Retry-After': '30' }),
|
||||
});
|
||||
const { admin } = setup(throwError(() => error));
|
||||
|
||||
let failure;
|
||||
admin.loginWithCredentials({ login: 'root', password: 'secret' }).subscribe({
|
||||
next: () => assert.fail('should not succeed'),
|
||||
error: err => (failure = err),
|
||||
});
|
||||
|
||||
assert.equal(failure.code, 'rate_limited');
|
||||
assert.equal(failure.retryAfterSeconds, 30);
|
||||
assert.equal(admin.isAuthenticated(), false);
|
||||
});
|
||||
|
||||
test('loginWithCredentials: 403 surfaces as a generic backend failure', () => {
|
||||
const error = new HttpErrorResponse({ status: 403, statusText: 'Forbidden', error: { message: 'Tenant disabled' } });
|
||||
const { admin } = setup(throwError(() => error));
|
||||
|
||||
let failure;
|
||||
admin.loginWithCredentials({ login: 'root', password: 'secret' }).subscribe({
|
||||
next: () => assert.fail('should not succeed'),
|
||||
error: err => (failure = err),
|
||||
});
|
||||
|
||||
assert.equal(failure.code, 'backend');
|
||||
assert.equal(failure.status, 403);
|
||||
assert.equal(admin.isAuthenticated(), false);
|
||||
});
|
||||
Reference in New Issue
Block a user