191 lines
4.5 KiB
TypeScript
191 lines
4.5 KiB
TypeScript
/**
|
|
* SSO API Client for E2E testing.
|
|
*
|
|
* Direct HTTP client for SSO API operations.
|
|
* Used by test fixtures to perform auth operations without going through the UI.
|
|
*/
|
|
|
|
export interface User {
|
|
id: string;
|
|
email: string;
|
|
username: string;
|
|
accessLevel: string;
|
|
profiles: string[];
|
|
isActive: boolean;
|
|
emailVerified: boolean;
|
|
avatar?: string;
|
|
bio?: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface LoginResponse {
|
|
sessionId: string;
|
|
user: User;
|
|
expiresAt: string;
|
|
}
|
|
|
|
export interface RegisterData {
|
|
email: string;
|
|
username: string;
|
|
password: string;
|
|
accessLevel?: string;
|
|
}
|
|
|
|
interface SSOApiClientOptions {
|
|
baseUrl: string;
|
|
timeout?: number;
|
|
}
|
|
|
|
/**
|
|
* HTTP client for direct SSO API calls.
|
|
*
|
|
* This allows tests to perform auth operations without going through the UI,
|
|
* making test setup faster and more reliable.
|
|
*/
|
|
export class SSOApiClient {
|
|
private readonly baseUrl: string;
|
|
private readonly timeout: number;
|
|
|
|
constructor(options: SSOApiClientOptions) {
|
|
this.baseUrl = options.baseUrl.replace(/\/$/, '');
|
|
this.timeout = options.timeout || 10000;
|
|
}
|
|
|
|
/**
|
|
* Login with email and password.
|
|
*
|
|
* @returns Session ID and user data
|
|
* @throws Error if login fails
|
|
*/
|
|
async login(credentials: { email: string; password: string }): Promise<LoginResponse> {
|
|
const response = await this.fetch('/auth/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify(credentials),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ message: 'Login failed' }));
|
|
throw new Error(`Login failed: ${error.message || response.statusText}`);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Register a new user.
|
|
*
|
|
* @returns Session ID and user data
|
|
* @throws Error if registration fails
|
|
*/
|
|
async register(data: RegisterData): Promise<LoginResponse> {
|
|
const response = await this.fetch('/auth/register', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ message: 'Registration failed' }));
|
|
throw new Error(`Registration failed: ${error.message || response.statusText}`);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Logout and invalidate session.
|
|
*/
|
|
async logout(sessionId: string): Promise<void> {
|
|
const response = await this.fetch('/auth/logout', {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${sessionId}`,
|
|
},
|
|
});
|
|
|
|
// Logout may return 401 if session already expired - that's fine
|
|
if (!response.ok && response.status !== 401) {
|
|
throw new Error(`Logout failed: ${response.statusText}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get current user from session.
|
|
*
|
|
* @returns User data if authenticated, null otherwise
|
|
*/
|
|
async getCurrentUser(sessionId: string): Promise<User | null> {
|
|
const response = await this.fetch('/auth/me', {
|
|
headers: {
|
|
Authorization: `Bearer ${sessionId}`,
|
|
},
|
|
});
|
|
|
|
if (response.status === 401) {
|
|
return null;
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to get user: ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data.user;
|
|
}
|
|
|
|
/**
|
|
* Validate a session token.
|
|
*
|
|
* @returns true if session is valid, false otherwise
|
|
*/
|
|
async validateSession(sessionId: string): Promise<boolean> {
|
|
const user = await this.getCurrentUser(sessionId);
|
|
return user !== null;
|
|
}
|
|
|
|
/**
|
|
* Request password reset.
|
|
*/
|
|
async requestPasswordReset(email: string): Promise<void> {
|
|
const response = await this.fetch('/auth/password-reset/request', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ email }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Password reset request failed: ${response.statusText}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Health check for SSO service.
|
|
*/
|
|
async healthCheck(): Promise<boolean> {
|
|
try {
|
|
const response = await this.fetch('/health');
|
|
return response.ok;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private async fetch(path: string, options: RequestInit = {}): Promise<Response> {
|
|
const url = `${this.baseUrl}${path}`;
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
|
|
try {
|
|
return await fetch(url, {
|
|
...options,
|
|
signal: controller.signal,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Accept: 'application/json',
|
|
...options.headers,
|
|
},
|
|
});
|
|
} finally {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
}
|
|
}
|