@shokirovr16/frontend-library 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/bin/cmfrt.js +69 -0
- package/package.json +47 -0
- package/src/auth/README.md +193 -0
- package/src/auth/core/AuthEngine.js +623 -0
- package/src/auth/core/OidcClient.js +79 -0
- package/src/auth/core/OidcDiscovery.js +17 -0
- package/src/auth/core/Pkce.js +18 -0
- package/src/auth/events/AuthEventBus.js +22 -0
- package/src/auth/http/authFetch.js +32 -0
- package/src/auth/http/createAuthHttpClient.js +42 -0
- package/src/auth/index.js +90 -0
- package/src/auth/permissions/ClaimsNormalizer.js +69 -0
- package/src/auth/permissions/permissions.js +26 -0
- package/src/auth/react/AuthProvider.js +34 -0
- package/src/auth/react/guards/RequireAuth.js +35 -0
- package/src/auth/react/guards/RequirePermission.js +16 -0
- package/src/auth/react/guards/withAuthGuard.js +12 -0
- package/src/auth/react/hooks/useRequireAuth.js +24 -0
- package/src/auth/react/index.js +6 -0
- package/src/auth/react/useAuth.js +29 -0
- package/src/auth/silent/silentCallback.js +42 -0
- package/src/auth/singleton.js +22 -0
- package/src/auth/storage/InMemoryTokenStore.js +56 -0
- package/src/auth/storage/TransactionStore.js +51 -0
- package/src/auth/sync/BroadcastChannelSync.js +29 -0
- package/src/auth/tenancy/TenantResolver.js +39 -0
- package/src/auth/types.js +113 -0
- package/src/auth/utils/base64url.js +15 -0
- package/src/auth/utils/jwt.js +26 -0
- package/src/auth/utils/random.js +13 -0
- package/src/auth/utils/url.js +27 -0
- package/src/commands/add.js +80 -0
- package/src/commands/init.js +113 -0
- package/src/commands/list.js +92 -0
- package/src/commands/remove.js +150 -0
- package/src/commands/status.js +96 -0
- package/src/commands/theme.js +47 -0
- package/src/commands/uninstall.js +198 -0
- package/src/commands/update.js +151 -0
- package/src/lib/config.js +55 -0
- package/src/lib/fs.js +13 -0
- package/src/lib/packageManager.js +30 -0
- package/src/lib/paths.js +14 -0
- package/src/lib/registry.js +11 -0
- package/src/lib/styles.js +223 -0
- package/src/lib/targets.js +15 -0
- package/src/lib/theme.js +102 -0
- package/templates/docs/cmfrt-doc.md +82 -0
- package/templates/lib/utils.js +6 -0
- package/templates/registry.json +42 -0
- package/templates/styles/theme.cjs +832 -0
- package/templates/styles/type-utilities.css +136 -0
- package/templates/styles/type-utility-classes.css +138 -0
- package/templates/styles/variables.css +1560 -0
- package/templates/styles/variables.json +6870 -0
- package/templates/ui/button.jsx +117 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const DISCOVERY_PATH = '/.well-known/openid-configuration';
|
|
2
|
+
|
|
3
|
+
export async function loadOidcDiscovery(issuer, fetchImpl = fetch) {
|
|
4
|
+
if (!issuer) throw new Error('[cmfrt/auth] issuer is required for OIDC discovery.');
|
|
5
|
+
const url = issuer.replace(/\/+$/g, '') + DISCOVERY_PATH;
|
|
6
|
+
const res = await fetchImpl(url, { headers: { accept: 'application/json' } });
|
|
7
|
+
if (!res.ok) {
|
|
8
|
+
throw new Error(`[cmfrt/auth] OIDC discovery failed: ${res.status} ${res.statusText}`);
|
|
9
|
+
}
|
|
10
|
+
/** @type {any} */
|
|
11
|
+
const json = await res.json();
|
|
12
|
+
if (!json.authorization_endpoint || !json.token_endpoint) {
|
|
13
|
+
throw new Error('[cmfrt/auth] Invalid OIDC discovery document (missing endpoints).');
|
|
14
|
+
}
|
|
15
|
+
return json;
|
|
16
|
+
}
|
|
17
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { uint8ArrayToBase64Url } from '../utils/base64url.js';
|
|
2
|
+
import { randomString } from '../utils/random.js';
|
|
3
|
+
|
|
4
|
+
export function generateCodeVerifier() {
|
|
5
|
+
// RFC 7636 allows 43..128 chars. Use 64.
|
|
6
|
+
return randomString(64);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function codeChallengeS256(codeVerifier) {
|
|
10
|
+
const cryptoObj = globalThis.crypto;
|
|
11
|
+
if (!cryptoObj?.subtle) {
|
|
12
|
+
throw new Error('[cmfrt/auth] crypto.subtle is required for PKCE S256.');
|
|
13
|
+
}
|
|
14
|
+
const data = new TextEncoder().encode(codeVerifier);
|
|
15
|
+
const digest = await cryptoObj.subtle.digest('SHA-256', data);
|
|
16
|
+
return uint8ArrayToBase64Url(new Uint8Array(digest));
|
|
17
|
+
}
|
|
18
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function createAuthEventBus() {
|
|
2
|
+
/** @type {Set<(event:any)=>void>} */
|
|
3
|
+
const listeners = new Set();
|
|
4
|
+
|
|
5
|
+
function emit(event) {
|
|
6
|
+
for (const listener of listeners) {
|
|
7
|
+
try {
|
|
8
|
+
listener(event);
|
|
9
|
+
} catch {
|
|
10
|
+
// best-effort
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function subscribe(listener) {
|
|
16
|
+
listeners.add(listener);
|
|
17
|
+
return () => listeners.delete(listener);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return { emit, subscribe };
|
|
21
|
+
}
|
|
22
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getAuth } from '../singleton.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fetch wrapper that:
|
|
5
|
+
* - adds Authorization header
|
|
6
|
+
* - ensures fresh token (single-flight via engine)
|
|
7
|
+
* - retries once on 401 after refresh
|
|
8
|
+
*
|
|
9
|
+
* @param {RequestInfo|URL} input
|
|
10
|
+
* @param {RequestInit} [init]
|
|
11
|
+
* @param {{ engine?: any, minValiditySec?: number, retryOn401?: boolean }} [options]
|
|
12
|
+
*/
|
|
13
|
+
export async function authFetch(input, init, options) {
|
|
14
|
+
const engine = options?.engine || getAuth();
|
|
15
|
+
const retryOn401 = options?.retryOn401 ?? true;
|
|
16
|
+
const minValiditySec = options?.minValiditySec;
|
|
17
|
+
|
|
18
|
+
const doFetch = async (isRetry) => {
|
|
19
|
+
await engine.ensureFreshToken(minValiditySec);
|
|
20
|
+
const token = engine.getAccessToken();
|
|
21
|
+
const headers = new Headers(init?.headers || {});
|
|
22
|
+
if (token) headers.set('authorization', `Bearer ${token}`);
|
|
23
|
+
const res = await fetch(input, { ...init, headers });
|
|
24
|
+
if (res.status === 401 && retryOn401 && !isRetry) {
|
|
25
|
+
await engine.refresh();
|
|
26
|
+
return doFetch(true);
|
|
27
|
+
}
|
|
28
|
+
return res;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return doFetch(false);
|
|
32
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Axios integration without importing axios:
|
|
3
|
+
* user passes an axios instance.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function createAuthHttpClient({ axios, engine, minValiditySec = 30, retryOn401 = true }) {
|
|
7
|
+
if (!axios) throw new Error('[cmfrt/auth] axios instance is required.');
|
|
8
|
+
if (!engine) throw new Error('[cmfrt/auth] engine is required.');
|
|
9
|
+
|
|
10
|
+
const instance = axios.create ? axios.create() : axios;
|
|
11
|
+
|
|
12
|
+
instance.interceptors.request.use(async (config) => {
|
|
13
|
+
await engine.ensureFreshToken(minValiditySec);
|
|
14
|
+
const token = engine.getAccessToken();
|
|
15
|
+
if (token) {
|
|
16
|
+
config.headers = config.headers || {};
|
|
17
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
18
|
+
}
|
|
19
|
+
return config;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
instance.interceptors.response.use(
|
|
23
|
+
(res) => res,
|
|
24
|
+
async (error) => {
|
|
25
|
+
const status = error?.response?.status;
|
|
26
|
+
const original = error?.config;
|
|
27
|
+
if (!retryOn401 || status !== 401 || !original || original.__cmfrtAuthRetry) throw error;
|
|
28
|
+
|
|
29
|
+
original.__cmfrtAuthRetry = true;
|
|
30
|
+
await engine.refresh();
|
|
31
|
+
const token = engine.getAccessToken();
|
|
32
|
+
if (token) {
|
|
33
|
+
original.headers = original.headers || {};
|
|
34
|
+
original.headers.Authorization = `Bearer ${token}`;
|
|
35
|
+
}
|
|
36
|
+
return instance(original);
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return instance;
|
|
41
|
+
}
|
|
42
|
+
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export { createAuthEngine } from './core/AuthEngine.js';
|
|
2
|
+
export { createDefaultTenantResolver } from './tenancy/TenantResolver.js';
|
|
3
|
+
export { createAuthEventBus } from './events/AuthEventBus.js';
|
|
4
|
+
|
|
5
|
+
export { initializeAuth, getAuth } from './singleton.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {import('./types.js').AuthRuntimeConfig} AuthRuntimeConfig
|
|
9
|
+
* @typedef {import('./types.js').AuthEngine} AuthEngine
|
|
10
|
+
* @typedef {import('./types.js').InitializeAuthOptions} InitializeAuthOptions
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { getAuth as _getAuth } from './singleton.js';
|
|
14
|
+
|
|
15
|
+
export function subscribeAuthEvents(listener) {
|
|
16
|
+
return _getAuth().subscribeAuthEvents(listener);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getUser() {
|
|
20
|
+
return _getAuth().getUser();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getAccessToken() {
|
|
24
|
+
return _getAuth().getAccessToken();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getIdToken() {
|
|
28
|
+
return _getAuth().getIdToken();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isAuthenticated() {
|
|
32
|
+
return _getAuth().isAuthenticated();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function login(params) {
|
|
36
|
+
return _getAuth().login(params);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function logout(params) {
|
|
40
|
+
return _getAuth().logout(params);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function refresh(params) {
|
|
44
|
+
return _getAuth().refresh(params);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function ensureFreshToken(minValiditySec) {
|
|
48
|
+
return _getAuth().ensureFreshToken(minValiditySec);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function handleCallback(params) {
|
|
52
|
+
return _getAuth().handleCallback(params);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function handleSilentCallbackMessage(event) {
|
|
56
|
+
return _getAuth().handleSilentCallbackMessage(event);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function clearSession(reason) {
|
|
60
|
+
return _getAuth().clearSession(reason);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function setTenant(tenant) {
|
|
64
|
+
return _getAuth().setTenant(tenant);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function switchTenant(tenant, opts) {
|
|
68
|
+
return _getAuth().switchTenant(tenant, opts);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function hasRole(role) {
|
|
72
|
+
return _getAuth().hasRole(role);
|
|
73
|
+
}
|
|
74
|
+
export function hasAnyRole(roles) {
|
|
75
|
+
return _getAuth().hasAnyRole(roles);
|
|
76
|
+
}
|
|
77
|
+
export function hasAllRoles(roles) {
|
|
78
|
+
return _getAuth().hasAllRoles(roles);
|
|
79
|
+
}
|
|
80
|
+
export function hasPermission(permission) {
|
|
81
|
+
return _getAuth().hasPermission(permission);
|
|
82
|
+
}
|
|
83
|
+
export function can(permission) {
|
|
84
|
+
return _getAuth().can(permission);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { authFetch } from './http/authFetch.js';
|
|
88
|
+
export { createAuthHttpClient } from './http/createAuthHttpClient.js';
|
|
89
|
+
|
|
90
|
+
export { createSilentCheckSsoCallback } from './silent/silentCallback.js';
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
function getByPath(obj, path) {
|
|
2
|
+
if (!obj || !path) return undefined;
|
|
3
|
+
const parts = [];
|
|
4
|
+
// Supports `a.b[0].c` and `resource_access[{clientId}].roles`
|
|
5
|
+
const re = /([^[.\]]+)|\[(.*?)\]/g;
|
|
6
|
+
let m;
|
|
7
|
+
while ((m = re.exec(path))) {
|
|
8
|
+
const part = m[1] ?? m[2];
|
|
9
|
+
if (part === '') continue;
|
|
10
|
+
parts.push(part);
|
|
11
|
+
}
|
|
12
|
+
let cur = obj;
|
|
13
|
+
for (const part of parts) {
|
|
14
|
+
if (cur == null) return undefined;
|
|
15
|
+
cur = cur[part];
|
|
16
|
+
}
|
|
17
|
+
return cur;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function uniq(arr) {
|
|
21
|
+
return Array.from(new Set((arr || []).filter(Boolean)));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function normalizeClaims({ accessClaims, idClaims, clientId, claimsPolicy }) {
|
|
25
|
+
const rolePaths = claimsPolicy?.roleClaimPaths || [
|
|
26
|
+
'realm_access.roles',
|
|
27
|
+
'resource_access[{clientId}].roles',
|
|
28
|
+
];
|
|
29
|
+
const permissionPaths = claimsPolicy?.permissionClaimPaths || [];
|
|
30
|
+
const profilePaths = claimsPolicy?.profileClaimPaths || {
|
|
31
|
+
sub: 'sub',
|
|
32
|
+
name: 'name',
|
|
33
|
+
email: 'email',
|
|
34
|
+
username: 'preferred_username',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const ctx = { clientId };
|
|
38
|
+
const replaceVars = (p) => p.replaceAll('{clientId}', String(ctx.clientId));
|
|
39
|
+
|
|
40
|
+
const roles = [];
|
|
41
|
+
for (const p of rolePaths) {
|
|
42
|
+
const v = getByPath(accessClaims, replaceVars(p));
|
|
43
|
+
if (Array.isArray(v)) roles.push(...v);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const permissions = [];
|
|
47
|
+
for (const p of permissionPaths) {
|
|
48
|
+
const v = getByPath(accessClaims, replaceVars(p));
|
|
49
|
+
if (Array.isArray(v)) permissions.push(...v);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** @type {any} */
|
|
53
|
+
const profile = {};
|
|
54
|
+
for (const [k, p] of Object.entries(profilePaths)) {
|
|
55
|
+
const v = getByPath(idClaims || accessClaims, replaceVars(p));
|
|
56
|
+
if (v != null) profile[k] = v;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
...profile,
|
|
61
|
+
roles: uniq(roles),
|
|
62
|
+
permissions: uniq(permissions),
|
|
63
|
+
claims: {
|
|
64
|
+
access: accessClaims || null,
|
|
65
|
+
id: idClaims || null,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function hasRole(user, role) {
|
|
2
|
+
if (!user || !role) return false;
|
|
3
|
+
return user.roles?.includes(role) || false;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function hasAnyRole(user, roles) {
|
|
7
|
+
if (!user) return false;
|
|
8
|
+
for (const role of roles || []) if (user.roles?.includes(role)) return true;
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function hasAllRoles(user, roles) {
|
|
13
|
+
if (!user) return false;
|
|
14
|
+
for (const role of roles || []) if (!user.roles?.includes(role)) return false;
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function hasPermission(user, permission) {
|
|
19
|
+
if (!user || !permission) return false;
|
|
20
|
+
return user.permissions?.includes(permission) || false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function can(user, permission) {
|
|
24
|
+
return hasPermission(user, permission);
|
|
25
|
+
}
|
|
26
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { getAuth } from '../singleton.js';
|
|
3
|
+
|
|
4
|
+
const AuthContext = createContext(null);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {{
|
|
8
|
+
* engine?: any,
|
|
9
|
+
* children: React.ReactNode,
|
|
10
|
+
* loading?: React.ReactNode,
|
|
11
|
+
* onAuthEvent?: (event:any)=>void
|
|
12
|
+
* }} props
|
|
13
|
+
*/
|
|
14
|
+
export function AuthProvider({ engine, children, loading = null, onAuthEvent }) {
|
|
15
|
+
const auth = engine || getAuth();
|
|
16
|
+
const [snapshot, setSnapshot] = useState(() => auth.getSnapshot());
|
|
17
|
+
|
|
18
|
+
useEffect(() => auth.subscribe(setSnapshot), [auth]);
|
|
19
|
+
useEffect(() => (onAuthEvent ? auth.subscribeAuthEvents(onAuthEvent) : undefined), [auth, onAuthEvent]);
|
|
20
|
+
|
|
21
|
+
const value = useMemo(() => ({ engine: auth, snapshot }), [auth, snapshot]);
|
|
22
|
+
|
|
23
|
+
if (snapshot.status === 'loading_config' || snapshot.status === 'discovering' || snapshot.status === 'idle') {
|
|
24
|
+
return loading;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return React.createElement(AuthContext.Provider, { value }, children);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useAuthContext() {
|
|
31
|
+
const ctx = useContext(AuthContext);
|
|
32
|
+
if (!ctx) throw new Error('[cmfrt/auth] AuthProvider is missing.');
|
|
33
|
+
return ctx;
|
|
34
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { useLocation } from 'react-router-dom';
|
|
3
|
+
import { useAuth } from '../useAuth.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {{
|
|
7
|
+
* children: React.ReactNode,
|
|
8
|
+
* fallback?: React.ReactNode,
|
|
9
|
+
* guestOnly?: boolean
|
|
10
|
+
* }} props
|
|
11
|
+
*/
|
|
12
|
+
export function RequireAuth({ children, fallback = null, guestOnly = false }) {
|
|
13
|
+
const auth = useAuth();
|
|
14
|
+
const location = useLocation();
|
|
15
|
+
|
|
16
|
+
const returnTo = location.pathname + location.search + location.hash;
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (guestOnly) return;
|
|
20
|
+
if (auth.status === 'authenticated') return;
|
|
21
|
+
if (auth.status === 'authenticating') return;
|
|
22
|
+
if (auth.status === 'refreshing') return;
|
|
23
|
+
if (auth.status === 'ready' || auth.status === 'logged_out') {
|
|
24
|
+
auth.login({ returnTo });
|
|
25
|
+
}
|
|
26
|
+
}, [guestOnly, auth.status, returnTo]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
27
|
+
|
|
28
|
+
if (guestOnly) {
|
|
29
|
+
if (auth.status === 'authenticated') return fallback;
|
|
30
|
+
return children;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (auth.status === 'authenticated') return children;
|
|
34
|
+
return fallback;
|
|
35
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useAuth } from '../useAuth.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {{
|
|
6
|
+
* permission: string,
|
|
7
|
+
* children: React.ReactNode,
|
|
8
|
+
* fallback?: React.ReactNode
|
|
9
|
+
* }} props
|
|
10
|
+
*/
|
|
11
|
+
export function RequirePermission({ permission, children, fallback = null }) {
|
|
12
|
+
const auth = useAuth();
|
|
13
|
+
if (auth.status !== 'authenticated') return fallback;
|
|
14
|
+
if (auth.can(permission)) return children;
|
|
15
|
+
return fallback;
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { RequireAuth } from './RequireAuth.js';
|
|
3
|
+
|
|
4
|
+
export function withAuthGuard(Component, { fallback = null } = {}) {
|
|
5
|
+
return function WithAuthGuard(props) {
|
|
6
|
+
return React.createElement(
|
|
7
|
+
RequireAuth,
|
|
8
|
+
{ fallback },
|
|
9
|
+
React.createElement(Component, { ...props })
|
|
10
|
+
);
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useAuth } from '../useAuth.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook version of route-guard: triggers login when unauthenticated.
|
|
6
|
+
* @param {{ returnTo?: string, enabled?: boolean }} [opts]
|
|
7
|
+
*/
|
|
8
|
+
export function useRequireAuth(opts) {
|
|
9
|
+
const auth = useAuth();
|
|
10
|
+
const enabled = opts?.enabled ?? true;
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!enabled) return;
|
|
14
|
+
if (auth.status === 'authenticated') return;
|
|
15
|
+
if (auth.status === 'authenticating') return;
|
|
16
|
+
if (auth.status === 'refreshing') return;
|
|
17
|
+
if (auth.status === 'ready' || auth.status === 'logged_out') {
|
|
18
|
+
auth.login({ returnTo: opts?.returnTo });
|
|
19
|
+
}
|
|
20
|
+
}, [enabled, auth.status]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
21
|
+
|
|
22
|
+
return auth;
|
|
23
|
+
}
|
|
24
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { AuthProvider } from './AuthProvider.js';
|
|
2
|
+
export { useAuth } from './useAuth.js';
|
|
3
|
+
export { RequireAuth } from './guards/RequireAuth.js';
|
|
4
|
+
export { RequirePermission } from './guards/RequirePermission.js';
|
|
5
|
+
export { withAuthGuard } from './guards/withAuthGuard.js';
|
|
6
|
+
export { useRequireAuth } from './hooks/useRequireAuth.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useAuthContext } from './AuthProvider.js';
|
|
3
|
+
|
|
4
|
+
export function useAuth() {
|
|
5
|
+
const { engine, snapshot } = useAuthContext();
|
|
6
|
+
|
|
7
|
+
return useMemo(
|
|
8
|
+
() => ({
|
|
9
|
+
...snapshot,
|
|
10
|
+
engine,
|
|
11
|
+
login: engine.login,
|
|
12
|
+
logout: engine.logout,
|
|
13
|
+
handleCallback: engine.handleCallback,
|
|
14
|
+
ensureFreshToken: engine.ensureFreshToken,
|
|
15
|
+
refresh: engine.refresh,
|
|
16
|
+
getAccessToken: engine.getAccessToken,
|
|
17
|
+
getIdToken: engine.getIdToken,
|
|
18
|
+
getUser: engine.getUser,
|
|
19
|
+
hasRole: engine.hasRole,
|
|
20
|
+
hasAnyRole: engine.hasAnyRole,
|
|
21
|
+
hasAllRoles: engine.hasAllRoles,
|
|
22
|
+
hasPermission: engine.hasPermission,
|
|
23
|
+
can: engine.can,
|
|
24
|
+
switchTenant: engine.switchTenant,
|
|
25
|
+
setTenant: engine.setTenant,
|
|
26
|
+
}),
|
|
27
|
+
[engine, snapshot]
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper for the silent-check-sso redirect page (same-origin recommended).
|
|
3
|
+
*
|
|
4
|
+
* Usage (in your silent callback page/app route):
|
|
5
|
+
* import { createSilentCheckSsoCallback } from 'cmfrt/auth';
|
|
6
|
+
* createSilentCheckSsoCallback();
|
|
7
|
+
*
|
|
8
|
+
* It forwards the OIDC response (code/state/error) to the parent window via postMessage.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { parseUrlParams, stripQueryParams } from '../utils/url.js';
|
|
12
|
+
|
|
13
|
+
export function createSilentCheckSsoCallback({
|
|
14
|
+
messageType = 'CMFRT_OIDC_SILENT_CALLBACK',
|
|
15
|
+
targetOrigin = '*',
|
|
16
|
+
stripParams = true,
|
|
17
|
+
} = {}) {
|
|
18
|
+
if (typeof window === 'undefined') return;
|
|
19
|
+
|
|
20
|
+
const params = parseUrlParams(window.location.href);
|
|
21
|
+
const payload = {
|
|
22
|
+
type: messageType,
|
|
23
|
+
url: window.location.href,
|
|
24
|
+
params,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
window.parent?.postMessage(payload, targetOrigin);
|
|
29
|
+
} catch {
|
|
30
|
+
// ignore
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (stripParams) {
|
|
34
|
+
try {
|
|
35
|
+
const clean = stripQueryParams(window.location.href, ['code', 'state', 'error', 'error_description', 'session_state']);
|
|
36
|
+
window.history.replaceState({}, '', clean);
|
|
37
|
+
} catch {
|
|
38
|
+
// ignore
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createAuthEngine } from './core/AuthEngine.js';
|
|
2
|
+
|
|
3
|
+
let _defaultEngine = null;
|
|
4
|
+
|
|
5
|
+
export function initializeAuth(options) {
|
|
6
|
+
_defaultEngine = createAuthEngine(options);
|
|
7
|
+
return _defaultEngine;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getAuth() {
|
|
11
|
+
if (!_defaultEngine) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
'[cmfrt/auth] Default auth engine is not initialized. Call initializeAuth(...) first or pass `engine` to AuthProvider.'
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
return _defaultEngine;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function _setDefaultAuthEngine(engine) {
|
|
20
|
+
_defaultEngine = engine;
|
|
21
|
+
}
|
|
22
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { decodeJwt, getJwtExp } from '../utils/jwt.js';
|
|
2
|
+
|
|
3
|
+
export function createInMemoryTokenStore() {
|
|
4
|
+
/** @type {any} */
|
|
5
|
+
let tokens = null;
|
|
6
|
+
/** @type {any} */
|
|
7
|
+
let claims = null;
|
|
8
|
+
|
|
9
|
+
function set(next) {
|
|
10
|
+
tokens = next || null;
|
|
11
|
+
const accessClaims = next?.access_token ? decodeJwt(next.access_token) : null;
|
|
12
|
+
const idClaims = next?.id_token ? decodeJwt(next.id_token) : null;
|
|
13
|
+
claims = { access: accessClaims, id: idClaims };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function clear() {
|
|
17
|
+
tokens = null;
|
|
18
|
+
claims = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function get() {
|
|
22
|
+
return tokens;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getClaims() {
|
|
26
|
+
return claims;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getAccessToken() {
|
|
30
|
+
return tokens?.access_token || null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getIdToken() {
|
|
34
|
+
return tokens?.id_token || null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getRefreshToken() {
|
|
38
|
+
return tokens?.refresh_token || null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getAccessTokenExpSec() {
|
|
42
|
+
return getJwtExp(getAccessToken());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
set,
|
|
47
|
+
clear,
|
|
48
|
+
get,
|
|
49
|
+
getClaims,
|
|
50
|
+
getAccessToken,
|
|
51
|
+
getIdToken,
|
|
52
|
+
getRefreshToken,
|
|
53
|
+
getAccessTokenExpSec,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
function safeSessionStorage() {
|
|
2
|
+
try {
|
|
3
|
+
if (typeof window === 'undefined') return null;
|
|
4
|
+
return window.sessionStorage;
|
|
5
|
+
} catch {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createTransactionStore({ namespace }) {
|
|
11
|
+
const storage = safeSessionStorage();
|
|
12
|
+
const prefix = namespace ? `${namespace}.` : 'cmfrt.auth.txn.';
|
|
13
|
+
|
|
14
|
+
function key(k) {
|
|
15
|
+
return prefix + k;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function set(k, v) {
|
|
19
|
+
if (!storage) return;
|
|
20
|
+
storage.setItem(key(k), JSON.stringify(v));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function get(k) {
|
|
24
|
+
if (!storage) return null;
|
|
25
|
+
const raw = storage.getItem(key(k));
|
|
26
|
+
if (!raw) return null;
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(raw);
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function remove(k) {
|
|
35
|
+
if (!storage) return;
|
|
36
|
+
storage.removeItem(key(k));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function clearAll() {
|
|
40
|
+
if (!storage) return;
|
|
41
|
+
const keys = [];
|
|
42
|
+
for (let i = 0; i < storage.length; i++) {
|
|
43
|
+
const k = storage.key(i);
|
|
44
|
+
if (k && k.startsWith(prefix)) keys.push(k);
|
|
45
|
+
}
|
|
46
|
+
for (const k of keys) storage.removeItem(k);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { set, get, remove, clearAll };
|
|
50
|
+
}
|
|
51
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function createBroadcastChannelSync({ name, onMessage }) {
|
|
2
|
+
if (typeof window === 'undefined' || typeof BroadcastChannel === 'undefined') {
|
|
3
|
+
return {
|
|
4
|
+
post() {},
|
|
5
|
+
close() {},
|
|
6
|
+
isSupported: false,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const channel = new BroadcastChannel(name);
|
|
11
|
+
channel.addEventListener('message', (ev) => {
|
|
12
|
+
try {
|
|
13
|
+
onMessage?.(ev.data);
|
|
14
|
+
} catch {
|
|
15
|
+
// best-effort
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function post(message) {
|
|
20
|
+
channel.postMessage(message);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function close() {
|
|
24
|
+
channel.close();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return { post, close, isSupported: true };
|
|
28
|
+
}
|
|
29
|
+
|