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.
- package/package.json +3 -2
- package/templates/base/.prettierrc.json +12 -0
- package/templates/base/README.md +73 -0
- package/templates/base/biome.json +54 -0
- package/templates/base/eslint.config.js +23 -0
- package/templates/base/index.html +13 -0
- package/templates/base/package-lock.json +3898 -0
- package/templates/base/package.json +42 -0
- package/templates/base/public/vite.svg +1 -0
- package/templates/base/src/App.css +0 -0
- package/templates/base/src/App.tsx +11 -0
- package/templates/base/src/global.d.ts +4 -0
- package/templates/base/src/index.css +1 -0
- package/templates/base/src/main.tsx +29 -0
- package/templates/base/tsconfig.app.json +32 -0
- package/templates/base/tsconfig.json +19 -0
- package/templates/base/tsconfig.node.json +26 -0
- package/templates/base/vite.config.ts +14 -0
- package/templates/targets/power-pages/.env +3 -0
- package/templates/targets/power-pages/README.md +180 -0
- package/templates/targets/power-pages/powerpages.config.json +5 -0
- package/templates/targets/power-pages/src/App.patch.tsx +17 -0
- package/templates/targets/power-pages/src/context/AuthContext.tsx +273 -0
- package/templates/targets/power-pages/src/main.patch.tsx +32 -0
- package/templates/targets/power-pages/vite.config.patch.ts +23 -0
- package/templates/targets/swa/.env +2 -0
- package/templates/targets/swa/package.patch.json +5 -0
- package/templates/targets/swa/staticwebapp.config.json +5 -0
- package/templates/targets/swa/swa-cli.config.json +14 -0
- package/templates/targets/webresource/README.md +263 -0
- package/templates/targets/webresource/src/services/AuthService.ts +38 -0
- package/templates/targets/webresource/token.json +8 -0
- package/templates/targets/webresource/vite.config.patch.ts +40 -0
- package/templates/ui/kendo/kendo-tw-preset.js +240 -0
- package/templates/ui/kendo/package.patch.json +7 -0
- package/templates/ui/kendo/src/main.patch.tsx +29 -0
- package/templates/ui/kendo/tailwind.config.js +14 -0
- package/templates/ui/shadcn-ui/components.json +22 -0
- package/templates/ui/shadcn-ui/package.patch.json +50 -0
- package/templates/ui/shadcn-ui/src/components/ui/accordion.tsx +64 -0
- package/templates/ui/shadcn-ui/src/components/ui/alert-dialog.tsx +155 -0
- package/templates/ui/shadcn-ui/src/components/ui/alert.tsx +66 -0
- package/templates/ui/shadcn-ui/src/components/ui/aspect-ratio.tsx +11 -0
- package/templates/ui/shadcn-ui/src/components/ui/avatar.tsx +51 -0
- package/templates/ui/shadcn-ui/src/components/ui/badge.tsx +46 -0
- package/templates/ui/shadcn-ui/src/components/ui/breadcrumb.tsx +109 -0
- package/templates/ui/shadcn-ui/src/components/ui/button-group.tsx +83 -0
- package/templates/ui/shadcn-ui/src/components/ui/button.tsx +60 -0
- package/templates/ui/shadcn-ui/src/components/ui/calendar.tsx +216 -0
- package/templates/ui/shadcn-ui/src/components/ui/card.tsx +92 -0
- package/templates/ui/shadcn-ui/src/components/ui/carousel.tsx +239 -0
- package/templates/ui/shadcn-ui/src/components/ui/chart.tsx +357 -0
- package/templates/ui/shadcn-ui/src/components/ui/checkbox.tsx +32 -0
- package/templates/ui/shadcn-ui/src/components/ui/collapsible.tsx +31 -0
- package/templates/ui/shadcn-ui/src/components/ui/command.tsx +182 -0
- package/templates/ui/shadcn-ui/src/components/ui/context-menu.tsx +252 -0
- package/templates/ui/shadcn-ui/src/components/ui/dialog.tsx +141 -0
- package/templates/ui/shadcn-ui/src/components/ui/drawer.tsx +135 -0
- package/templates/ui/shadcn-ui/src/components/ui/dropdown-menu.tsx +255 -0
- package/templates/ui/shadcn-ui/src/components/ui/empty.tsx +104 -0
- package/templates/ui/shadcn-ui/src/components/ui/field.tsx +246 -0
- package/templates/ui/shadcn-ui/src/components/ui/form.tsx +167 -0
- package/templates/ui/shadcn-ui/src/components/ui/hover-card.tsx +44 -0
- package/templates/ui/shadcn-ui/src/components/ui/input-group.tsx +170 -0
- package/templates/ui/shadcn-ui/src/components/ui/input-otp.tsx +75 -0
- package/templates/ui/shadcn-ui/src/components/ui/input.tsx +21 -0
- package/templates/ui/shadcn-ui/src/components/ui/item.tsx +193 -0
- package/templates/ui/shadcn-ui/src/components/ui/kbd.tsx +28 -0
- package/templates/ui/shadcn-ui/src/components/ui/label.tsx +24 -0
- package/templates/ui/shadcn-ui/src/components/ui/menubar.tsx +274 -0
- package/templates/ui/shadcn-ui/src/components/ui/navigation-menu.tsx +168 -0
- package/templates/ui/shadcn-ui/src/components/ui/pagination.tsx +127 -0
- package/templates/ui/shadcn-ui/src/components/ui/popover.tsx +48 -0
- package/templates/ui/shadcn-ui/src/components/ui/progress.tsx +29 -0
- package/templates/ui/shadcn-ui/src/components/ui/radio-group.tsx +45 -0
- package/templates/ui/shadcn-ui/src/components/ui/resizable.tsx +54 -0
- package/templates/ui/shadcn-ui/src/components/ui/scroll-area.tsx +58 -0
- package/templates/ui/shadcn-ui/src/components/ui/select.tsx +185 -0
- package/templates/ui/shadcn-ui/src/components/ui/separator.tsx +28 -0
- package/templates/ui/shadcn-ui/src/components/ui/sheet.tsx +137 -0
- package/templates/ui/shadcn-ui/src/components/ui/sidebar.tsx +726 -0
- package/templates/ui/shadcn-ui/src/components/ui/skeleton.tsx +13 -0
- package/templates/ui/shadcn-ui/src/components/ui/slider.tsx +63 -0
- package/templates/ui/shadcn-ui/src/components/ui/sonner.tsx +38 -0
- package/templates/ui/shadcn-ui/src/components/ui/spinner.tsx +16 -0
- package/templates/ui/shadcn-ui/src/components/ui/switch.tsx +31 -0
- package/templates/ui/shadcn-ui/src/components/ui/table.tsx +114 -0
- package/templates/ui/shadcn-ui/src/components/ui/tabs.tsx +66 -0
- package/templates/ui/shadcn-ui/src/components/ui/textarea.tsx +18 -0
- package/templates/ui/shadcn-ui/src/components/ui/toggle-group.tsx +81 -0
- package/templates/ui/shadcn-ui/src/components/ui/toggle.tsx +45 -0
- package/templates/ui/shadcn-ui/src/components/ui/tooltip.tsx +61 -0
- package/templates/ui/shadcn-ui/src/hooks/use-mobile.ts +19 -0
- package/templates/ui/shadcn-ui/src/index.patch.css +121 -0
- 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 @@
|
|
|
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,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,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,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
|
+
};
|