create-ec-app 0.0.6 → 0.0.7

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.
Files changed (95) hide show
  1. package/package.json +3 -2
  2. package/templates/base/.prettierrc.json +12 -0
  3. package/templates/base/README.md +73 -0
  4. package/templates/base/biome.json +54 -0
  5. package/templates/base/eslint.config.js +23 -0
  6. package/templates/base/index.html +13 -0
  7. package/templates/base/package-lock.json +3898 -0
  8. package/templates/base/package.json +42 -0
  9. package/templates/base/public/vite.svg +1 -0
  10. package/templates/base/src/App.css +0 -0
  11. package/templates/base/src/App.tsx +11 -0
  12. package/templates/base/src/global.d.ts +4 -0
  13. package/templates/base/src/index.css +1 -0
  14. package/templates/base/src/main.tsx +29 -0
  15. package/templates/base/tsconfig.app.json +32 -0
  16. package/templates/base/tsconfig.json +19 -0
  17. package/templates/base/tsconfig.node.json +26 -0
  18. package/templates/base/vite.config.ts +14 -0
  19. package/templates/targets/power-pages/.env +3 -0
  20. package/templates/targets/power-pages/README.md +180 -0
  21. package/templates/targets/power-pages/powerpages.config.json +5 -0
  22. package/templates/targets/power-pages/src/App.patch.tsx +17 -0
  23. package/templates/targets/power-pages/src/context/AuthContext.tsx +273 -0
  24. package/templates/targets/power-pages/src/main.patch.tsx +32 -0
  25. package/templates/targets/power-pages/vite.config.patch.ts +23 -0
  26. package/templates/targets/swa/.env +2 -0
  27. package/templates/targets/swa/package.patch.json +5 -0
  28. package/templates/targets/swa/staticwebapp.config.json +5 -0
  29. package/templates/targets/swa/swa-cli.config.json +14 -0
  30. package/templates/targets/webresource/README.md +263 -0
  31. package/templates/targets/webresource/src/services/AuthService.ts +38 -0
  32. package/templates/targets/webresource/token.json +8 -0
  33. package/templates/targets/webresource/vite.config.patch.ts +40 -0
  34. package/templates/ui/kendo/kendo-tw-preset.js +240 -0
  35. package/templates/ui/kendo/package.patch.json +7 -0
  36. package/templates/ui/kendo/src/main.patch.tsx +29 -0
  37. package/templates/ui/kendo/tailwind.config.js +14 -0
  38. package/templates/ui/shadcn-ui/components.json +22 -0
  39. package/templates/ui/shadcn-ui/package.patch.json +50 -0
  40. package/templates/ui/shadcn-ui/src/components/ui/accordion.tsx +64 -0
  41. package/templates/ui/shadcn-ui/src/components/ui/alert-dialog.tsx +155 -0
  42. package/templates/ui/shadcn-ui/src/components/ui/alert.tsx +66 -0
  43. package/templates/ui/shadcn-ui/src/components/ui/aspect-ratio.tsx +11 -0
  44. package/templates/ui/shadcn-ui/src/components/ui/avatar.tsx +51 -0
  45. package/templates/ui/shadcn-ui/src/components/ui/badge.tsx +46 -0
  46. package/templates/ui/shadcn-ui/src/components/ui/breadcrumb.tsx +109 -0
  47. package/templates/ui/shadcn-ui/src/components/ui/button-group.tsx +83 -0
  48. package/templates/ui/shadcn-ui/src/components/ui/button.tsx +60 -0
  49. package/templates/ui/shadcn-ui/src/components/ui/calendar.tsx +216 -0
  50. package/templates/ui/shadcn-ui/src/components/ui/card.tsx +92 -0
  51. package/templates/ui/shadcn-ui/src/components/ui/carousel.tsx +239 -0
  52. package/templates/ui/shadcn-ui/src/components/ui/chart.tsx +357 -0
  53. package/templates/ui/shadcn-ui/src/components/ui/checkbox.tsx +32 -0
  54. package/templates/ui/shadcn-ui/src/components/ui/collapsible.tsx +31 -0
  55. package/templates/ui/shadcn-ui/src/components/ui/command.tsx +182 -0
  56. package/templates/ui/shadcn-ui/src/components/ui/context-menu.tsx +252 -0
  57. package/templates/ui/shadcn-ui/src/components/ui/dialog.tsx +141 -0
  58. package/templates/ui/shadcn-ui/src/components/ui/drawer.tsx +135 -0
  59. package/templates/ui/shadcn-ui/src/components/ui/dropdown-menu.tsx +255 -0
  60. package/templates/ui/shadcn-ui/src/components/ui/empty.tsx +104 -0
  61. package/templates/ui/shadcn-ui/src/components/ui/field.tsx +246 -0
  62. package/templates/ui/shadcn-ui/src/components/ui/form.tsx +167 -0
  63. package/templates/ui/shadcn-ui/src/components/ui/hover-card.tsx +44 -0
  64. package/templates/ui/shadcn-ui/src/components/ui/input-group.tsx +170 -0
  65. package/templates/ui/shadcn-ui/src/components/ui/input-otp.tsx +75 -0
  66. package/templates/ui/shadcn-ui/src/components/ui/input.tsx +21 -0
  67. package/templates/ui/shadcn-ui/src/components/ui/item.tsx +193 -0
  68. package/templates/ui/shadcn-ui/src/components/ui/kbd.tsx +28 -0
  69. package/templates/ui/shadcn-ui/src/components/ui/label.tsx +24 -0
  70. package/templates/ui/shadcn-ui/src/components/ui/menubar.tsx +274 -0
  71. package/templates/ui/shadcn-ui/src/components/ui/navigation-menu.tsx +168 -0
  72. package/templates/ui/shadcn-ui/src/components/ui/pagination.tsx +127 -0
  73. package/templates/ui/shadcn-ui/src/components/ui/popover.tsx +48 -0
  74. package/templates/ui/shadcn-ui/src/components/ui/progress.tsx +29 -0
  75. package/templates/ui/shadcn-ui/src/components/ui/radio-group.tsx +45 -0
  76. package/templates/ui/shadcn-ui/src/components/ui/resizable.tsx +54 -0
  77. package/templates/ui/shadcn-ui/src/components/ui/scroll-area.tsx +58 -0
  78. package/templates/ui/shadcn-ui/src/components/ui/select.tsx +185 -0
  79. package/templates/ui/shadcn-ui/src/components/ui/separator.tsx +28 -0
  80. package/templates/ui/shadcn-ui/src/components/ui/sheet.tsx +137 -0
  81. package/templates/ui/shadcn-ui/src/components/ui/sidebar.tsx +726 -0
  82. package/templates/ui/shadcn-ui/src/components/ui/skeleton.tsx +13 -0
  83. package/templates/ui/shadcn-ui/src/components/ui/slider.tsx +63 -0
  84. package/templates/ui/shadcn-ui/src/components/ui/sonner.tsx +38 -0
  85. package/templates/ui/shadcn-ui/src/components/ui/spinner.tsx +16 -0
  86. package/templates/ui/shadcn-ui/src/components/ui/switch.tsx +31 -0
  87. package/templates/ui/shadcn-ui/src/components/ui/table.tsx +114 -0
  88. package/templates/ui/shadcn-ui/src/components/ui/tabs.tsx +66 -0
  89. package/templates/ui/shadcn-ui/src/components/ui/textarea.tsx +18 -0
  90. package/templates/ui/shadcn-ui/src/components/ui/toggle-group.tsx +81 -0
  91. package/templates/ui/shadcn-ui/src/components/ui/toggle.tsx +45 -0
  92. package/templates/ui/shadcn-ui/src/components/ui/tooltip.tsx +61 -0
  93. package/templates/ui/shadcn-ui/src/hooks/use-mobile.ts +19 -0
  94. package/templates/ui/shadcn-ui/src/index.patch.css +121 -0
  95. package/templates/ui/shadcn-ui/src/lib/utils.ts +6 -0
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "base",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview",
11
+ "format": "biome format --write .",
12
+ "lint:biome": "biome lint .",
13
+ "check": "biome check --write --no-errors-on-unmatched .",
14
+ "format:check": "biome format --check .",
15
+ "lint:fix": "biome lint --apply ."
16
+ },
17
+ "dependencies": {
18
+ "@tailwindcss/vite": "^4.1.18",
19
+ "tailwindcss": "^4.1.18",
20
+ "@tanstack/react-query": "^5.90.16",
21
+ "@types/xrm": "^9.0.88",
22
+ "react": "^19.2.3",
23
+ "react-dom": "^19.2.3",
24
+ "zod": "^4.3.5",
25
+ "zustand": "^5.0.9"
26
+ },
27
+ "devDependencies": {
28
+ "@eslint/js": "^9.39.2",
29
+ "@tanstack/eslint-plugin-query": "^5.91.2",
30
+ "@types/node": "^24.10.4",
31
+ "@types/react": "^19.2.7",
32
+ "@types/react-dom": "^19.2.3",
33
+ "@vitejs/plugin-react": "^5.1.2",
34
+ "eslint": "^9.39.2",
35
+ "eslint-plugin-react-hooks": "^7.0.1",
36
+ "eslint-plugin-react-refresh": "^0.4.26",
37
+ "globals": "^16.5.0",
38
+ "typescript": "~5.9.3",
39
+ "typescript-eslint": "^8.51.0",
40
+ "vite": "^7.3.0"
41
+ }
42
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
File without changes
@@ -0,0 +1,11 @@
1
+ import "./App.css";
2
+
3
+ function App() {
4
+ return (
5
+ <div className="flex flex-col h-screen items-center justify-center">
6
+ <p>Hello, world!</p>
7
+ </div>
8
+ );
9
+ }
10
+
11
+ export default App;
@@ -0,0 +1,4 @@
1
+ declare module "*.css";
2
+ declare module "*.scss";
3
+ declare module "*.svg";
4
+ declare module "*.png";
@@ -0,0 +1 @@
1
+ @import "tailwindcss";
@@ -0,0 +1,29 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4
+
5
+ import "./index.css";
6
+ import App from "./App.tsx";
7
+
8
+ const queryClient = new QueryClient({
9
+ defaultOptions: {
10
+ queries: {
11
+ refetchOnWindowFocus: false,
12
+ retry: 3,
13
+ staleTime: 5 * 60 * 1000, // 5 minutes
14
+ },
15
+ mutations: {
16
+ retry: 1,
17
+ },
18
+ },
19
+ });
20
+
21
+ const root = createRoot(document.getElementById("root")!);
22
+
23
+ root.render(
24
+ <StrictMode>
25
+ <QueryClientProvider client={queryClient}>
26
+ <App />
27
+ </QueryClientProvider>
28
+ </StrictMode>
29
+ );
@@ -0,0 +1,32 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "types": ["vite/client"],
9
+ "skipLibCheck": true,
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+ "jsx": "react-jsx",
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true,
24
+
25
+ "baseUrl": ".",
26
+ "paths": {
27
+ "@/*": ["./src/*"]
28
+ }
29
+ },
30
+ "include": ["src"]
31
+ }
32
+
@@ -0,0 +1,19 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ {
5
+ "path": "./tsconfig.app.json"
6
+ },
7
+ {
8
+ "path": "./tsconfig.node.json"
9
+ }
10
+ ],
11
+ "compilerOptions": {
12
+ "baseUrl": ".",
13
+ "paths": {
14
+ "@/*": [
15
+ "./src/*"
16
+ ]
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "types": ["node"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["vite.config.ts"]
26
+ }
@@ -0,0 +1,14 @@
1
+ import path from "node:path";
2
+ import tailwindcss from "@tailwindcss/vite";
3
+ import react from "@vitejs/plugin-react";
4
+ import { defineConfig } from "vite";
5
+
6
+ // https://vite.dev/config/
7
+ export default defineConfig({
8
+ plugins: [react(), tailwindcss()],
9
+ resolve: {
10
+ alias: {
11
+ "@": path.resolve(__dirname, "./src"),
12
+ },
13
+ },
14
+ });
@@ -0,0 +1,3 @@
1
+ VITE_AAD_CLIENT_ID=db392465-21e2-4e5f-9fa5-a25d4dab354f
2
+ VITE_AAD_TENANT_ID=961b8f6e-3902-49b5-b679-5beabfc73016
3
+ VITE_AUTH_DEBUG=true
@@ -0,0 +1,180 @@
1
+ # EC Power Pages Single Page Application (SPA)
2
+
3
+ This is a Power Pages Single Page Application (SPA) built with React, Vite, and TypeScript, generated by `create-ec-app`. It follows Microsoft’s official Power Pages code site model, providing a modern, scalable, and maintainable frontend architecture tailored specifically for Power Pages environments. The app includes integrated authentication, data fetching, and UI options designed to work seamlessly within the Power Pages runtime.
4
+
5
+ ## Features
6
+
7
+ - **React + Vite + TypeScript**: Modern frontend stack for fast development and optimized builds.
8
+ - **Tailwind CSS**: Utility-first CSS framework with an optional Kendo UI preset for styling consistency.
9
+ - **UI Library Choice**: Select between Kendo UI (including theme selection) or Shadcn/ui components.
10
+ - **TanStack Query**: Preconfigured React Query provider for efficient data fetching and caching.
11
+ - **ADAL Authentication**: Integrated Azure Active Directory Authentication Library (ADAL) for Entra ID authentication with token caching.
12
+ - **Local Development Support**: Full authentication support during local development using ADAL
13
+ - **Optimized Vite Build Output**: Builds optimized assets tailored for upload to Power Pages code sites.
14
+
15
+ ## Auth and API Access
16
+
17
+ This SPA uses ADAL (Azure Active Directory Authentication Library) for authentication. While ADAL is deprecated by Microsoft, it remains functional for Power Pages scenarios until MSAL v2 support is available.
18
+
19
+ ### Authentication Setup
20
+
21
+ The `AuthContext` requires the following environment variables:
22
+
23
+ - `VITE_AAD_TENANT_ID`: Your Azure AD tenant ID (defaults to "common")
24
+ - `VITE_AAD_CLIENT_ID`: Your Azure AD application (client) ID (required)
25
+ - `VITE_AUTH_DEBUG`: Optional, set to "true" for verbose authentication logging
26
+
27
+ ### How It Works
28
+
29
+ 1. **Script Loading**: ADAL is dynamically loaded from Microsoft's CDN (`https://secure.aadcdn.microsoftonline-p.com/lib/1.0.17/js/adal.min.js`)
30
+ 2. **Token Caching**: Authentication tokens are cached in localStorage for persistence
31
+ 3. **User Information**: The authenticated user object includes `userName`, `displayableId`, `name`, `givenName`, `familyName`, and `idToken`
32
+ 4. **Token Retrieval**: Access ID tokens via the `getIdToken()` method from the auth context
33
+
34
+ ### Authentication Flow
35
+
36
+ - When users call `login()`, they're redirected to Azure AD for authentication
37
+ - After successful authentication, users are redirected back with tokens
38
+ - Tokens are automatically cached and reused for subsequent requests
39
+ - Call `logout()` to clear tokens and sign out the user
40
+
41
+ ## Example Data Fetch Using TanStack Query and AuthContext
42
+
43
+ Here is an example of a service module `src/services/contacts.ts` that demonstrates fetching contacts data from the Power Pages API using TanStack Query:
44
+
45
+ ```ts
46
+ import { useQuery } from "@tanstack/react-query";
47
+ import { AuthButton } from "./components/shared/AuthButton";
48
+ import { useAuth } from "./context/AuthContext";
49
+
50
+ type Account = {
51
+ name: string | null;
52
+ accountnumber: string | null;
53
+ accountid: string;
54
+ };
55
+
56
+ function App() {
57
+ const { isAuthenticated, user } = useAuth();
58
+
59
+ const { data: accounts, isLoading } = useQuery({
60
+ queryKey: ["accounts"],
61
+ queryFn: async () => {
62
+ const response = await fetch(
63
+ `_api/accounts?$select=name,accountnumber&$top=5`,
64
+ {
65
+ method: "GET",
66
+ headers: {
67
+ "Content-Type": "application/json",
68
+ Authorization: `Bearer ${user?.idToken}`,
69
+ },
70
+ }
71
+ );
72
+
73
+ if (!response.ok) {
74
+ throw new Error("Network response was not ok");
75
+ }
76
+ return response.json();
77
+ },
78
+ enabled: isAuthenticated && !!user,
79
+ });
80
+
81
+ if (isLoading) {
82
+ return (
83
+ <div className="flex flex-col items-center justify-center h-screen">
84
+ <div>Loading...</div>
85
+ </div>
86
+ );
87
+ }
88
+
89
+ return (
90
+ <div className="flex flex-col items-center justify-center h-screen gap-4">
91
+ {accounts ? (
92
+ <div>
93
+ <h2 className="mb-4 text-xl font-bold">Accounts:</h2>
94
+ <ul className="list-disc list-inside">
95
+ {accounts.value.map((account: Account) => (
96
+ <li key={account.accountid}>
97
+ {account.name} - {account.accountnumber}
98
+ </li>
99
+ ))}
100
+ </ul>
101
+ </div>
102
+ ) : (
103
+ <div>
104
+ {isAuthenticated ? (
105
+ <div>No accounts data available.</div>
106
+ ) : (
107
+ <>
108
+ <div>Please log in to view your accounts.</div>
109
+ <AuthButton />
110
+ </>
111
+ )}
112
+ </div>
113
+ )}
114
+ </div>
115
+ );
116
+ }
117
+
118
+ export default App;
119
+ ```
120
+
121
+ ## Creating an Auth Button
122
+
123
+ ```tsx
124
+ import { useAuth } from "@/context/AuthContext";
125
+ import { Button } from "../ui/button";
126
+
127
+ export const AuthButton = () => {
128
+ const { isAuthenticated, user, login, logout } = useAuth();
129
+ const displayName = user?.name || user?.userName || user?.givenName || "";
130
+
131
+ return (
132
+ <div className="flex flex-col items-center gap-4">
133
+ {isAuthenticated ? (
134
+ <>
135
+ {displayName && <span>Welcome {displayName}</span>}
136
+ <Button variant="default" onClick={logout}>
137
+ Sign Out
138
+ </Button>
139
+ </>
140
+ ) : (
141
+ <Button onClick={login} variant="outline">
142
+ Sign In
143
+ </Button>
144
+ )}
145
+ </div>
146
+ );
147
+ };
148
+ ```
149
+
150
+ ## UI Libraries
151
+
152
+ - Kendo UI: Theme CSS is imported in `src/main.tsx`. Example:
153
+
154
+ ```tsx
155
+ import { Button } from "@progress/kendo-react-buttons";
156
+ <Button>Click me</Button>;
157
+ ```
158
+
159
+ - Shadcn/ui: Components are installed and available under the `@/components` alias. Example:
160
+
161
+ ```tsx
162
+ import { Button } from "@/components/ui/button";
163
+ <Button>Click me</Button>;
164
+ ```
165
+
166
+ ## Deployment
167
+
168
+ To deploy your Power Pages SPA:
169
+
170
+ - Build the project using `npm run build` or `npm run build:dev` for development builds.
171
+ - Upload the contents of the `dist` folder to your Power Pages site’s **code site**. This is a dedicated location for hosting SPA assets as static content.
172
+ - Reference your built JavaScript and CSS files in your Power Pages pages as needed.
173
+
174
+ For detailed guidance on Power Pages code sites and deployment, see the [Microsoft Power Pages documentation on code sites](https://learn.microsoft.com/en-us/power-pages/developer/code-sites).
175
+
176
+ Power Pages code sites support continuous deployment workflows via the Power Platform CLI, enabling streamlined updates and integration with source control.
177
+
178
+ ## Configuration
179
+
180
+ The `powerpages.config.json` file defines your site configuration, including output paths and landing page settings. Adjust this file to match your project and deployment conventions.
@@ -0,0 +1,5 @@
1
+ {
2
+ "compiledPath": "./dist",
3
+ "siteName": "SITE_NAME",
4
+ "defaultLandingPage": "index.html"
5
+ }
@@ -0,0 +1,17 @@
1
+ import { useAuth } from "./context/AuthContext";
2
+
3
+ function App() {
4
+ const { isAuthenticated, user } = useAuth(); //INFO: User is where the token information is stored (user?.idToken)
5
+
6
+ return (
7
+ <div className="flex flex-col items-center justify-center h-screen gap-4">
8
+ {isAuthenticated ? (
9
+ <div>You are logged in</div>
10
+ ) : (
11
+ <div>Please Log In</div>
12
+ )}
13
+ </div>
14
+ );
15
+ }
16
+
17
+ export default App;
@@ -0,0 +1,273 @@
1
+ import {
2
+ createContext,
3
+ type ReactNode,
4
+ useCallback,
5
+ useContext,
6
+ useEffect,
7
+ useRef,
8
+ useState,
9
+ } from "react";
10
+
11
+ // NOTE: ADAL is deprecated. Prefer migrating to MSAL V2 when it is available
12
+ // Env vars required:
13
+ // VITE_AAD_TENANT_ID
14
+ // VITE_AAD_CLIENT_ID
15
+ // Optional:
16
+ // VITE_AUTH_DEBUG=true for verbose logging.
17
+
18
+ const AAD_TENANT_ID =
19
+ (import.meta as ImportMeta).env?.VITE_AAD_TENANT_ID || "common";
20
+ const AAD_CLIENT_ID = (import.meta as ImportMeta).env?.VITE_AAD_CLIENT_ID || "";
21
+ const AUTH_DEBUG =
22
+ ((import.meta as ImportMeta).env?.VITE_AUTH_DEBUG ?? "")
23
+ .toString()
24
+ .toLowerCase() === "true";
25
+ if (!AAD_CLIENT_ID)
26
+ console.warn(
27
+ "VITE_AAD_CLIENT_ID is not set. Entra (ADAL) login will fail."
28
+ );
29
+
30
+ interface AADUserInfo {
31
+ userName?: string;
32
+ profile?: Record<string, unknown>;
33
+ idToken?: string;
34
+ displayableId?: string;
35
+ name?: string;
36
+ givenName?: string;
37
+ familyName?: string;
38
+ username?: string;
39
+ }
40
+
41
+ declare global {
42
+ interface Window {
43
+ AuthenticationContext?: {
44
+ new (config: Record<string, unknown>): ADALContext;
45
+ } & ((config: Record<string, unknown>) => ADALContext);
46
+ }
47
+ }
48
+
49
+ interface ADALContext {
50
+ isCallback: (hash: string) => boolean;
51
+ handleWindowCallback: () => void;
52
+ getLoginError: () => string | null;
53
+ getCachedUser: () => {
54
+ userName?: string;
55
+ profile?: Record<string, unknown>;
56
+ } | null;
57
+ getCachedToken: (resource: string) => string | null;
58
+ login: () => void;
59
+ logOut: () => void;
60
+ }
61
+
62
+ interface AuthContextType {
63
+ isAuthenticated: boolean;
64
+ user: AADUserInfo | null;
65
+ login: () => Promise<void> | void;
66
+ logout: () => void;
67
+ error: string | null;
68
+ clearError: () => void;
69
+ getIdToken: () => Promise<string | null>;
70
+ }
71
+
72
+ const AuthContext = createContext<AuthContextType | null>(null);
73
+
74
+ export const useAuth = () => {
75
+ const context = useContext(AuthContext);
76
+ if (!context) {
77
+ throw new Error("useAuth must be used within an AuthProvider");
78
+ }
79
+ return context;
80
+ };
81
+
82
+ interface AuthProviderProps {
83
+ children: ReactNode;
84
+ }
85
+
86
+ export const AuthProvider = ({ children }: AuthProviderProps) => (
87
+ <AuthProviderContent>{children}</AuthProviderContent>
88
+ );
89
+
90
+ const AuthProviderContent = ({ children }: { children: ReactNode }) => {
91
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
92
+ const [user, setUser] = useState<AADUserInfo | null>(null);
93
+ const [idToken, setIdToken] = useState<string | null>(null);
94
+ const [error, setError] = useState<string | null>(null);
95
+ const scriptLoadedRef = useRef(false);
96
+ const adalContextRef = useRef<ADALContext | null>(null);
97
+
98
+ const clearError = () => setError(null);
99
+
100
+ const logDebug = useCallback((...args: unknown[]) => {
101
+ if (AUTH_DEBUG) console.log("[AuthDebug]", ...args);
102
+ }, []);
103
+
104
+ //INFO: We load the ADAL script from Microsoft's CDN. The alternative is to do it via npm, but it's deprecated.
105
+ const loadAdalScript = useCallback((): Promise<void> => {
106
+ if (scriptLoadedRef.current) return Promise.resolve();
107
+ return new Promise((resolve, reject) => {
108
+ const existing = document.querySelector(
109
+ "script[data-adal]"
110
+ ) as HTMLScriptElement | null;
111
+ if (existing) {
112
+ scriptLoadedRef.current = true;
113
+ resolve();
114
+ return;
115
+ }
116
+ const script = document.createElement("script");
117
+
118
+ //INFO: current version is pinned. Update this as needed.
119
+ script.src =
120
+ "https://secure.aadcdn.microsoftonline-p.com/lib/1.0.17/js/adal.min.js";
121
+ script.async = true;
122
+ script.defer = true;
123
+ script.setAttribute("data-adal", "true");
124
+ script.onload = () => {
125
+ scriptLoadedRef.current = true;
126
+ resolve();
127
+ };
128
+ script.onerror = (e) => reject(e);
129
+ document.head.appendChild(script);
130
+ });
131
+ }, []);
132
+
133
+ const buildAdalConfig = (): Record<string, unknown> => ({
134
+ tenant: AAD_TENANT_ID,
135
+ clientId: AAD_CLIENT_ID,
136
+ redirectUri: window.location.origin,
137
+ cacheLocation: "localStorage", //INFO: check what is required. Might be sessionStorage might be localStorage
138
+ navigateToLoginRequestUrl: false,
139
+ // endpoints: { 'https://graph.microsoft.com': 'https://graph.microsoft.com' }, //INFO: Add more resources as needed
140
+ extraQueryParameter: "prompt=select_account",
141
+ });
142
+
143
+ const ensureAdalContext = useCallback((): ADALContext | null => {
144
+ if (
145
+ !adalContextRef.current &&
146
+ window.AuthenticationContext &&
147
+ AAD_CLIENT_ID
148
+ ) {
149
+ try {
150
+ const Ctx = window.AuthenticationContext as unknown as {
151
+ new (config: Record<string, unknown>): ADALContext;
152
+ };
153
+ adalContextRef.current = new Ctx(buildAdalConfig());
154
+ logDebug("ADAL context created");
155
+ } catch (e) {
156
+ console.warn("Failed to create ADAL context", e);
157
+ }
158
+ }
159
+ return adalContextRef.current;
160
+ }, [logDebug]);
161
+
162
+ const processCallbackIfPresent = useCallback(() => {
163
+ const ctx = ensureAdalContext();
164
+ if (!ctx) return;
165
+ if (ctx.isCallback(window.location.hash)) {
166
+ ctx.handleWindowCallback();
167
+ const err = ctx.getLoginError();
168
+ if (err) {
169
+ setError(err);
170
+ }
171
+ }
172
+ }, [ensureAdalContext]);
173
+
174
+ const populateFromCache = useCallback(() => {
175
+ const ctx = ensureAdalContext();
176
+ if (!ctx) return;
177
+ const cachedUser = ctx.getCachedUser();
178
+ const cachedIdToken = ctx.getCachedToken(AAD_CLIENT_ID);
179
+ if (cachedUser && cachedIdToken) {
180
+ const profile = cachedUser.profile || {};
181
+ const profileRecord = profile as Record<string, unknown>;
182
+ const str = (v: unknown) => (typeof v === "string" ? v : undefined);
183
+ const mapped: AADUserInfo = {
184
+ userName: cachedUser.userName,
185
+ displayableId: str(profileRecord["upn"]) || cachedUser.userName,
186
+ name: str(profileRecord["name"]),
187
+ givenName: str(profileRecord["given_name"]),
188
+ familyName: str(profileRecord["family_name"]),
189
+ username: cachedUser.userName,
190
+ idToken: cachedIdToken,
191
+ profile,
192
+ };
193
+ setUser(mapped);
194
+ setIdToken(cachedIdToken);
195
+ setIsAuthenticated(true);
196
+ } else {
197
+ setIsAuthenticated(false);
198
+ }
199
+ }, [ensureAdalContext]);
200
+
201
+ const login = async () => {
202
+ setError(null);
203
+ if (!AAD_CLIENT_ID) {
204
+ setError("AAD client ID not configured.");
205
+ return;
206
+ }
207
+ try {
208
+ await loadAdalScript();
209
+ const ctx = ensureAdalContext();
210
+ if (!ctx) {
211
+ setError("ADAL context not available.");
212
+ return;
213
+ }
214
+ ctx.login();
215
+ } catch (e) {
216
+ console.error("AAD login failed", e);
217
+ setError("Login failed.");
218
+ }
219
+ };
220
+
221
+ const logout = () => {
222
+ try {
223
+ const ctx = ensureAdalContext();
224
+ if (ctx) ctx.logOut();
225
+ setIsAuthenticated(false);
226
+ setUser(null);
227
+ setIdToken(null);
228
+ } catch (e) {
229
+ console.error("Logout failed", e);
230
+ setError("Logout failed.");
231
+ }
232
+ };
233
+
234
+ const getIdToken = async (): Promise<string | null> => idToken;
235
+
236
+ useEffect(() => {
237
+ loadAdalScript()
238
+ .then(() => {
239
+ processCallbackIfPresent();
240
+ populateFromCache();
241
+ })
242
+ .catch((e) => {
243
+ console.warn("Failed to load ADAL script", e);
244
+ setError("Failed to load authentication library.");
245
+ });
246
+ if (AUTH_DEBUG) {
247
+ console.log("[AuthDebug] Mounted AuthProvider (ADAL)", {
248
+ origin: window.location.origin,
249
+ tenant: AAD_TENANT_ID,
250
+ clientIdPresent: !!AAD_CLIENT_ID,
251
+ time: new Date().toISOString(),
252
+ });
253
+ }
254
+ }, [loadAdalScript, processCallbackIfPresent, populateFromCache]);
255
+
256
+ const value = {
257
+ isAuthenticated,
258
+ user,
259
+ login,
260
+ logout,
261
+ error,
262
+ clearError,
263
+ getIdToken,
264
+ };
265
+
266
+ if (error) {
267
+ throw new Error(`Authentication error: ${error}`);
268
+ }
269
+
270
+ return (
271
+ <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
272
+ );
273
+ };