@arkitektbedriftene/fe-lib 0.2.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.
@@ -0,0 +1,248 @@
1
+ import { SigninRedirectArgs, User, UserManager } from "oidc-client-ts";
2
+ import {
3
+ Context,
4
+ createContext,
5
+ ReactNode,
6
+ useCallback,
7
+ useContext,
8
+ useEffect,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ } from "react";
13
+
14
+ export * from "oidc-client-ts";
15
+
16
+ export * from "./impersonate";
17
+
18
+ export type AuthState = {
19
+ user: User | null;
20
+ isLoading: boolean;
21
+ isAuthenticated: boolean;
22
+ isError: boolean;
23
+ error: Error | null;
24
+ };
25
+
26
+ export type AuthContextData = {
27
+ state: AuthState;
28
+ handleSigninCallback: () => Promise<User | undefined>;
29
+ redirectToSignin: UserManager["signinRedirect"];
30
+ };
31
+
32
+ export type AuthProviderConfig = {
33
+ onSigninComplete: (user: User | null) => void;
34
+ };
35
+
36
+ const AuthProviderCore = ({
37
+ userManager,
38
+ context,
39
+ children,
40
+ }: {
41
+ userManager: UserManager;
42
+ context: Context<AuthContextData | null>;
43
+ children: ReactNode;
44
+ }) => {
45
+ const [state, setState] = useState<AuthState>({
46
+ user: null,
47
+ isLoading: true,
48
+ isAuthenticated: false,
49
+ isError: false,
50
+ error: null,
51
+ });
52
+
53
+ // Initialize on first render
54
+ const isInitialized = useRef(false);
55
+ useEffect(() => {
56
+ if (isInitialized.current) {
57
+ return;
58
+ }
59
+ isInitialized.current = true;
60
+
61
+ void (async () => {
62
+ try {
63
+ const user = await userManager.getUser();
64
+ setState({
65
+ user,
66
+ isLoading: false,
67
+ isAuthenticated: user ? !user.expired : false,
68
+ isError: false,
69
+ error: null,
70
+ });
71
+ } catch (error) {
72
+ setState({
73
+ user: null,
74
+ isLoading: false,
75
+ isAuthenticated: false,
76
+ isError: true,
77
+ error:
78
+ error instanceof Error
79
+ ? error
80
+ : new Error("Unknown error during auth"),
81
+ });
82
+ }
83
+ })();
84
+ }, [userManager]);
85
+
86
+ // Set up UserManager events
87
+ useEffect(() => {
88
+ const onUserLoaded = (user: User) => {
89
+ setState({
90
+ user,
91
+ isLoading: false,
92
+ isAuthenticated: !user.expired,
93
+ isError: false,
94
+ error: null,
95
+ });
96
+ };
97
+ userManager.events.addUserLoaded(onUserLoaded);
98
+
99
+ const onUserUnloaded = () => {
100
+ setState({
101
+ ...state,
102
+ user: null,
103
+ isAuthenticated: false,
104
+ });
105
+ };
106
+ userManager.events.addUserUnloaded(onUserUnloaded);
107
+
108
+ const onSilentRenewError = (error: Error) => {
109
+ setState({
110
+ ...state,
111
+ isLoading: false,
112
+ isError: true,
113
+ error,
114
+ });
115
+ };
116
+ userManager.events.addSilentRenewError(onSilentRenewError);
117
+
118
+ return () => {
119
+ userManager.events.removeUserLoaded(onUserLoaded);
120
+ userManager.events.removeUserUnloaded(onUserUnloaded);
121
+ userManager.events.removeSilentRenewError(onSilentRenewError);
122
+ };
123
+ }, [userManager]);
124
+
125
+ const handleSigninCallback = useCallback(async () => {
126
+ const user = await userManager.signinCallback();
127
+
128
+ setState({
129
+ user: user ?? null,
130
+ isLoading: false,
131
+ isAuthenticated: user ? !user.expired : false,
132
+ isError: false,
133
+ error: null,
134
+ });
135
+
136
+ return user ?? undefined;
137
+ }, [userManager]);
138
+
139
+ const redirectToSignin = useCallback(
140
+ async (args?: SigninRedirectArgs | undefined) => {
141
+ try {
142
+ await userManager.signinRedirect(args);
143
+ } catch (error) {
144
+ console.error(error);
145
+ }
146
+ },
147
+ [userManager]
148
+ );
149
+
150
+ const contextValue = useMemo(
151
+ () => ({
152
+ state,
153
+ handleSigninCallback,
154
+ redirectToSignin,
155
+ }),
156
+ [state, handleSigninCallback, redirectToSignin]
157
+ );
158
+
159
+ return <context.Provider value={contextValue}>{children}</context.Provider>;
160
+ };
161
+
162
+ const useAuthContextCore = (context: Context<AuthContextData | null>) => {
163
+ const contextData = useContext(context);
164
+ if (!contextData) {
165
+ throw new Error("useAuthContext must be used within an AuthProvider");
166
+ }
167
+ return contextData;
168
+ };
169
+
170
+ const useAuthStateCore = (context: Context<AuthContextData | null>) => {
171
+ const { state } = useAuthContextCore(context);
172
+ return state;
173
+ };
174
+
175
+ const useSigninCallbackCore = (
176
+ context: Context<AuthContextData | null>,
177
+ onDone?: (user?: User) => void
178
+ ) => {
179
+ const { state, handleSigninCallback } = useAuthContextCore(context);
180
+
181
+ const isInitialized = useRef(false);
182
+ useEffect(() => {
183
+ // Only run once
184
+ if (isInitialized.current) {
185
+ return;
186
+ }
187
+ isInitialized.current = true;
188
+ handleSigninCallback()
189
+ // Wait for state to update
190
+ // Otherwise any navigation that happens in onDone will
191
+ // happen before the state update and the user will be
192
+ // redirected to the signin page again.
193
+ .then(
194
+ (u) =>
195
+ new Promise<User | undefined>((resolve) =>
196
+ setTimeout(() => resolve(u), 0)
197
+ )
198
+ )
199
+ .then((u) => onDone?.(u));
200
+ }, [handleSigninCallback]);
201
+
202
+ return state;
203
+ };
204
+
205
+ export const createAuthContext = (userManager: UserManager) => {
206
+ const AuthContext = createContext<AuthContextData | null>(null);
207
+
208
+ const AuthProvider = ({ children }: { children: ReactNode }) => {
209
+ return (
210
+ <AuthProviderCore userManager={userManager} context={AuthContext}>
211
+ {children}
212
+ </AuthProviderCore>
213
+ );
214
+ };
215
+
216
+ const useAuthContext = () => useAuthContextCore(AuthContext);
217
+ const useAuthState = () => useAuthStateCore(AuthContext);
218
+
219
+ /**
220
+ * Hook to handle the signin callback. Use this on the signin callback page.
221
+ * @param onDone Optional callback to run after signin is complete. Useful to redirect to a different page.
222
+ */
223
+ const useSigninCallback = (onDone?: (user?: User) => void) =>
224
+ useSigninCallbackCore(AuthContext, onDone);
225
+
226
+ /**
227
+ * Async function to get the access token for the current user
228
+ * outside of a React component.
229
+ *
230
+ * Useful for prefetching data in React Router or similar.
231
+ */
232
+ const getAccessToken = async () => {
233
+ const user = await userManager.getUser();
234
+ if (!user) {
235
+ return null;
236
+ }
237
+ return user.access_token;
238
+ };
239
+
240
+ return {
241
+ AuthContext,
242
+ AuthProvider,
243
+ useAuthContext,
244
+ useAuthState,
245
+ useSigninCallback,
246
+ getAccessToken,
247
+ };
248
+ };
package/src/main.tsx ADDED
@@ -0,0 +1,10 @@
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App'
4
+ import './index.css'
5
+
6
+ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ )
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
6
+ "allowJs": false,
7
+ "skipLibCheck": true,
8
+ "esModuleInterop": false,
9
+ "allowSyntheticDefaultImports": true,
10
+ "strict": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "module": "ESNext",
13
+ "moduleResolution": "Node",
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "noEmit": true,
17
+ "jsx": "react-jsx"
18
+ },
19
+ "include": ["src"],
20
+ "references": [{ "path": "./tsconfig.node.json" }]
21
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "module": "ESNext",
5
+ "moduleResolution": "Node",
6
+ "allowSyntheticDefaultImports": true
7
+ },
8
+ "include": ["vite.config.ts"]
9
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react-swc";
3
+ import dts from "vite-plugin-dts";
4
+
5
+ // https://vitejs.dev/config/
6
+ export default defineConfig({
7
+ build: {
8
+ lib: {
9
+ entry: [
10
+ "src/lib/index.ts",
11
+ "src/lib/oidc/oidc.tsx",
12
+ "src/lib/hooks/hooks.ts",
13
+ ],
14
+ name: "fe-lib",
15
+ fileName: (format, entryname) => `${entryname}.${format}.js`,
16
+ },
17
+ rollupOptions: {
18
+ external: ["react", "react-dom"],
19
+ output: {
20
+ globals: {
21
+ react: "React",
22
+ "react-dom": "ReactDOM",
23
+ },
24
+ },
25
+ },
26
+ },
27
+ plugins: [
28
+ react(),
29
+ dts({
30
+ insertTypesEntry: true,
31
+ }),
32
+ ],
33
+ });