Initial Version
This commit is contained in:
100
core/src/services/authenticationService.ts
Normal file
100
core/src/services/authenticationService.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { ChallengeResponse, IdentifyResponse, RedirectResponse, SessionStatus, StartResponse, VerifyResponse } from '@KTXC/types/authenticationTypes';
|
||||
import { fetchWrapper } from '@KTXC/utils/helpers/fetch-wrapper';
|
||||
|
||||
export const authenticationService = {
|
||||
/**
|
||||
* Initialize authentication - get session and available methods
|
||||
*/
|
||||
async start(): Promise<StartResponse> {
|
||||
return fetchWrapper.get('/auth/start', undefined, { skipLogoutOnError: true });
|
||||
},
|
||||
|
||||
/**
|
||||
* Identify user - stores identity in session for identity-first flow
|
||||
* Returns tenant-wide methods (no user-specific filtering to prevent enumeration)
|
||||
*
|
||||
* @param session - Session ID from start
|
||||
* @param identity - User identity (email/username)
|
||||
*/
|
||||
async identify(session: string, identity: string): Promise<IdentifyResponse> {
|
||||
return fetchWrapper.post('/auth/identify', {
|
||||
session,
|
||||
identity,
|
||||
}, { skipLogoutOnError: true });
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify a factor (primary or secondary)
|
||||
*
|
||||
* @param session - Session ID from init
|
||||
* @param method - Provider/method ID (e.g., 'default', 'totp')
|
||||
* @param response - User's response (password, code, etc.)
|
||||
* @param identity - User identity for credential-based auth (email/username)
|
||||
*/
|
||||
async verify(
|
||||
session: string,
|
||||
method: string,
|
||||
response: string,
|
||||
identity?: string
|
||||
): Promise<VerifyResponse> {
|
||||
return fetchWrapper.post('/auth/verify', {
|
||||
session,
|
||||
method,
|
||||
response,
|
||||
...(identity && { identity }),
|
||||
}, { autoRetry: false, skipLogoutOnError: true });
|
||||
},
|
||||
|
||||
/**
|
||||
* Begin redirect-based authentication (OIDC/SAML)
|
||||
*/
|
||||
async beginRedirect(
|
||||
session: string,
|
||||
method: string,
|
||||
returnUrl: string = '/'
|
||||
): Promise<RedirectResponse> {
|
||||
return fetchWrapper.post('/auth/redirect', {
|
||||
session,
|
||||
method,
|
||||
return_url: returnUrl,
|
||||
}, { skipLogoutOnError: true });
|
||||
},
|
||||
|
||||
/**
|
||||
* Start a challenge for methods that require it (SMS, email, TOTP)
|
||||
*/
|
||||
async beginChallenge(session: string, method: string): Promise<ChallengeResponse> {
|
||||
return fetchWrapper.post('/auth/challenge', {
|
||||
session,
|
||||
method,
|
||||
}, { skipLogoutOnError: true });
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current session status
|
||||
*/
|
||||
async getStatus(session: string): Promise<SessionStatus> {
|
||||
return fetchWrapper.get(`/auth/status?session=${encodeURIComponent(session)}`, undefined, { skipLogoutOnError: true });
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancel authentication session
|
||||
*/
|
||||
async cancelSession(session: string): Promise<void> {
|
||||
await fetchWrapper.delete(`/auth/session?session=${encodeURIComponent(session)}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Refresh access token
|
||||
*/
|
||||
async refresh(): Promise<void> {
|
||||
await fetchWrapper.post('/auth/refresh', {});
|
||||
},
|
||||
|
||||
/**
|
||||
* Logout
|
||||
*/
|
||||
async logout(): Promise<void> {
|
||||
await fetchWrapper.post('/auth/logout', {});
|
||||
},
|
||||
};
|
||||
1
core/src/services/user/index.ts
Normal file
1
core/src/services/user/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { userService } from './userService';
|
||||
153
core/src/services/user/userService.ts
Normal file
153
core/src/services/user/userService.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { fetchWrapper } from '@KTXC/utils/helpers/fetch-wrapper';
|
||||
|
||||
/**
|
||||
* User Service
|
||||
* Provides methods for managing user profile and settings with batched updates
|
||||
*/
|
||||
|
||||
// Pending updates for profile and settings
|
||||
let pendingProfileUpdates: Record<string, any> = {};
|
||||
let pendingSettingsUpdates: Record<string, any> = {};
|
||||
let profileUpdateTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let settingsUpdateTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// Default debounce delay in milliseconds
|
||||
const DEBOUNCE_DELAY = 500;
|
||||
|
||||
export const userService = {
|
||||
/**
|
||||
* Update profile field(s) with debouncing
|
||||
* Multiple calls within the debounce window will be batched into a single request
|
||||
*/
|
||||
updateProfile(fields: Record<string, any>): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Merge new fields with pending updates
|
||||
Object.assign(pendingProfileUpdates, fields);
|
||||
|
||||
// Clear existing timer
|
||||
if (profileUpdateTimer) {
|
||||
clearTimeout(profileUpdateTimer);
|
||||
}
|
||||
|
||||
// Set new timer to batch updates
|
||||
profileUpdateTimer = setTimeout(async () => {
|
||||
const updates = { ...pendingProfileUpdates };
|
||||
pendingProfileUpdates = {};
|
||||
profileUpdateTimer = null;
|
||||
|
||||
try {
|
||||
await fetchWrapper.put('/user/profile', { data: updates });
|
||||
resolve();
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
}, DEBOUNCE_DELAY);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Update profile field(s) immediately without debouncing
|
||||
*/
|
||||
async updateProfileImmediate(fields: Record<string, any>): Promise<void> {
|
||||
// Cancel pending debounced update
|
||||
if (profileUpdateTimer) {
|
||||
clearTimeout(profileUpdateTimer);
|
||||
profileUpdateTimer = null;
|
||||
}
|
||||
|
||||
// Merge with pending updates and send immediately
|
||||
const updates = { ...pendingProfileUpdates, ...fields };
|
||||
pendingProfileUpdates = {};
|
||||
|
||||
return fetchWrapper.put('/user/profile', { data: updates });
|
||||
},
|
||||
|
||||
/**
|
||||
* Flush any pending profile updates immediately
|
||||
*/
|
||||
async flushProfileUpdates(): Promise<void> {
|
||||
if (profileUpdateTimer) {
|
||||
clearTimeout(profileUpdateTimer);
|
||||
profileUpdateTimer = null;
|
||||
}
|
||||
|
||||
if (Object.keys(pendingProfileUpdates).length > 0) {
|
||||
const updates = { ...pendingProfileUpdates };
|
||||
pendingProfileUpdates = {};
|
||||
return fetchWrapper.put('/user/profile', { data: updates });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update setting(s) with debouncing
|
||||
* Multiple calls within the debounce window will be batched into a single request
|
||||
*/
|
||||
updateSettings(settings: Record<string, any>): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Merge new settings with pending updates
|
||||
Object.assign(pendingSettingsUpdates, settings);
|
||||
|
||||
// Clear existing timer
|
||||
if (settingsUpdateTimer) {
|
||||
clearTimeout(settingsUpdateTimer);
|
||||
}
|
||||
|
||||
// Set new timer to batch updates
|
||||
settingsUpdateTimer = setTimeout(async () => {
|
||||
const updates = { ...pendingSettingsUpdates };
|
||||
pendingSettingsUpdates = {};
|
||||
settingsUpdateTimer = null;
|
||||
|
||||
try {
|
||||
await fetchWrapper.put('/user/settings', { data: updates });
|
||||
resolve();
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
}, DEBOUNCE_DELAY);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Update setting(s) immediately without debouncing
|
||||
*/
|
||||
async updateSettingsImmediate(settings: Record<string, any>): Promise<void> {
|
||||
// Cancel pending debounced update
|
||||
if (settingsUpdateTimer) {
|
||||
clearTimeout(settingsUpdateTimer);
|
||||
settingsUpdateTimer = null;
|
||||
}
|
||||
|
||||
// Merge with pending updates and send immediately
|
||||
const updates = { ...pendingSettingsUpdates, ...settings };
|
||||
pendingSettingsUpdates = {};
|
||||
|
||||
return fetchWrapper.put('/user/settings', { data: updates });
|
||||
},
|
||||
|
||||
/**
|
||||
* Flush any pending settings updates immediately
|
||||
*/
|
||||
async flushSettingsUpdates(): Promise<void> {
|
||||
if (settingsUpdateTimer) {
|
||||
clearTimeout(settingsUpdateTimer);
|
||||
settingsUpdateTimer = null;
|
||||
}
|
||||
|
||||
if (Object.keys(pendingSettingsUpdates).length > 0) {
|
||||
const updates = { ...pendingSettingsUpdates };
|
||||
pendingSettingsUpdates = {};
|
||||
return fetchWrapper.put('/user/settings', { data: updates });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Flush all pending updates (profile and settings)
|
||||
*/
|
||||
async flushAll(): Promise<void> {
|
||||
await Promise.all([
|
||||
this.flushProfileUpdates(),
|
||||
this.flushSettingsUpdates(),
|
||||
]);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user