@phila/sso-react 0.0.2 → 0.0.4
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/README.md +153 -0
- package/dist/index.js +115 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +94 -74
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# @phila/sso-react
|
|
2
|
+
|
|
3
|
+
React adapter for `@phila/sso-core`. Provides context providers and hooks for Azure AD B2C authentication.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @phila/sso-react @phila/sso-core @azure/msal-browser
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start (Vite + B2C)
|
|
12
|
+
|
|
13
|
+
The `B2CSSOProvider` reads `VITE_SSO_*` environment variables automatically:
|
|
14
|
+
|
|
15
|
+
```env
|
|
16
|
+
VITE_SSO_CLIENT_ID=your-client-id
|
|
17
|
+
VITE_SSO_TENANT=YourTenant
|
|
18
|
+
VITE_SSO_AUTHORITY_DOMAIN=YourTenant.b2clogin.com
|
|
19
|
+
VITE_SSO_REDIRECT_URI=http://localhost:3000
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
// App.tsx
|
|
24
|
+
import { B2CSSOProvider, useAuth } from "@phila/sso-react";
|
|
25
|
+
|
|
26
|
+
function AuthContent() {
|
|
27
|
+
const { isAuthenticated, userName, authReady, signIn, signOut } = useAuth();
|
|
28
|
+
|
|
29
|
+
if (!authReady) return <div>Loading...</div>;
|
|
30
|
+
|
|
31
|
+
if (isAuthenticated) {
|
|
32
|
+
return (
|
|
33
|
+
<div>
|
|
34
|
+
<p>Welcome, {userName}</p>
|
|
35
|
+
<button onClick={() => signOut()}>Sign out</button>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return <button onClick={() => signIn()}>Sign in</button>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default function App() {
|
|
44
|
+
return (
|
|
45
|
+
<B2CSSOProvider>
|
|
46
|
+
<AuthContent />
|
|
47
|
+
</B2CSSOProvider>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Advanced Setup
|
|
53
|
+
|
|
54
|
+
For full control over the provider configuration:
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { SSOProvider } from "@phila/sso-react";
|
|
58
|
+
import { B2CProvider } from "@phila/sso-core";
|
|
59
|
+
|
|
60
|
+
function App() {
|
|
61
|
+
return (
|
|
62
|
+
<SSOProvider
|
|
63
|
+
config={{
|
|
64
|
+
provider: new B2CProvider({
|
|
65
|
+
clientId: "your-client-id",
|
|
66
|
+
b2cEnvironment: "YourTenant",
|
|
67
|
+
authorityDomain: "YourTenant.b2clogin.com",
|
|
68
|
+
redirectUri: "http://localhost:3000",
|
|
69
|
+
apiScopes: ["https://YourTenant.onmicrosoft.com/api/read"],
|
|
70
|
+
policies: {
|
|
71
|
+
signUpSignIn: "B2C_1A_SIGNUP_SIGNIN",
|
|
72
|
+
signInOnly: "B2C_1A_AD_SIGNIN_ONLY",
|
|
73
|
+
resetPassword: "B2C_1A_PASSWORDRESET",
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
debug: true,
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
<AuthContent />
|
|
80
|
+
</SSOProvider>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### `SSOProvider` Props
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
interface SSOProviderProps {
|
|
89
|
+
config: SSOClientConfig;
|
|
90
|
+
children: ReactNode;
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### `B2CSSOProvider` Props
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
interface B2CSSOProviderProps {
|
|
98
|
+
children: ReactNode;
|
|
99
|
+
signInPolicy?: string; // default: "B2C_1A_AD_SIGNIN_ONLY"
|
|
100
|
+
resetPasswordPolicy?: string; // default: "B2C_1A_PASSWORDRESET"
|
|
101
|
+
debug?: boolean; // default: import.meta.env.DEV
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Hooks
|
|
106
|
+
|
|
107
|
+
### `useAuth()`
|
|
108
|
+
|
|
109
|
+
Primary hook for authentication. Must be called within an `SSOProvider` or `B2CSSOProvider`.
|
|
110
|
+
|
|
111
|
+
**State:**
|
|
112
|
+
|
|
113
|
+
| Property | Type | Description |
|
|
114
|
+
| ----------------- | --------------------- | ------------------------------ |
|
|
115
|
+
| `isAuthenticated` | `boolean` | User is signed in |
|
|
116
|
+
| `isLoading` | `boolean` | Auth operation in progress |
|
|
117
|
+
| `user` | `AccountInfo \| null` | MSAL account info |
|
|
118
|
+
| `token` | `string \| null` | Current access token |
|
|
119
|
+
| `error` | `Error \| null` | Last auth error |
|
|
120
|
+
| `activePolicy` | `string \| null` | Active B2C policy |
|
|
121
|
+
| `authReady` | `boolean` | Initialization complete |
|
|
122
|
+
| `userName` | `string \| null` | Display name from token claims |
|
|
123
|
+
|
|
124
|
+
**Actions:**
|
|
125
|
+
|
|
126
|
+
| Method | Returns | Description |
|
|
127
|
+
| ------------------------------ | ------------------------- | -------------------------------- |
|
|
128
|
+
| `signIn(options?)` | `Promise<void>` | Start sign-in flow |
|
|
129
|
+
| `signInCityEmployee(options?)` | `Promise<void>` | Sign in with sign-in-only policy |
|
|
130
|
+
| `signOut(options?)` | `Promise<void>` | Sign out |
|
|
131
|
+
| `forgotPassword()` | `Promise<void>` | Start password reset |
|
|
132
|
+
| `acquireToken(options?)` | `Promise<string \| null>` | Get access token |
|
|
133
|
+
|
|
134
|
+
**Utilities:**
|
|
135
|
+
|
|
136
|
+
| Method | Returns | Description |
|
|
137
|
+
| --------------- | --------- | ------------------------------------------------------- |
|
|
138
|
+
| `hasRole(role)` | `boolean` | Check user role from `roles` or `extension_Roles` claim |
|
|
139
|
+
|
|
140
|
+
### `useSSOClient()`
|
|
141
|
+
|
|
142
|
+
Returns the raw `SSOClient` instance for advanced use cases.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
const client = useSSOClient();
|
|
146
|
+
client.events.on("auth:tokenAcquired", token => {
|
|
147
|
+
/* ... */
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,116 @@
|
|
|
1
|
-
"use strict";
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const jsxRuntime = require("react/jsx-runtime");
|
|
4
|
+
const react = require("react");
|
|
5
|
+
const ssoCore = require("@phila/sso-core");
|
|
6
|
+
const INITIAL_STATE = {
|
|
7
|
+
isAuthenticated: false,
|
|
8
|
+
isLoading: true,
|
|
9
|
+
user: null,
|
|
10
|
+
token: null,
|
|
11
|
+
error: null,
|
|
12
|
+
activePolicy: null,
|
|
13
|
+
authReady: false
|
|
14
|
+
};
|
|
15
|
+
const SSOContext = react.createContext(null);
|
|
16
|
+
function SSOProvider({ config, children }) {
|
|
17
|
+
const clientRef = react.useRef(null);
|
|
18
|
+
const [state, setState] = react.useState(INITIAL_STATE);
|
|
19
|
+
if (!clientRef.current) {
|
|
20
|
+
clientRef.current = new ssoCore.SSOClient(config);
|
|
21
|
+
}
|
|
22
|
+
const client = clientRef.current;
|
|
23
|
+
react.useEffect(() => {
|
|
24
|
+
const unsubscribe = client.events.on("auth:stateChanged", (newState) => {
|
|
25
|
+
setState(newState);
|
|
26
|
+
});
|
|
27
|
+
client.initialize();
|
|
28
|
+
return () => {
|
|
29
|
+
unsubscribe();
|
|
30
|
+
client.destroy();
|
|
31
|
+
};
|
|
32
|
+
}, [client]);
|
|
33
|
+
return /* @__PURE__ */ jsxRuntime.jsx(SSOContext.Provider, { value: { client, state }, children });
|
|
34
|
+
}
|
|
35
|
+
function useAuth() {
|
|
36
|
+
const context = react.useContext(SSOContext);
|
|
37
|
+
if (!context) {
|
|
38
|
+
throw new Error("useAuth must be used within an SSOProvider");
|
|
39
|
+
}
|
|
40
|
+
const { client, state } = context;
|
|
41
|
+
const signIn = react.useCallback((options) => client.signIn(options), [client]);
|
|
42
|
+
const signInCityEmployee = react.useCallback((options) => client.signInCityEmployee(options), [client]);
|
|
43
|
+
const signOut = react.useCallback((options) => client.signOut(options), [client]);
|
|
44
|
+
const forgotPassword = react.useCallback(() => client.forgotPassword(), [client]);
|
|
45
|
+
const acquireToken = react.useCallback((options) => client.acquireToken(options), [client]);
|
|
46
|
+
const hasRole = react.useCallback(
|
|
47
|
+
(role) => {
|
|
48
|
+
const claims = state.user?.idTokenClaims;
|
|
49
|
+
if (!claims) return false;
|
|
50
|
+
const roles = claims.roles ?? claims.extension_Roles ?? [];
|
|
51
|
+
return Array.isArray(roles) && roles.includes(role);
|
|
52
|
+
},
|
|
53
|
+
[state.user]
|
|
54
|
+
);
|
|
55
|
+
const userName = react.useMemo(() => {
|
|
56
|
+
const claims = state.user?.idTokenClaims;
|
|
57
|
+
if (!claims) return null;
|
|
58
|
+
const given = claims.given_name ?? claims.name ?? "";
|
|
59
|
+
const family = claims.family_name ?? "";
|
|
60
|
+
return family ? `${given} ${family}`.trim() : given;
|
|
61
|
+
}, [state.user]);
|
|
62
|
+
return {
|
|
63
|
+
// State
|
|
64
|
+
isAuthenticated: state.isAuthenticated,
|
|
65
|
+
isLoading: state.isLoading,
|
|
66
|
+
user: state.user,
|
|
67
|
+
token: state.token,
|
|
68
|
+
error: state.error,
|
|
69
|
+
activePolicy: state.activePolicy,
|
|
70
|
+
authReady: state.authReady,
|
|
71
|
+
userName,
|
|
72
|
+
// Actions
|
|
73
|
+
signIn,
|
|
74
|
+
signInCityEmployee,
|
|
75
|
+
signOut,
|
|
76
|
+
forgotPassword,
|
|
77
|
+
acquireToken,
|
|
78
|
+
// Utilities
|
|
79
|
+
hasRole
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function useSSOClient() {
|
|
83
|
+
const context = react.useContext(SSOContext);
|
|
84
|
+
if (!context) {
|
|
85
|
+
throw new Error("useSSOClient must be used within an SSOProvider");
|
|
86
|
+
}
|
|
87
|
+
return context.client;
|
|
88
|
+
}
|
|
89
|
+
function B2CSSOProvider({
|
|
90
|
+
children,
|
|
91
|
+
signInPolicy = "B2C_1A_AD_SIGNIN_ONLY",
|
|
92
|
+
resetPasswordPolicy = "B2C_1A_PASSWORDRESET",
|
|
93
|
+
debug = false
|
|
94
|
+
}) {
|
|
95
|
+
const providerRef = react.useRef(null);
|
|
96
|
+
if (!providerRef.current) {
|
|
97
|
+
providerRef.current = new ssoCore.B2CProvider({
|
|
98
|
+
clientId: void 0,
|
|
99
|
+
b2cEnvironment: void 0,
|
|
100
|
+
authorityDomain: void 0,
|
|
101
|
+
redirectUri: void 0,
|
|
102
|
+
policies: {
|
|
103
|
+
signUpSignIn: signInPolicy,
|
|
104
|
+
signInOnly: signInPolicy,
|
|
105
|
+
resetPassword: resetPasswordPolicy
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return /* @__PURE__ */ jsxRuntime.jsx(SSOProvider, { config: { provider: providerRef.current, debug }, children });
|
|
110
|
+
}
|
|
111
|
+
exports.B2CSSOProvider = B2CSSOProvider;
|
|
112
|
+
exports.SSOContext = SSOContext;
|
|
113
|
+
exports.SSOProvider = SSOProvider;
|
|
114
|
+
exports.useAuth = useAuth;
|
|
115
|
+
exports.useSSOClient = useSSOClient;
|
|
2
116
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/context.tsx","../src/hooks/useAuth.ts","../src/hooks/useSSOClient.ts","../src/B2CSSOProvider.tsx"],"sourcesContent":["import { createContext, useRef, useState, useEffect } from \"react\";\nimport { SSOClient } from \"@phila/sso-core\";\nimport type { SSOClientConfig, SSOClientState } from \"@phila/sso-core\";\nimport type { ReactNode } from \"react\";\n\nconst INITIAL_STATE: SSOClientState = {\n isAuthenticated: false,\n isLoading: true,\n user: null,\n token: null,\n error: null,\n activePolicy: null,\n authReady: false,\n};\n\nexport interface SSOContextValue {\n client: SSOClient;\n state: SSOClientState;\n}\n\nexport const SSOContext = createContext<SSOContextValue | null>(null);\n\nexport interface SSOProviderProps {\n config: SSOClientConfig;\n children: ReactNode;\n}\n\nexport function SSOProvider({ config, children }: SSOProviderProps) {\n const clientRef = useRef<SSOClient | null>(null);\n const [state, setState] = useState<SSOClientState>(INITIAL_STATE);\n\n // Create client once (stable across renders)\n if (!clientRef.current) {\n clientRef.current = new SSOClient(config);\n }\n\n const client = clientRef.current;\n\n useEffect(() => {\n // Subscribe to state changes\n const unsubscribe = client.events.on(\"auth:stateChanged\", (newState: SSOClientState) => {\n setState(newState);\n });\n\n // Initialize\n client.initialize();\n\n return () => {\n unsubscribe();\n client.destroy();\n };\n }, [client]);\n\n return <SSOContext.Provider value={{ client, state }}>{children}</SSOContext.Provider>;\n}\n","import { useContext, useCallback, useMemo } from \"react\";\nimport { SSOContext } from \"../context\";\nimport type { SignInOptions, SignOutOptions, TokenOptions } from \"@phila/sso-core\";\n\n/**\n * Primary hook for SSO authentication in React apps.\n * Same API shape as the Vue composable for consistency.\n */\nexport function useAuth() {\n const context = useContext(SSOContext);\n if (!context) {\n throw new Error(\"useAuth must be used within an SSOProvider\");\n }\n\n const { client, state } = context;\n\n // Stable action references\n const signIn = useCallback((options?: SignInOptions) => client.signIn(options), [client]);\n\n const signInCityEmployee = useCallback((options?: SignInOptions) => client.signInCityEmployee(options), [client]);\n\n const signOut = useCallback((options?: SignOutOptions) => client.signOut(options), [client]);\n\n const forgotPassword = useCallback(() => client.forgotPassword(), [client]);\n\n const acquireToken = useCallback((options?: TokenOptions) => client.acquireToken(options), [client]);\n\n const hasRole = useCallback(\n (role: string): boolean => {\n const claims = state.user?.idTokenClaims as Record<string, unknown> | undefined;\n if (!claims) return false;\n const roles = (claims.roles ?? claims.extension_Roles ?? []) as string[];\n return Array.isArray(roles) && roles.includes(role);\n },\n [state.user],\n );\n\n const userName = useMemo(() => {\n const claims = state.user?.idTokenClaims as Record<string, unknown> | undefined;\n if (!claims) return null;\n const given = (claims.given_name ?? claims.name ?? \"\") as string;\n const family = (claims.family_name ?? \"\") as string;\n return family ? `${given} ${family}`.trim() : given;\n }, [state.user]);\n\n return {\n // State\n isAuthenticated: state.isAuthenticated,\n isLoading: state.isLoading,\n user: state.user,\n token: state.token,\n error: state.error,\n activePolicy: state.activePolicy,\n authReady: state.authReady,\n userName,\n\n // Actions\n signIn,\n signInCityEmployee,\n signOut,\n forgotPassword,\n acquireToken,\n\n // Utilities\n hasRole,\n };\n}\n","import { useContext } from \"react\";\nimport { SSOContext } from \"../context\";\nimport type { SSOClient } from \"@phila/sso-core\";\n\n/**\n * Access the raw SSOClient instance.\n * Must be called within an SSOProvider.\n */\nexport function useSSOClient(): SSOClient {\n const context = useContext(SSOContext);\n if (!context) {\n throw new Error(\"useSSOClient must be used within an SSOProvider\");\n }\n return context.client;\n}\n","// ABOUTME: Convenience provider component for React/Vite apps using Azure AD B2C\n// ABOUTME: Reads VITE_SSO_* env vars by convention so apps need zero B2CProvider boilerplate\nimport { useRef } from \"react\";\nimport { B2CProvider } from \"@phila/sso-core\";\nimport { SSOProvider } from \"./context\";\nimport type { ReactNode } from \"react\";\n\nexport interface B2CSSOProviderProps {\n children: ReactNode;\n signInPolicy?: string;\n resetPasswordPolicy?: string;\n debug?: boolean;\n}\n\n/**\n * Drop-in provider for Azure AD B2C authentication.\n * Reads connection details from VITE_SSO_* environment variables:\n * VITE_SSO_CLIENT_ID, VITE_SSO_TENANT, VITE_SSO_AUTHORITY_DOMAIN, VITE_SSO_REDIRECT_URI\n */\nexport function B2CSSOProvider({\n children,\n signInPolicy = \"B2C_1A_AD_SIGNIN_ONLY\",\n resetPasswordPolicy = \"B2C_1A_PASSWORDRESET\",\n debug = import.meta.env.DEV,\n}: B2CSSOProviderProps) {\n const providerRef = useRef<B2CProvider | null>(null);\n\n // Create once — props that affect provider construction are treated as initializers\n if (!providerRef.current) {\n providerRef.current = new B2CProvider({\n clientId: import.meta.env.VITE_SSO_CLIENT_ID,\n b2cEnvironment: import.meta.env.VITE_SSO_TENANT,\n authorityDomain: import.meta.env.VITE_SSO_AUTHORITY_DOMAIN,\n redirectUri: import.meta.env.VITE_SSO_REDIRECT_URI,\n policies: {\n signUpSignIn: signInPolicy,\n signInOnly: signInPolicy,\n resetPassword: resetPasswordPolicy,\n },\n });\n }\n\n return <SSOProvider config={{ provider: providerRef.current, debug }}>{children}</SSOProvider>;\n}\n"],"names":["
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/context.tsx","../src/hooks/useAuth.ts","../src/hooks/useSSOClient.ts","../src/B2CSSOProvider.tsx"],"sourcesContent":["import { createContext, useRef, useState, useEffect } from \"react\";\nimport { SSOClient } from \"@phila/sso-core\";\nimport type { SSOClientConfig, SSOClientState } from \"@phila/sso-core\";\nimport type { ReactNode } from \"react\";\n\nconst INITIAL_STATE: SSOClientState = {\n isAuthenticated: false,\n isLoading: true,\n user: null,\n token: null,\n error: null,\n activePolicy: null,\n authReady: false,\n};\n\nexport interface SSOContextValue {\n client: SSOClient;\n state: SSOClientState;\n}\n\nexport const SSOContext = createContext<SSOContextValue | null>(null);\n\nexport interface SSOProviderProps {\n config: SSOClientConfig;\n children: ReactNode;\n}\n\nexport function SSOProvider({ config, children }: SSOProviderProps) {\n const clientRef = useRef<SSOClient | null>(null);\n const [state, setState] = useState<SSOClientState>(INITIAL_STATE);\n\n // Create client once (stable across renders)\n if (!clientRef.current) {\n clientRef.current = new SSOClient(config);\n }\n\n const client = clientRef.current;\n\n useEffect(() => {\n // Subscribe to state changes\n const unsubscribe = client.events.on(\"auth:stateChanged\", (newState: SSOClientState) => {\n setState(newState);\n });\n\n // Initialize\n client.initialize();\n\n return () => {\n unsubscribe();\n client.destroy();\n };\n }, [client]);\n\n return <SSOContext.Provider value={{ client, state }}>{children}</SSOContext.Provider>;\n}\n","import { useContext, useCallback, useMemo } from \"react\";\nimport { SSOContext } from \"../context\";\nimport type { SignInOptions, SignOutOptions, TokenOptions } from \"@phila/sso-core\";\n\n/**\n * Primary hook for SSO authentication in React apps.\n * Same API shape as the Vue composable for consistency.\n */\nexport function useAuth() {\n const context = useContext(SSOContext);\n if (!context) {\n throw new Error(\"useAuth must be used within an SSOProvider\");\n }\n\n const { client, state } = context;\n\n // Stable action references\n const signIn = useCallback((options?: SignInOptions) => client.signIn(options), [client]);\n\n const signInCityEmployee = useCallback((options?: SignInOptions) => client.signInCityEmployee(options), [client]);\n\n const signOut = useCallback((options?: SignOutOptions) => client.signOut(options), [client]);\n\n const forgotPassword = useCallback(() => client.forgotPassword(), [client]);\n\n const acquireToken = useCallback((options?: TokenOptions) => client.acquireToken(options), [client]);\n\n const hasRole = useCallback(\n (role: string): boolean => {\n const claims = state.user?.idTokenClaims as Record<string, unknown> | undefined;\n if (!claims) return false;\n const roles = (claims.roles ?? claims.extension_Roles ?? []) as string[];\n return Array.isArray(roles) && roles.includes(role);\n },\n [state.user],\n );\n\n const userName = useMemo(() => {\n const claims = state.user?.idTokenClaims as Record<string, unknown> | undefined;\n if (!claims) return null;\n const given = (claims.given_name ?? claims.name ?? \"\") as string;\n const family = (claims.family_name ?? \"\") as string;\n return family ? `${given} ${family}`.trim() : given;\n }, [state.user]);\n\n return {\n // State\n isAuthenticated: state.isAuthenticated,\n isLoading: state.isLoading,\n user: state.user,\n token: state.token,\n error: state.error,\n activePolicy: state.activePolicy,\n authReady: state.authReady,\n userName,\n\n // Actions\n signIn,\n signInCityEmployee,\n signOut,\n forgotPassword,\n acquireToken,\n\n // Utilities\n hasRole,\n };\n}\n","import { useContext } from \"react\";\nimport { SSOContext } from \"../context\";\nimport type { SSOClient } from \"@phila/sso-core\";\n\n/**\n * Access the raw SSOClient instance.\n * Must be called within an SSOProvider.\n */\nexport function useSSOClient(): SSOClient {\n const context = useContext(SSOContext);\n if (!context) {\n throw new Error(\"useSSOClient must be used within an SSOProvider\");\n }\n return context.client;\n}\n","// ABOUTME: Convenience provider component for React/Vite apps using Azure AD B2C\n// ABOUTME: Reads VITE_SSO_* env vars by convention so apps need zero B2CProvider boilerplate\nimport { useRef } from \"react\";\nimport { B2CProvider } from \"@phila/sso-core\";\nimport { SSOProvider } from \"./context\";\nimport type { ReactNode } from \"react\";\n\nexport interface B2CSSOProviderProps {\n children: ReactNode;\n signInPolicy?: string;\n resetPasswordPolicy?: string;\n debug?: boolean;\n}\n\n/**\n * Drop-in provider for Azure AD B2C authentication.\n * Reads connection details from VITE_SSO_* environment variables:\n * VITE_SSO_CLIENT_ID, VITE_SSO_TENANT, VITE_SSO_AUTHORITY_DOMAIN, VITE_SSO_REDIRECT_URI\n */\nexport function B2CSSOProvider({\n children,\n signInPolicy = \"B2C_1A_AD_SIGNIN_ONLY\",\n resetPasswordPolicy = \"B2C_1A_PASSWORDRESET\",\n debug = import.meta.env.DEV,\n}: B2CSSOProviderProps) {\n const providerRef = useRef<B2CProvider | null>(null);\n\n // Create once — props that affect provider construction are treated as initializers\n if (!providerRef.current) {\n providerRef.current = new B2CProvider({\n clientId: import.meta.env.VITE_SSO_CLIENT_ID,\n b2cEnvironment: import.meta.env.VITE_SSO_TENANT,\n authorityDomain: import.meta.env.VITE_SSO_AUTHORITY_DOMAIN,\n redirectUri: import.meta.env.VITE_SSO_REDIRECT_URI,\n policies: {\n signUpSignIn: signInPolicy,\n signInOnly: signInPolicy,\n resetPassword: resetPasswordPolicy,\n },\n });\n }\n\n return <SSOProvider config={{ provider: providerRef.current, debug }}>{children}</SSOProvider>;\n}\n"],"names":["createContext","useRef","useState","SSOClient","useEffect","jsx","useContext","useCallback","useMemo","B2CProvider"],"mappings":";;;;;AAKA,MAAM,gBAAgC;AAAA,EACpC,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,MAAM;AAAA,EACN,OAAO;AAAA,EACP,OAAO;AAAA,EACP,cAAc;AAAA,EACd,WAAW;AACb;AAOO,MAAM,aAAaA,MAAAA,cAAsC,IAAI;AAO7D,SAAS,YAAY,EAAE,QAAQ,YAA8B;AAClE,QAAM,YAAYC,MAAAA,OAAyB,IAAI;AAC/C,QAAM,CAAC,OAAO,QAAQ,IAAIC,MAAAA,SAAyB,aAAa;AAGhE,MAAI,CAAC,UAAU,SAAS;AACtB,cAAU,UAAU,IAAIC,QAAAA,UAAU,MAAM;AAAA,EAC1C;AAEA,QAAM,SAAS,UAAU;AAEzBC,QAAAA,UAAU,MAAM;AAEd,UAAM,cAAc,OAAO,OAAO,GAAG,qBAAqB,CAAC,aAA6B;AACtF,eAAS,QAAQ;AAAA,IACnB,CAAC;AAGD,WAAO,WAAA;AAEP,WAAO,MAAM;AACX,kBAAA;AACA,aAAO,QAAA;AAAA,IACT;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,SAAOC,2BAAAA,IAAC,WAAW,UAAX,EAAoB,OAAO,EAAE,QAAQ,SAAU,UAAS;AAClE;AC9CO,SAAS,UAAU;AACxB,QAAM,UAAUC,MAAAA,WAAW,UAAU;AACrC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,4CAA4C;AAAA,EAC9D;AAEA,QAAM,EAAE,QAAQ,MAAA,IAAU;AAG1B,QAAM,SAASC,kBAAY,CAAC,YAA4B,OAAO,OAAO,OAAO,GAAG,CAAC,MAAM,CAAC;AAExF,QAAM,qBAAqBA,kBAAY,CAAC,YAA4B,OAAO,mBAAmB,OAAO,GAAG,CAAC,MAAM,CAAC;AAEhH,QAAM,UAAUA,kBAAY,CAAC,YAA6B,OAAO,QAAQ,OAAO,GAAG,CAAC,MAAM,CAAC;AAE3F,QAAM,iBAAiBA,MAAAA,YAAY,MAAM,OAAO,kBAAkB,CAAC,MAAM,CAAC;AAE1E,QAAM,eAAeA,kBAAY,CAAC,YAA2B,OAAO,aAAa,OAAO,GAAG,CAAC,MAAM,CAAC;AAEnG,QAAM,UAAUA,MAAAA;AAAAA,IACd,CAAC,SAA0B;AACzB,YAAM,SAAS,MAAM,MAAM;AAC3B,UAAI,CAAC,OAAQ,QAAO;AACpB,YAAM,QAAS,OAAO,SAAS,OAAO,mBAAmB,CAAA;AACzD,aAAO,MAAM,QAAQ,KAAK,KAAK,MAAM,SAAS,IAAI;AAAA,IACpD;AAAA,IACA,CAAC,MAAM,IAAI;AAAA,EAAA;AAGb,QAAM,WAAWC,MAAAA,QAAQ,MAAM;AAC7B,UAAM,SAAS,MAAM,MAAM;AAC3B,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,QAAS,OAAO,cAAc,OAAO,QAAQ;AACnD,UAAM,SAAU,OAAO,eAAe;AACtC,WAAO,SAAS,GAAG,KAAK,IAAI,MAAM,GAAG,SAAS;AAAA,EAChD,GAAG,CAAC,MAAM,IAAI,CAAC;AAEf,SAAO;AAAA;AAAA,IAEL,iBAAiB,MAAM;AAAA,IACvB,WAAW,MAAM;AAAA,IACjB,MAAM,MAAM;AAAA,IACZ,OAAO,MAAM;AAAA,IACb,OAAO,MAAM;AAAA,IACb,cAAc,MAAM;AAAA,IACpB,WAAW,MAAM;AAAA,IACjB;AAAA;AAAA,IAGA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAGA;AAAA,EAAA;AAEJ;AC1DO,SAAS,eAA0B;AACxC,QAAM,UAAUF,MAAAA,WAAW,UAAU;AACrC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,iDAAiD;AAAA,EACnE;AACA,SAAO,QAAQ;AACjB;ACKO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA,eAAe;AAAA,EACf,sBAAsB;AAAA,EACtB,QAAQ;AACV,GAAwB;AACtB,QAAM,cAAcL,MAAAA,OAA2B,IAAI;AAGnD,MAAI,CAAC,YAAY,SAAS;AACxB,gBAAY,UAAU,IAAIQ,oBAAY;AAAA,MACpC,UAAU;AAAA,MACV,gBAAgB;AAAA,MAChB,iBAAiB;AAAA,MACjB,aAAa;AAAA,MACb,UAAU;AAAA,QACR,cAAc;AAAA,QACd,YAAY;AAAA,QACZ,eAAe;AAAA,MAAA;AAAA,IACjB,CACD;AAAA,EACH;AAEA,SAAOJ,+BAAC,eAAY,QAAQ,EAAE,UAAU,YAAY,SAAS,SAAU,UAAS;AAClF;;;;;;"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,96 +1,116 @@
|
|
|
1
|
-
import { jsx
|
|
2
|
-
import { createContext
|
|
3
|
-
import { SSOClient
|
|
4
|
-
const
|
|
5
|
-
isAuthenticated:
|
|
6
|
-
isLoading:
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useRef, useState, useEffect, useContext, useCallback, useMemo } from "react";
|
|
3
|
+
import { SSOClient, B2CProvider } from "@phila/sso-core";
|
|
4
|
+
const INITIAL_STATE = {
|
|
5
|
+
isAuthenticated: false,
|
|
6
|
+
isLoading: true,
|
|
7
7
|
user: null,
|
|
8
8
|
token: null,
|
|
9
9
|
error: null,
|
|
10
10
|
activePolicy: null,
|
|
11
|
-
authReady:
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
authReady: false
|
|
12
|
+
};
|
|
13
|
+
const SSOContext = createContext(null);
|
|
14
|
+
function SSOProvider({ config, children }) {
|
|
15
|
+
const clientRef = useRef(null);
|
|
16
|
+
const [state, setState] = useState(INITIAL_STATE);
|
|
17
|
+
if (!clientRef.current) {
|
|
18
|
+
clientRef.current = new SSOClient(config);
|
|
19
|
+
}
|
|
20
|
+
const client = clientRef.current;
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const unsubscribe = client.events.on("auth:stateChanged", (newState) => {
|
|
23
|
+
setState(newState);
|
|
20
24
|
});
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
client.initialize();
|
|
26
|
+
return () => {
|
|
27
|
+
unsubscribe();
|
|
28
|
+
client.destroy();
|
|
23
29
|
};
|
|
24
|
-
}, [
|
|
30
|
+
}, [client]);
|
|
31
|
+
return /* @__PURE__ */ jsx(SSOContext.Provider, { value: { client, state }, children });
|
|
25
32
|
}
|
|
26
|
-
function
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
33
|
+
function useAuth() {
|
|
34
|
+
const context = useContext(SSOContext);
|
|
35
|
+
if (!context) {
|
|
29
36
|
throw new Error("useAuth must be used within an SSOProvider");
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
}
|
|
38
|
+
const { client, state } = context;
|
|
39
|
+
const signIn = useCallback((options) => client.signIn(options), [client]);
|
|
40
|
+
const signInCityEmployee = useCallback((options) => client.signInCityEmployee(options), [client]);
|
|
41
|
+
const signOut = useCallback((options) => client.signOut(options), [client]);
|
|
42
|
+
const forgotPassword = useCallback(() => client.forgotPassword(), [client]);
|
|
43
|
+
const acquireToken = useCallback((options) => client.acquireToken(options), [client]);
|
|
44
|
+
const hasRole = useCallback(
|
|
45
|
+
(role) => {
|
|
46
|
+
const claims = state.user?.idTokenClaims;
|
|
47
|
+
if (!claims) return false;
|
|
48
|
+
const roles = claims.roles ?? claims.extension_Roles ?? [];
|
|
49
|
+
return Array.isArray(roles) && roles.includes(role);
|
|
36
50
|
},
|
|
37
|
-
[
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
[state.user]
|
|
52
|
+
);
|
|
53
|
+
const userName = useMemo(() => {
|
|
54
|
+
const claims = state.user?.idTokenClaims;
|
|
55
|
+
if (!claims) return null;
|
|
56
|
+
const given = claims.given_name ?? claims.name ?? "";
|
|
57
|
+
const family = claims.family_name ?? "";
|
|
58
|
+
return family ? `${given} ${family}`.trim() : given;
|
|
59
|
+
}, [state.user]);
|
|
44
60
|
return {
|
|
45
61
|
// State
|
|
46
|
-
isAuthenticated:
|
|
47
|
-
isLoading:
|
|
48
|
-
user:
|
|
49
|
-
token:
|
|
50
|
-
error:
|
|
51
|
-
activePolicy:
|
|
52
|
-
authReady:
|
|
53
|
-
userName
|
|
62
|
+
isAuthenticated: state.isAuthenticated,
|
|
63
|
+
isLoading: state.isLoading,
|
|
64
|
+
user: state.user,
|
|
65
|
+
token: state.token,
|
|
66
|
+
error: state.error,
|
|
67
|
+
activePolicy: state.activePolicy,
|
|
68
|
+
authReady: state.authReady,
|
|
69
|
+
userName,
|
|
54
70
|
// Actions
|
|
55
|
-
signIn
|
|
56
|
-
signInCityEmployee
|
|
57
|
-
signOut
|
|
58
|
-
forgotPassword
|
|
59
|
-
acquireToken
|
|
71
|
+
signIn,
|
|
72
|
+
signInCityEmployee,
|
|
73
|
+
signOut,
|
|
74
|
+
forgotPassword,
|
|
75
|
+
acquireToken,
|
|
60
76
|
// Utilities
|
|
61
|
-
hasRole
|
|
77
|
+
hasRole
|
|
62
78
|
};
|
|
63
79
|
}
|
|
64
|
-
function
|
|
65
|
-
const
|
|
66
|
-
if (!
|
|
80
|
+
function useSSOClient() {
|
|
81
|
+
const context = useContext(SSOContext);
|
|
82
|
+
if (!context) {
|
|
67
83
|
throw new Error("useSSOClient must be used within an SSOProvider");
|
|
68
|
-
|
|
84
|
+
}
|
|
85
|
+
return context.client;
|
|
69
86
|
}
|
|
70
|
-
function
|
|
71
|
-
children
|
|
72
|
-
signInPolicy
|
|
73
|
-
resetPasswordPolicy
|
|
74
|
-
debug
|
|
87
|
+
function B2CSSOProvider({
|
|
88
|
+
children,
|
|
89
|
+
signInPolicy = "B2C_1A_AD_SIGNIN_ONLY",
|
|
90
|
+
resetPasswordPolicy = "B2C_1A_PASSWORDRESET",
|
|
91
|
+
debug = false
|
|
75
92
|
}) {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
const providerRef = useRef(null);
|
|
94
|
+
if (!providerRef.current) {
|
|
95
|
+
providerRef.current = new B2CProvider({
|
|
96
|
+
clientId: void 0,
|
|
97
|
+
b2cEnvironment: void 0,
|
|
98
|
+
authorityDomain: void 0,
|
|
99
|
+
redirectUri: void 0,
|
|
100
|
+
policies: {
|
|
101
|
+
signUpSignIn: signInPolicy,
|
|
102
|
+
signInOnly: signInPolicy,
|
|
103
|
+
resetPassword: resetPasswordPolicy
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return /* @__PURE__ */ jsx(SSOProvider, { config: { provider: providerRef.current, debug }, children });
|
|
88
108
|
}
|
|
89
109
|
export {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
110
|
+
B2CSSOProvider,
|
|
111
|
+
SSOContext,
|
|
112
|
+
SSOProvider,
|
|
113
|
+
useAuth,
|
|
114
|
+
useSSOClient
|
|
95
115
|
};
|
|
96
116
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sources":["../src/context.tsx","../src/hooks/useAuth.ts","../src/hooks/useSSOClient.ts","../src/B2CSSOProvider.tsx"],"sourcesContent":["import { createContext, useRef, useState, useEffect } from \"react\";\nimport { SSOClient } from \"@phila/sso-core\";\nimport type { SSOClientConfig, SSOClientState } from \"@phila/sso-core\";\nimport type { ReactNode } from \"react\";\n\nconst INITIAL_STATE: SSOClientState = {\n isAuthenticated: false,\n isLoading: true,\n user: null,\n token: null,\n error: null,\n activePolicy: null,\n authReady: false,\n};\n\nexport interface SSOContextValue {\n client: SSOClient;\n state: SSOClientState;\n}\n\nexport const SSOContext = createContext<SSOContextValue | null>(null);\n\nexport interface SSOProviderProps {\n config: SSOClientConfig;\n children: ReactNode;\n}\n\nexport function SSOProvider({ config, children }: SSOProviderProps) {\n const clientRef = useRef<SSOClient | null>(null);\n const [state, setState] = useState<SSOClientState>(INITIAL_STATE);\n\n // Create client once (stable across renders)\n if (!clientRef.current) {\n clientRef.current = new SSOClient(config);\n }\n\n const client = clientRef.current;\n\n useEffect(() => {\n // Subscribe to state changes\n const unsubscribe = client.events.on(\"auth:stateChanged\", (newState: SSOClientState) => {\n setState(newState);\n });\n\n // Initialize\n client.initialize();\n\n return () => {\n unsubscribe();\n client.destroy();\n };\n }, [client]);\n\n return <SSOContext.Provider value={{ client, state }}>{children}</SSOContext.Provider>;\n}\n","import { useContext, useCallback, useMemo } from \"react\";\nimport { SSOContext } from \"../context\";\nimport type { SignInOptions, SignOutOptions, TokenOptions } from \"@phila/sso-core\";\n\n/**\n * Primary hook for SSO authentication in React apps.\n * Same API shape as the Vue composable for consistency.\n */\nexport function useAuth() {\n const context = useContext(SSOContext);\n if (!context) {\n throw new Error(\"useAuth must be used within an SSOProvider\");\n }\n\n const { client, state } = context;\n\n // Stable action references\n const signIn = useCallback((options?: SignInOptions) => client.signIn(options), [client]);\n\n const signInCityEmployee = useCallback((options?: SignInOptions) => client.signInCityEmployee(options), [client]);\n\n const signOut = useCallback((options?: SignOutOptions) => client.signOut(options), [client]);\n\n const forgotPassword = useCallback(() => client.forgotPassword(), [client]);\n\n const acquireToken = useCallback((options?: TokenOptions) => client.acquireToken(options), [client]);\n\n const hasRole = useCallback(\n (role: string): boolean => {\n const claims = state.user?.idTokenClaims as Record<string, unknown> | undefined;\n if (!claims) return false;\n const roles = (claims.roles ?? claims.extension_Roles ?? []) as string[];\n return Array.isArray(roles) && roles.includes(role);\n },\n [state.user],\n );\n\n const userName = useMemo(() => {\n const claims = state.user?.idTokenClaims as Record<string, unknown> | undefined;\n if (!claims) return null;\n const given = (claims.given_name ?? claims.name ?? \"\") as string;\n const family = (claims.family_name ?? \"\") as string;\n return family ? `${given} ${family}`.trim() : given;\n }, [state.user]);\n\n return {\n // State\n isAuthenticated: state.isAuthenticated,\n isLoading: state.isLoading,\n user: state.user,\n token: state.token,\n error: state.error,\n activePolicy: state.activePolicy,\n authReady: state.authReady,\n userName,\n\n // Actions\n signIn,\n signInCityEmployee,\n signOut,\n forgotPassword,\n acquireToken,\n\n // Utilities\n hasRole,\n };\n}\n","import { useContext } from \"react\";\nimport { SSOContext } from \"../context\";\nimport type { SSOClient } from \"@phila/sso-core\";\n\n/**\n * Access the raw SSOClient instance.\n * Must be called within an SSOProvider.\n */\nexport function useSSOClient(): SSOClient {\n const context = useContext(SSOContext);\n if (!context) {\n throw new Error(\"useSSOClient must be used within an SSOProvider\");\n }\n return context.client;\n}\n","// ABOUTME: Convenience provider component for React/Vite apps using Azure AD B2C\n// ABOUTME: Reads VITE_SSO_* env vars by convention so apps need zero B2CProvider boilerplate\nimport { useRef } from \"react\";\nimport { B2CProvider } from \"@phila/sso-core\";\nimport { SSOProvider } from \"./context\";\nimport type { ReactNode } from \"react\";\n\nexport interface B2CSSOProviderProps {\n children: ReactNode;\n signInPolicy?: string;\n resetPasswordPolicy?: string;\n debug?: boolean;\n}\n\n/**\n * Drop-in provider for Azure AD B2C authentication.\n * Reads connection details from VITE_SSO_* environment variables:\n * VITE_SSO_CLIENT_ID, VITE_SSO_TENANT, VITE_SSO_AUTHORITY_DOMAIN, VITE_SSO_REDIRECT_URI\n */\nexport function B2CSSOProvider({\n children,\n signInPolicy = \"B2C_1A_AD_SIGNIN_ONLY\",\n resetPasswordPolicy = \"B2C_1A_PASSWORDRESET\",\n debug = import.meta.env.DEV,\n}: B2CSSOProviderProps) {\n const providerRef = useRef<B2CProvider | null>(null);\n\n // Create once — props that affect provider construction are treated as initializers\n if (!providerRef.current) {\n providerRef.current = new B2CProvider({\n clientId: import.meta.env.VITE_SSO_CLIENT_ID,\n b2cEnvironment: import.meta.env.VITE_SSO_TENANT,\n authorityDomain: import.meta.env.VITE_SSO_AUTHORITY_DOMAIN,\n redirectUri: import.meta.env.VITE_SSO_REDIRECT_URI,\n policies: {\n signUpSignIn: signInPolicy,\n signInOnly: signInPolicy,\n resetPassword: resetPasswordPolicy,\n },\n });\n }\n\n return <SSOProvider config={{ provider: providerRef.current, debug }}>{children}</SSOProvider>;\n}\n"],"names":[
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":["../src/context.tsx","../src/hooks/useAuth.ts","../src/hooks/useSSOClient.ts","../src/B2CSSOProvider.tsx"],"sourcesContent":["import { createContext, useRef, useState, useEffect } from \"react\";\nimport { SSOClient } from \"@phila/sso-core\";\nimport type { SSOClientConfig, SSOClientState } from \"@phila/sso-core\";\nimport type { ReactNode } from \"react\";\n\nconst INITIAL_STATE: SSOClientState = {\n isAuthenticated: false,\n isLoading: true,\n user: null,\n token: null,\n error: null,\n activePolicy: null,\n authReady: false,\n};\n\nexport interface SSOContextValue {\n client: SSOClient;\n state: SSOClientState;\n}\n\nexport const SSOContext = createContext<SSOContextValue | null>(null);\n\nexport interface SSOProviderProps {\n config: SSOClientConfig;\n children: ReactNode;\n}\n\nexport function SSOProvider({ config, children }: SSOProviderProps) {\n const clientRef = useRef<SSOClient | null>(null);\n const [state, setState] = useState<SSOClientState>(INITIAL_STATE);\n\n // Create client once (stable across renders)\n if (!clientRef.current) {\n clientRef.current = new SSOClient(config);\n }\n\n const client = clientRef.current;\n\n useEffect(() => {\n // Subscribe to state changes\n const unsubscribe = client.events.on(\"auth:stateChanged\", (newState: SSOClientState) => {\n setState(newState);\n });\n\n // Initialize\n client.initialize();\n\n return () => {\n unsubscribe();\n client.destroy();\n };\n }, [client]);\n\n return <SSOContext.Provider value={{ client, state }}>{children}</SSOContext.Provider>;\n}\n","import { useContext, useCallback, useMemo } from \"react\";\nimport { SSOContext } from \"../context\";\nimport type { SignInOptions, SignOutOptions, TokenOptions } from \"@phila/sso-core\";\n\n/**\n * Primary hook for SSO authentication in React apps.\n * Same API shape as the Vue composable for consistency.\n */\nexport function useAuth() {\n const context = useContext(SSOContext);\n if (!context) {\n throw new Error(\"useAuth must be used within an SSOProvider\");\n }\n\n const { client, state } = context;\n\n // Stable action references\n const signIn = useCallback((options?: SignInOptions) => client.signIn(options), [client]);\n\n const signInCityEmployee = useCallback((options?: SignInOptions) => client.signInCityEmployee(options), [client]);\n\n const signOut = useCallback((options?: SignOutOptions) => client.signOut(options), [client]);\n\n const forgotPassword = useCallback(() => client.forgotPassword(), [client]);\n\n const acquireToken = useCallback((options?: TokenOptions) => client.acquireToken(options), [client]);\n\n const hasRole = useCallback(\n (role: string): boolean => {\n const claims = state.user?.idTokenClaims as Record<string, unknown> | undefined;\n if (!claims) return false;\n const roles = (claims.roles ?? claims.extension_Roles ?? []) as string[];\n return Array.isArray(roles) && roles.includes(role);\n },\n [state.user],\n );\n\n const userName = useMemo(() => {\n const claims = state.user?.idTokenClaims as Record<string, unknown> | undefined;\n if (!claims) return null;\n const given = (claims.given_name ?? claims.name ?? \"\") as string;\n const family = (claims.family_name ?? \"\") as string;\n return family ? `${given} ${family}`.trim() : given;\n }, [state.user]);\n\n return {\n // State\n isAuthenticated: state.isAuthenticated,\n isLoading: state.isLoading,\n user: state.user,\n token: state.token,\n error: state.error,\n activePolicy: state.activePolicy,\n authReady: state.authReady,\n userName,\n\n // Actions\n signIn,\n signInCityEmployee,\n signOut,\n forgotPassword,\n acquireToken,\n\n // Utilities\n hasRole,\n };\n}\n","import { useContext } from \"react\";\nimport { SSOContext } from \"../context\";\nimport type { SSOClient } from \"@phila/sso-core\";\n\n/**\n * Access the raw SSOClient instance.\n * Must be called within an SSOProvider.\n */\nexport function useSSOClient(): SSOClient {\n const context = useContext(SSOContext);\n if (!context) {\n throw new Error(\"useSSOClient must be used within an SSOProvider\");\n }\n return context.client;\n}\n","// ABOUTME: Convenience provider component for React/Vite apps using Azure AD B2C\n// ABOUTME: Reads VITE_SSO_* env vars by convention so apps need zero B2CProvider boilerplate\nimport { useRef } from \"react\";\nimport { B2CProvider } from \"@phila/sso-core\";\nimport { SSOProvider } from \"./context\";\nimport type { ReactNode } from \"react\";\n\nexport interface B2CSSOProviderProps {\n children: ReactNode;\n signInPolicy?: string;\n resetPasswordPolicy?: string;\n debug?: boolean;\n}\n\n/**\n * Drop-in provider for Azure AD B2C authentication.\n * Reads connection details from VITE_SSO_* environment variables:\n * VITE_SSO_CLIENT_ID, VITE_SSO_TENANT, VITE_SSO_AUTHORITY_DOMAIN, VITE_SSO_REDIRECT_URI\n */\nexport function B2CSSOProvider({\n children,\n signInPolicy = \"B2C_1A_AD_SIGNIN_ONLY\",\n resetPasswordPolicy = \"B2C_1A_PASSWORDRESET\",\n debug = import.meta.env.DEV,\n}: B2CSSOProviderProps) {\n const providerRef = useRef<B2CProvider | null>(null);\n\n // Create once — props that affect provider construction are treated as initializers\n if (!providerRef.current) {\n providerRef.current = new B2CProvider({\n clientId: import.meta.env.VITE_SSO_CLIENT_ID,\n b2cEnvironment: import.meta.env.VITE_SSO_TENANT,\n authorityDomain: import.meta.env.VITE_SSO_AUTHORITY_DOMAIN,\n redirectUri: import.meta.env.VITE_SSO_REDIRECT_URI,\n policies: {\n signUpSignIn: signInPolicy,\n signInOnly: signInPolicy,\n resetPassword: resetPasswordPolicy,\n },\n });\n }\n\n return <SSOProvider config={{ provider: providerRef.current, debug }}>{children}</SSOProvider>;\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phila/sso-react",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "React adapter for @phila/sso-core",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"author": "City of Philadelphia",
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@phila/sso-core": "0.0.
|
|
28
|
+
"@phila/sso-core": "0.0.4"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"react": "^18.0.0 || ^19.0.0"
|