@cas-system/react-cas-client 1.0.0 → 1.0.1

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.
@@ -0,0 +1,63 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { CasConfig, CasContextValue, CasUser } from './types';
3
+ /**
4
+ * React Context for CAS authentication state and actions.
5
+ *
6
+ * Access this through the {@link useCasAuth} or {@link useCasUser} hooks
7
+ * rather than consuming it directly.
8
+ */
9
+ export declare const CasContext: import("react").Context<CasContextValue | null>;
10
+ /**
11
+ * Props for the {@link CasProvider} component.
12
+ */
13
+ export interface CasProviderProps {
14
+ /** CAS configuration (server URL, client ID, etc.). */
15
+ config: CasConfig;
16
+ /** Child components that need access to CAS auth state. */
17
+ children: ReactNode;
18
+ /**
19
+ * Callback invoked after successful authentication.
20
+ * Useful for analytics, redirects, or syncing with app state.
21
+ */
22
+ onAuthSuccess?: (user: CasUser) => void;
23
+ /**
24
+ * Callback invoked when an authentication error occurs.
25
+ * Useful for error reporting or showing notifications.
26
+ */
27
+ onAuthError?: (error: string) => void;
28
+ }
29
+ /**
30
+ * CAS Authentication Provider.
31
+ *
32
+ * Wrap your application (or a subtree) in `<CasProvider>` to enable CAS
33
+ * authentication. On mount the provider will:
34
+ *
35
+ * 1. Check `sessionStorage` for an existing user session.
36
+ * 2. If the URL contains a `?token=` parameter (i.e. the CAS redirect
37
+ * callback), automatically validate it via the backend and establish
38
+ * a session.
39
+ *
40
+ * @example
41
+ * ```tsx
42
+ * import { CasProvider } from '@cas-system/react-cas-client';
43
+ *
44
+ * function App() {
45
+ * return (
46
+ * <CasProvider
47
+ * config={{
48
+ * serverUrl: 'https://cas.example.com',
49
+ * clientId: 'my-app',
50
+ * callbackUrl: 'https://myapp.com/auth/callback',
51
+ * backendValidateUrl: '/api/auth/validate',
52
+ * }}
53
+ * onAuthSuccess={(user) => console.log('Logged in:', user.username)}
54
+ * onAuthError={(err) => console.error('Auth failed:', err)}
55
+ * >
56
+ * <YourApp />
57
+ * </CasProvider>
58
+ * );
59
+ * }
60
+ * ```
61
+ */
62
+ export declare function CasProvider({ config, children, onAuthSuccess, onAuthError, }: CasProviderProps): import("react").JSX.Element;
63
+ //# sourceMappingURL=CasProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CasProvider.d.ts","sourceRoot":"","sources":["../src/CasProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEnE;;;;;GAKG;AACH,eAAO,MAAM,UAAU,iDAA8C,CAAC;AAGtE;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,uDAAuD;IACvD,MAAM,EAAE,SAAS,CAAC;IAElB,2DAA2D;IAC3D,QAAQ,EAAE,SAAS,CAAC;IAEpB;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAExC;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,QAAQ,EACR,aAAa,EACb,WAAW,GACZ,EAAE,gBAAgB,+BAqHlB"}
@@ -0,0 +1,133 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useCallback, useEffect, useMemo, useRef, useState, } from 'react';
3
+ import { CasClient } from './cas-client';
4
+ /**
5
+ * React Context for CAS authentication state and actions.
6
+ *
7
+ * Access this through the {@link useCasAuth} or {@link useCasUser} hooks
8
+ * rather than consuming it directly.
9
+ */
10
+ export const CasContext = createContext(null);
11
+ CasContext.displayName = 'CasContext';
12
+ /**
13
+ * CAS Authentication Provider.
14
+ *
15
+ * Wrap your application (or a subtree) in `<CasProvider>` to enable CAS
16
+ * authentication. On mount the provider will:
17
+ *
18
+ * 1. Check `sessionStorage` for an existing user session.
19
+ * 2. If the URL contains a `?token=` parameter (i.e. the CAS redirect
20
+ * callback), automatically validate it via the backend and establish
21
+ * a session.
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * import { CasProvider } from '@cas-system/react-cas-client';
26
+ *
27
+ * function App() {
28
+ * return (
29
+ * <CasProvider
30
+ * config={{
31
+ * serverUrl: 'https://cas.example.com',
32
+ * clientId: 'my-app',
33
+ * callbackUrl: 'https://myapp.com/auth/callback',
34
+ * backendValidateUrl: '/api/auth/validate',
35
+ * }}
36
+ * onAuthSuccess={(user) => console.log('Logged in:', user.username)}
37
+ * onAuthError={(err) => console.error('Auth failed:', err)}
38
+ * >
39
+ * <YourApp />
40
+ * </CasProvider>
41
+ * );
42
+ * }
43
+ * ```
44
+ */
45
+ export function CasProvider({ config, children, onAuthSuccess, onAuthError, }) {
46
+ const [user, setUser] = useState(null);
47
+ const [isLoading, setIsLoading] = useState(true);
48
+ const [error, setError] = useState(null);
49
+ // Stable reference to the CAS client — recreated only when config changes
50
+ const client = useMemo(() => new CasClient(config), [config]);
51
+ // Guard to ensure the callback is handled at most once (React strict-mode safe)
52
+ const callbackHandled = useRef(false);
53
+ // -------------------------------------------------------------------------
54
+ // Initialisation
55
+ // -------------------------------------------------------------------------
56
+ useEffect(() => {
57
+ let cancelled = false;
58
+ async function init() {
59
+ try {
60
+ // 1. If there is a ?token= in the URL, handle the callback
61
+ const token = client.extractTokenFromUrl();
62
+ if (token && !callbackHandled.current) {
63
+ callbackHandled.current = true;
64
+ const validatedUser = await client.handleCallback();
65
+ if (!cancelled && validatedUser) {
66
+ setUser(validatedUser);
67
+ setError(null);
68
+ onAuthSuccess?.(validatedUser);
69
+ }
70
+ }
71
+ else {
72
+ // 2. Otherwise, try to restore a session from sessionStorage
73
+ const existingUser = client.getUser();
74
+ if (!cancelled && existingUser) {
75
+ setUser(existingUser);
76
+ }
77
+ }
78
+ }
79
+ catch (err) {
80
+ if (!cancelled) {
81
+ const message = err instanceof Error ? err.message : 'Authentication failed';
82
+ setError(message);
83
+ setUser(null);
84
+ onAuthError?.(message);
85
+ }
86
+ }
87
+ finally {
88
+ if (!cancelled) {
89
+ setIsLoading(false);
90
+ }
91
+ }
92
+ }
93
+ init();
94
+ return () => {
95
+ cancelled = true;
96
+ };
97
+ // eslint-disable-next-line react-hooks/exhaustive-deps
98
+ }, [client]);
99
+ // -------------------------------------------------------------------------
100
+ // Actions
101
+ // -------------------------------------------------------------------------
102
+ const login = useCallback((returnUrl) => {
103
+ client.login(returnUrl);
104
+ }, [client]);
105
+ const logout = useCallback(async (redirectUrl) => {
106
+ await client.logout(redirectUrl);
107
+ setUser(null);
108
+ setError(null);
109
+ }, [client]);
110
+ const hasRole = useCallback((role) => {
111
+ return user?.roles?.includes(role) ?? false;
112
+ }, [user]);
113
+ const hasAnyRole = useCallback((roles) => {
114
+ if (!user?.roles)
115
+ return false;
116
+ return roles.some((role) => user.roles.includes(role));
117
+ }, [user]);
118
+ // -------------------------------------------------------------------------
119
+ // Context value
120
+ // -------------------------------------------------------------------------
121
+ const value = useMemo(() => ({
122
+ user,
123
+ isAuthenticated: user !== null,
124
+ isLoading,
125
+ error,
126
+ login,
127
+ logout,
128
+ hasRole,
129
+ hasAnyRole,
130
+ }), [user, isLoading, error, login, logout, hasRole, hasAnyRole]);
131
+ return _jsx(CasContext.Provider, { value: value, children: children });
132
+ }
133
+ //# sourceMappingURL=CasProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CasProvider.js","sourceRoot":"","sources":["../src/CasProvider.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,161 @@
1
+ import type { CasConfig, CasUser } from './types';
2
+ /**
3
+ * Core CAS (Central Authentication System) client for browser-side operations.
4
+ *
5
+ * This class handles the SSO redirect flow, token extraction from callback URLs,
6
+ * session management via `sessionStorage`, and role-based access checks.
7
+ *
8
+ * **Security note:** Token validation is ALWAYS delegated to your backend.
9
+ * This client never sends `client_secret` or validates JWTs in the browser.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { CasClient } from '@cas-system/react-cas-client';
14
+ *
15
+ * const client = new CasClient({
16
+ * serverUrl: 'https://cas.example.com',
17
+ * clientId: 'my-app',
18
+ * callbackUrl: 'https://myapp.com/auth/callback',
19
+ * backendValidateUrl: '/api/auth/validate',
20
+ * });
21
+ *
22
+ * // Redirect to CAS login
23
+ * client.login();
24
+ *
25
+ * // Handle the callback (after redirect back)
26
+ * const user = await client.handleCallback();
27
+ * ```
28
+ */
29
+ export declare class CasClient {
30
+ private readonly config;
31
+ /**
32
+ * Create a new CAS client instance.
33
+ * @param config - The CAS configuration object.
34
+ */
35
+ constructor(config: CasConfig);
36
+ /**
37
+ * Build the full CAS SSO login URL.
38
+ *
39
+ * @param returnUrl - Optional URL to return to after authentication.
40
+ * Falls back to `config.callbackUrl`, then to `window.location.href`.
41
+ * @returns The complete login URL including query parameters.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * const url = client.getLoginUrl('/dashboard');
46
+ * // => "https://cas.example.com/sso/login?client_id=my-app&response_type=token&redirect_uri=https://myapp.com/dashboard"
47
+ * ```
48
+ */
49
+ getLoginUrl(returnUrl?: string): string;
50
+ /**
51
+ * Redirect the browser to the CAS SSO login page.
52
+ *
53
+ * @param returnUrl - Optional URL to return to after authentication.
54
+ */
55
+ login(returnUrl?: string): void;
56
+ /**
57
+ * Extract the JWT token from the current URL's `?token=` query parameter.
58
+ *
59
+ * After the CAS server authenticates the user, it redirects back to
60
+ * `callbackUrl?token=JWT_TOKEN`. This method reads that parameter.
61
+ *
62
+ * @returns The JWT token string, or `null` if not present.
63
+ */
64
+ extractTokenFromUrl(): string | null;
65
+ /**
66
+ * Send the token to your backend for validation.
67
+ *
68
+ * Your backend should forward the token to
69
+ * `POST {casServerUrl}/api/validate-token` along with `client_id` and
70
+ * `client_secret` — values that must **never** be exposed to the browser.
71
+ *
72
+ * @param token - The JWT token to validate.
73
+ * @returns The validated {@link CasUser} object.
74
+ * @throws {Error} If `backendValidateUrl` is not configured or the request fails.
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * const user = await client.validateTokenViaBackend(token);
79
+ * console.log(user.username); // 'john_doe'
80
+ * ```
81
+ */
82
+ validateTokenViaBackend(token: string): Promise<CasUser>;
83
+ /**
84
+ * Full callback handler: extract the token from the URL, validate it via the
85
+ * backend, persist the user in `sessionStorage`, and clean the URL.
86
+ *
87
+ * Call this on the page the CAS server redirects to after login.
88
+ *
89
+ * @returns The validated {@link CasUser}, or `null` if no `?token=` param is present.
90
+ * @throws {Error} If token validation fails.
91
+ */
92
+ handleCallback(): Promise<CasUser | null>;
93
+ /**
94
+ * Retrieve the currently stored user from `sessionStorage`.
95
+ *
96
+ * @returns The stored {@link CasUser}, or `null` if no session exists.
97
+ */
98
+ getUser(): CasUser | null;
99
+ /**
100
+ * Retrieve the currently stored JWT token from `sessionStorage`.
101
+ *
102
+ * @returns The stored token string, or `null` if no session exists.
103
+ */
104
+ getToken(): string | null;
105
+ /**
106
+ * Check whether a user session currently exists.
107
+ *
108
+ * @returns `true` if a user is stored in `sessionStorage`.
109
+ */
110
+ isAuthenticated(): boolean;
111
+ /**
112
+ * Persist a {@link CasUser} in `sessionStorage`.
113
+ * @internal
114
+ */
115
+ private setUser;
116
+ /**
117
+ * Persist a JWT token in `sessionStorage`.
118
+ * @internal
119
+ */
120
+ private setToken;
121
+ /**
122
+ * Clear all CAS-related data from `sessionStorage`.
123
+ * @internal
124
+ */
125
+ private clearSession;
126
+ /**
127
+ * Log the user out by clearing the local session, calling the CAS server's
128
+ * logout endpoint, and optionally redirecting.
129
+ *
130
+ * @param redirectUrl - URL to navigate to after logout. If omitted, the
131
+ * page is reloaded at the current location.
132
+ */
133
+ logout(redirectUrl?: string): Promise<void>;
134
+ /**
135
+ * Check whether the current user has a specific role.
136
+ *
137
+ * @param role - The role to check (case-sensitive).
138
+ * @returns `true` if the stored user has the given role.
139
+ */
140
+ userHasRole(role: string): boolean;
141
+ /**
142
+ * Check whether the current user has **at least one** of the given roles.
143
+ *
144
+ * @param roles - The roles to check.
145
+ * @returns `true` if the user has any of the specified roles.
146
+ */
147
+ userHasAnyRole(roles: string[]): boolean;
148
+ /**
149
+ * Check whether the current user has **all** of the given roles.
150
+ *
151
+ * @param roles - The roles to check.
152
+ * @returns `true` if the user has every specified role.
153
+ */
154
+ userHasAllRoles(roles: string[]): boolean;
155
+ /**
156
+ * Remove the `token` query parameter from the browser URL without reloading.
157
+ * @internal
158
+ */
159
+ private cleanUrl;
160
+ }
161
+ //# sourceMappingURL=cas-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cas-client.d.ts","sourceRoot":"","sources":["../src/cas-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAQlD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;IAEnC;;;OAGG;gBACS,MAAM,EAAE,SAAS;IAQ7B;;;;;;;;;;;;OAYG;IACH,WAAW,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM;IAiBvC;;;;OAIG;IACH,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAU/B;;;;;;;OAOG;IACH,mBAAmB,IAAI,MAAM,GAAG,IAAI;IAOpC;;;;;;;;;;;;;;;;OAgBG;IACG,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA4B9D;;;;;;;;OAQG;IACG,cAAc,IAAI,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAoB/C;;;;OAIG;IACH,OAAO,IAAI,OAAO,GAAG,IAAI;IAWzB;;;;OAIG;IACH,QAAQ,IAAI,MAAM,GAAG,IAAI;IAUzB;;;;OAIG;IACH,eAAe,IAAI,OAAO;IAI1B;;;OAGG;IACH,OAAO,CAAC,OAAO;IAKf;;;OAGG;IACH,OAAO,CAAC,QAAQ;IAKhB;;;OAGG;IACH,OAAO,CAAC,YAAY;IAUpB;;;;;;OAMG;IACG,MAAM,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA4BjD;;;;;OAKG;IACH,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAKlC;;;;;OAKG;IACH,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO;IAMxC;;;;;OAKG;IACH,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO;IAUzC;;;OAGG;IACH,OAAO,CAAC,QAAQ;CASjB"}
@@ -0,0 +1,309 @@
1
+ /** Key used to persist user data in sessionStorage. */
2
+ const STORAGE_KEY = 'cas_user';
3
+ /** Key used to persist the JWT token in sessionStorage. */
4
+ const TOKEN_STORAGE_KEY = 'cas_token';
5
+ /**
6
+ * Core CAS (Central Authentication System) client for browser-side operations.
7
+ *
8
+ * This class handles the SSO redirect flow, token extraction from callback URLs,
9
+ * session management via `sessionStorage`, and role-based access checks.
10
+ *
11
+ * **Security note:** Token validation is ALWAYS delegated to your backend.
12
+ * This client never sends `client_secret` or validates JWTs in the browser.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { CasClient } from '@cas-system/react-cas-client';
17
+ *
18
+ * const client = new CasClient({
19
+ * serverUrl: 'https://cas.example.com',
20
+ * clientId: 'my-app',
21
+ * callbackUrl: 'https://myapp.com/auth/callback',
22
+ * backendValidateUrl: '/api/auth/validate',
23
+ * });
24
+ *
25
+ * // Redirect to CAS login
26
+ * client.login();
27
+ *
28
+ * // Handle the callback (after redirect back)
29
+ * const user = await client.handleCallback();
30
+ * ```
31
+ */
32
+ export class CasClient {
33
+ /**
34
+ * Create a new CAS client instance.
35
+ * @param config - The CAS configuration object.
36
+ */
37
+ constructor(config) {
38
+ this.config = config;
39
+ }
40
+ // ---------------------------------------------------------------------------
41
+ // Login / Redirect
42
+ // ---------------------------------------------------------------------------
43
+ /**
44
+ * Build the full CAS SSO login URL.
45
+ *
46
+ * @param returnUrl - Optional URL to return to after authentication.
47
+ * Falls back to `config.callbackUrl`, then to `window.location.href`.
48
+ * @returns The complete login URL including query parameters.
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * const url = client.getLoginUrl('/dashboard');
53
+ * // => "https://cas.example.com/sso/login?client_id=my-app&response_type=token&redirect_uri=https://myapp.com/dashboard"
54
+ * ```
55
+ */
56
+ getLoginUrl(returnUrl) {
57
+ const redirectUri = returnUrl ??
58
+ this.config.callbackUrl ??
59
+ (typeof window !== 'undefined'
60
+ ? window.location.href
61
+ : '');
62
+ const params = new URLSearchParams({
63
+ client_id: this.config.clientId,
64
+ response_type: 'token',
65
+ redirect_uri: redirectUri,
66
+ });
67
+ return `${this.config.serverUrl}/sso/login?${params.toString()}`;
68
+ }
69
+ /**
70
+ * Redirect the browser to the CAS SSO login page.
71
+ *
72
+ * @param returnUrl - Optional URL to return to after authentication.
73
+ */
74
+ login(returnUrl) {
75
+ if (typeof window !== 'undefined') {
76
+ window.location.href = this.getLoginUrl(returnUrl);
77
+ }
78
+ }
79
+ // ---------------------------------------------------------------------------
80
+ // Token Handling
81
+ // ---------------------------------------------------------------------------
82
+ /**
83
+ * Extract the JWT token from the current URL's `?token=` query parameter.
84
+ *
85
+ * After the CAS server authenticates the user, it redirects back to
86
+ * `callbackUrl?token=JWT_TOKEN`. This method reads that parameter.
87
+ *
88
+ * @returns The JWT token string, or `null` if not present.
89
+ */
90
+ extractTokenFromUrl() {
91
+ if (typeof window === 'undefined')
92
+ return null;
93
+ const params = new URLSearchParams(window.location.search);
94
+ return params.get('token');
95
+ }
96
+ /**
97
+ * Send the token to your backend for validation.
98
+ *
99
+ * Your backend should forward the token to
100
+ * `POST {casServerUrl}/api/validate-token` along with `client_id` and
101
+ * `client_secret` — values that must **never** be exposed to the browser.
102
+ *
103
+ * @param token - The JWT token to validate.
104
+ * @returns The validated {@link CasUser} object.
105
+ * @throws {Error} If `backendValidateUrl` is not configured or the request fails.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * const user = await client.validateTokenViaBackend(token);
110
+ * console.log(user.username); // 'john_doe'
111
+ * ```
112
+ */
113
+ async validateTokenViaBackend(token) {
114
+ const { backendValidateUrl } = this.config;
115
+ if (!backendValidateUrl) {
116
+ throw new Error('[CAS Client] backendValidateUrl is not configured. ' +
117
+ 'Token validation must be performed by your backend server.');
118
+ }
119
+ const response = await fetch(backendValidateUrl, {
120
+ method: 'POST',
121
+ headers: { 'Content-Type': 'application/json' },
122
+ body: JSON.stringify({ token }),
123
+ credentials: 'include',
124
+ });
125
+ if (!response.ok) {
126
+ const errorText = await response.text().catch(() => 'Unknown error');
127
+ throw new Error(`[CAS Client] Token validation failed (${response.status}): ${errorText}`);
128
+ }
129
+ const data = await response.json();
130
+ return data;
131
+ }
132
+ /**
133
+ * Full callback handler: extract the token from the URL, validate it via the
134
+ * backend, persist the user in `sessionStorage`, and clean the URL.
135
+ *
136
+ * Call this on the page the CAS server redirects to after login.
137
+ *
138
+ * @returns The validated {@link CasUser}, or `null` if no `?token=` param is present.
139
+ * @throws {Error} If token validation fails.
140
+ */
141
+ async handleCallback() {
142
+ const token = this.extractTokenFromUrl();
143
+ if (!token)
144
+ return null;
145
+ const user = await this.validateTokenViaBackend(token);
146
+ // Persist user and token in sessionStorage
147
+ this.setUser(user);
148
+ this.setToken(token);
149
+ // Remove the token from the URL to prevent accidental leakage
150
+ this.cleanUrl();
151
+ return user;
152
+ }
153
+ // ---------------------------------------------------------------------------
154
+ // Session Management
155
+ // ---------------------------------------------------------------------------
156
+ /**
157
+ * Retrieve the currently stored user from `sessionStorage`.
158
+ *
159
+ * @returns The stored {@link CasUser}, or `null` if no session exists.
160
+ */
161
+ getUser() {
162
+ if (typeof window === 'undefined')
163
+ return null;
164
+ try {
165
+ const raw = sessionStorage.getItem(STORAGE_KEY);
166
+ return raw ? JSON.parse(raw) : null;
167
+ }
168
+ catch {
169
+ return null;
170
+ }
171
+ }
172
+ /**
173
+ * Retrieve the currently stored JWT token from `sessionStorage`.
174
+ *
175
+ * @returns The stored token string, or `null` if no session exists.
176
+ */
177
+ getToken() {
178
+ if (typeof window === 'undefined')
179
+ return null;
180
+ try {
181
+ return sessionStorage.getItem(TOKEN_STORAGE_KEY);
182
+ }
183
+ catch {
184
+ return null;
185
+ }
186
+ }
187
+ /**
188
+ * Check whether a user session currently exists.
189
+ *
190
+ * @returns `true` if a user is stored in `sessionStorage`.
191
+ */
192
+ isAuthenticated() {
193
+ return this.getUser() !== null;
194
+ }
195
+ /**
196
+ * Persist a {@link CasUser} in `sessionStorage`.
197
+ * @internal
198
+ */
199
+ setUser(user) {
200
+ if (typeof window === 'undefined')
201
+ return;
202
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(user));
203
+ }
204
+ /**
205
+ * Persist a JWT token in `sessionStorage`.
206
+ * @internal
207
+ */
208
+ setToken(token) {
209
+ if (typeof window === 'undefined')
210
+ return;
211
+ sessionStorage.setItem(TOKEN_STORAGE_KEY, token);
212
+ }
213
+ /**
214
+ * Clear all CAS-related data from `sessionStorage`.
215
+ * @internal
216
+ */
217
+ clearSession() {
218
+ if (typeof window === 'undefined')
219
+ return;
220
+ sessionStorage.removeItem(STORAGE_KEY);
221
+ sessionStorage.removeItem(TOKEN_STORAGE_KEY);
222
+ }
223
+ // ---------------------------------------------------------------------------
224
+ // Logout
225
+ // ---------------------------------------------------------------------------
226
+ /**
227
+ * Log the user out by clearing the local session, calling the CAS server's
228
+ * logout endpoint, and optionally redirecting.
229
+ *
230
+ * @param redirectUrl - URL to navigate to after logout. If omitted, the
231
+ * page is reloaded at the current location.
232
+ */
233
+ async logout(redirectUrl) {
234
+ // Clear local session first
235
+ this.clearSession();
236
+ // Call the CAS server logout endpoint (fire-and-forget)
237
+ try {
238
+ await fetch(`${this.config.serverUrl}/api/logout`, {
239
+ method: 'POST',
240
+ credentials: 'include',
241
+ });
242
+ }
243
+ catch {
244
+ // Swallow network errors — the local session is already cleared
245
+ }
246
+ // Redirect
247
+ if (typeof window !== 'undefined') {
248
+ if (redirectUrl) {
249
+ window.location.href = redirectUrl;
250
+ }
251
+ else {
252
+ window.location.reload();
253
+ }
254
+ }
255
+ }
256
+ // ---------------------------------------------------------------------------
257
+ // Role Helpers
258
+ // ---------------------------------------------------------------------------
259
+ /**
260
+ * Check whether the current user has a specific role.
261
+ *
262
+ * @param role - The role to check (case-sensitive).
263
+ * @returns `true` if the stored user has the given role.
264
+ */
265
+ userHasRole(role) {
266
+ const user = this.getUser();
267
+ return user?.roles?.includes(role) ?? false;
268
+ }
269
+ /**
270
+ * Check whether the current user has **at least one** of the given roles.
271
+ *
272
+ * @param roles - The roles to check.
273
+ * @returns `true` if the user has any of the specified roles.
274
+ */
275
+ userHasAnyRole(roles) {
276
+ const user = this.getUser();
277
+ if (!user?.roles)
278
+ return false;
279
+ return roles.some((role) => user.roles.includes(role));
280
+ }
281
+ /**
282
+ * Check whether the current user has **all** of the given roles.
283
+ *
284
+ * @param roles - The roles to check.
285
+ * @returns `true` if the user has every specified role.
286
+ */
287
+ userHasAllRoles(roles) {
288
+ const user = this.getUser();
289
+ if (!user?.roles)
290
+ return false;
291
+ return roles.every((role) => user.roles.includes(role));
292
+ }
293
+ // ---------------------------------------------------------------------------
294
+ // Internal Helpers
295
+ // ---------------------------------------------------------------------------
296
+ /**
297
+ * Remove the `token` query parameter from the browser URL without reloading.
298
+ * @internal
299
+ */
300
+ cleanUrl() {
301
+ if (typeof window === 'undefined')
302
+ return;
303
+ const url = new URL(window.location.href);
304
+ url.searchParams.delete('token');
305
+ const cleanedUrl = url.pathname + (url.search || '') + url.hash;
306
+ window.history.replaceState({}, document.title, cleanedUrl);
307
+ }
308
+ }
309
+ //# sourceMappingURL=cas-client.js.map