mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-10-04 21:52:03 +00:00
BLOCK-10: Add CloudWatch alarms (DLQ, Lambda errors, RDS, API 5xx) with SNS email BLOCK-11: Remove sync-over-async deadlock in CurrentUserService BLOCK-12: Add AppDelegate OAuth URL callback handler for mobile BLOCK-13: Wire mobile 401 interceptor to dispatch Redux logout BLOCK-14: Fix JWT base64 padding crash and SysAdmin role detection BLOCK-15: Reset pagination to page 1 on filter change BLOCK-16: Add unsaved-changes guard (beforeunload + useBlocker) to AdminWorkspace FIX-08: Add BulkUpdateLineItems FluentValidation validator FIX-13: Display auth errors on LoginPage FIX-25: Add token refresh with retry queue to mobile API client FIX-44: Add httpx retry logic to all Lambda handlers FIX-42/43: Align docker-compose PG version (15) and DB name (proposals) with RDS
124 lines
3.6 KiB
TypeScript
124 lines
3.6 KiB
TypeScript
import { authorize, refresh, revoke } from 'react-native-app-auth';
|
|
import Config from '../../config';
|
|
import apiClient, { registerTokenRefresh } from './client';
|
|
import { authenticateWithCredentials } from './cognito-auth';
|
|
import {
|
|
tokenStorage,
|
|
userStorage,
|
|
StoredTokens,
|
|
StoredUser,
|
|
} from '../storage';
|
|
|
|
function parseUserFromIdToken(idToken: string): StoredUser {
|
|
const base64Url = idToken.split('.')[1];
|
|
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
|
const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');
|
|
const payload = JSON.parse(atob(padded));
|
|
const groups: string[] = payload['cognito:groups'] || [];
|
|
const role = groups.includes('sysadmins')
|
|
? 'SysAdmin'
|
|
: groups.includes('admins')
|
|
? 'Admin'
|
|
: 'Dispatcher';
|
|
return {
|
|
id: payload.sub,
|
|
email: payload.email,
|
|
displayName: payload.name || payload.email,
|
|
role: role as StoredUser['role'],
|
|
};
|
|
}
|
|
|
|
const cognitoConfig = {
|
|
clientId: Config.COGNITO_CLIENT_ID,
|
|
redirectUrl: Config.COGNITO_REDIRECT_URI,
|
|
scopes: Config.COGNITO_SCOPES,
|
|
serviceConfiguration: {
|
|
authorizationEndpoint: `https://${Config.COGNITO_DOMAIN}/oauth2/authorize`,
|
|
tokenEndpoint: `https://${Config.COGNITO_DOMAIN}/oauth2/token`,
|
|
revocationEndpoint: `https://${Config.COGNITO_DOMAIN}/oauth2/revoke`,
|
|
},
|
|
usePKCE: true,
|
|
iosPrefersEphemeralSession: false,
|
|
};
|
|
|
|
export const authApi = {
|
|
login: async (): Promise<StoredUser> => {
|
|
const result = await authorize(cognitoConfig);
|
|
|
|
const tokens: StoredTokens = {
|
|
accessToken: result.accessToken,
|
|
idToken: result.idToken,
|
|
refreshToken: result.refreshToken,
|
|
expiresAt: result.accessTokenExpirationDate,
|
|
};
|
|
await tokenStorage.save(tokens);
|
|
|
|
const profile = await authApi.getMe();
|
|
await userStorage.save(profile);
|
|
return profile;
|
|
},
|
|
|
|
loginWithCredentials: async (
|
|
email: string,
|
|
password: string,
|
|
): Promise<StoredUser> => {
|
|
const cognitoTokens = await authenticateWithCredentials(email, password);
|
|
|
|
const tokens: StoredTokens = {
|
|
accessToken: cognitoTokens.accessToken,
|
|
idToken: cognitoTokens.idToken,
|
|
refreshToken: cognitoTokens.refreshToken,
|
|
expiresAt: cognitoTokens.expiresAt,
|
|
};
|
|
await tokenStorage.save(tokens);
|
|
|
|
let profile: StoredUser;
|
|
try {
|
|
profile = await authApi.getMe();
|
|
} catch {
|
|
profile = parseUserFromIdToken(cognitoTokens.idToken);
|
|
}
|
|
await userStorage.save(profile);
|
|
return profile;
|
|
},
|
|
|
|
refreshTokens: async (): Promise<void> => {
|
|
const stored = await tokenStorage.get();
|
|
if (!stored?.refreshToken) throw new Error('No refresh token');
|
|
|
|
const result = await refresh(cognitoConfig, {
|
|
refreshToken: stored.refreshToken,
|
|
});
|
|
|
|
await tokenStorage.save({
|
|
accessToken: result.accessToken,
|
|
idToken: result.idToken || stored.idToken,
|
|
refreshToken: result.refreshToken || stored.refreshToken,
|
|
expiresAt: result.accessTokenExpirationDate,
|
|
});
|
|
},
|
|
|
|
logout: async (): Promise<void> => {
|
|
const stored = await tokenStorage.get();
|
|
if (stored?.accessToken) {
|
|
try {
|
|
await revoke(cognitoConfig, {
|
|
tokenToRevoke: stored.accessToken,
|
|
includeBasicAuth: false,
|
|
sendClientId: true,
|
|
});
|
|
} catch {
|
|
// Best-effort revocation
|
|
}
|
|
}
|
|
await tokenStorage.clear();
|
|
await userStorage.clear();
|
|
},
|
|
|
|
getMe: async (): Promise<StoredUser> => {
|
|
const res = await apiClient.get('/users/me');
|
|
return res.data;
|
|
},
|
|
};
|
|
|
|
registerTokenRefresh(() => authApi.refreshTokens());
|