feat: Two-Factor Authentication (TOTP, Email OTP) and OIDC/SSO – full implementation with admin UI (#933)

feat: Two-Factor Authentication (TOTP, Email OTP) and OIDC/SSO – full implementation with admin UI (#933)
This commit is contained in:
Sn0rrii
2026-04-13 13:24:28 +02:00
committed by GitHub
parent 8af0966e68
commit ba1c97c808
44 changed files with 10473 additions and 290 deletions
+14 -14
View File
@@ -7,23 +7,23 @@ import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import { setAuthToken, getAuthToken, api } from '../../api/client';
// Mock localStorage
const localStorageMock = {
// Mock sessionStorage (H-5: tokens are stored in sessionStorage, not localStorage)
const sessionStorageMock = {
store: {} as Record<string, string>,
getItem: vi.fn((key: string) => localStorageMock.store[key] || null),
getItem: vi.fn((key: string) => sessionStorageMock.store[key] || null),
setItem: vi.fn((key: string, value: string) => {
localStorageMock.store[key] = value;
sessionStorageMock.store[key] = value;
}),
removeItem: vi.fn((key: string) => {
delete localStorageMock.store[key];
delete sessionStorageMock.store[key];
}),
clear: vi.fn(() => {
localStorageMock.store = {};
sessionStorageMock.store = {};
}),
};
Object.defineProperty(window, 'localStorage', {
value: localStorageMock,
Object.defineProperty(window, 'sessionStorage', {
value: sessionStorageMock,
});
// Create MSW server
@@ -32,22 +32,22 @@ const server = setupServer();
beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }));
afterEach(() => {
server.resetHandlers();
localStorageMock.clear();
sessionStorageMock.clear();
setAuthToken(null);
});
afterAll(() => server.close());
describe('Auth Token Management', () => {
it('setAuthToken stores token in localStorage', () => {
it('setAuthToken stores token in sessionStorage', () => {
setAuthToken('test-token-123');
expect(localStorageMock.setItem).toHaveBeenCalledWith('auth_token', 'test-token-123');
expect(sessionStorageMock.setItem).toHaveBeenCalledWith('auth_token', 'test-token-123');
expect(getAuthToken()).toBe('test-token-123');
});
it('setAuthToken removes token from localStorage when null', () => {
it('setAuthToken removes token from sessionStorage when null', () => {
setAuthToken('test-token-123');
setAuthToken(null);
expect(localStorageMock.removeItem).toHaveBeenCalledWith('auth_token');
expect(sessionStorageMock.removeItem).toHaveBeenCalledWith('auth_token');
expect(getAuthToken()).toBeNull();
});
});
@@ -115,7 +115,7 @@ describe('API Client Auth Header', () => {
}
expect(getAuthToken()).toBeNull();
expect(localStorageMock.removeItem).toHaveBeenCalledWith('auth_token');
expect(sessionStorageMock.removeItem).toHaveBeenCalledWith('auth_token');
});
it('does not clear token on 401 with generic auth error', async () => {