igniteui-cli 15.2.2-alpha.3 → 15.3.0

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 (175) hide show
  1. package/lib/PromptSession.js +1 -1
  2. package/lib/commands/ai-config.d.ts +9 -2
  3. package/lib/commands/ai-config.js +49 -14
  4. package/lib/commands/build.js +7 -12
  5. package/lib/commands/new.js +4 -1
  6. package/package.json +4 -4
  7. package/templates/blazor/igb/projects/ai-config/files/skills/AGENTS.md +0 -5
  8. package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-components/SKILL.md +3 -1
  9. package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-components/references/charts.md +7 -35
  10. package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-components/references/data-display.md +1 -54
  11. package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-components/references/feedback.md +0 -38
  12. package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-components/references/form-controls.md +0 -68
  13. package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-components/references/layout-manager.md +1 -124
  14. package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-components/references/layout.md +0 -62
  15. package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-generate-from-image-design/references/gotchas.md +29 -6
  16. package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-theming/SKILL.md +2 -2
  17. package/templates/react/igr-ts/accordion/default/index.js +2 -1
  18. package/templates/react/igr-ts/avatar/default/index.js +2 -1
  19. package/templates/react/igr-ts/badge/default/index.js +2 -1
  20. package/templates/react/igr-ts/banner/default/index.js +2 -1
  21. package/templates/react/igr-ts/button/default/index.js +2 -1
  22. package/templates/react/igr-ts/button-group/default/index.js +2 -1
  23. package/templates/react/igr-ts/calendar/default/index.js +2 -1
  24. package/templates/react/igr-ts/card/default/index.js +2 -1
  25. package/templates/react/igr-ts/checkbox/default/index.js +2 -1
  26. package/templates/react/igr-ts/chip/default/index.js +2 -1
  27. package/templates/react/igr-ts/circular-progress/default/index.js +2 -1
  28. package/templates/react/igr-ts/constants.d.ts +2 -0
  29. package/templates/react/igr-ts/constants.js +5 -0
  30. package/templates/react/igr-ts/custom-templates/subscription-form/index.js +2 -1
  31. package/templates/react/igr-ts/date-picker/default/index.js +2 -1
  32. package/templates/react/igr-ts/divider/default/index.js +2 -1
  33. package/templates/react/igr-ts/dropdown/default/index.js +2 -1
  34. package/templates/react/igr-ts/expansion-panel/default/index.js +2 -1
  35. package/templates/react/igr-ts/form/default/index.js +2 -1
  36. package/templates/react/igr-ts/grid/basic/index.js +2 -1
  37. package/templates/react/igr-ts/icon/default/index.js +2 -1
  38. package/templates/react/igr-ts/icon-button/default/index.js +2 -1
  39. package/templates/react/igr-ts/input/default/index.js +2 -1
  40. package/templates/react/igr-ts/linear-progress/default/index.js +2 -1
  41. package/templates/react/igr-ts/list/default/index.js +2 -1
  42. package/templates/react/igr-ts/navbar/default/index.js +2 -1
  43. package/templates/react/igr-ts/projects/_base/files/package.json +2 -1
  44. package/templates/react/igr-ts/projects/_base/files/src/app/app.tsx +4 -2
  45. package/templates/react/igr-ts/projects/_base/files/src/setupTests.ts +12 -0
  46. package/templates/react/igr-ts/projects/_base/files/styles.css +6 -0
  47. package/templates/react/igr-ts/projects/_base_with_home/files/index.html +2 -1
  48. package/templates/react/igr-ts/projects/_base_with_home/files/src/app/home/home.tsx +60 -10
  49. package/templates/react/igr-ts/projects/_base_with_home/files/src/app/home/style.module.css +79 -20
  50. package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-components/SKILL.md +0 -8
  51. package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-components/reference/CHARTS-GRIDS.md +6 -36
  52. package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-components/reference/COMPONENT-CATALOGUE.md +8 -142
  53. package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-components/reference/EVENT-HANDLING.md +2 -0
  54. package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-customize-theme/SKILL.md +7 -14
  55. package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-customize-theme/reference/CSS-THEMING.md +2 -0
  56. package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-customize-theme/reference/MCP-SERVER.md +0 -8
  57. package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-generate-from-image-design/SKILL.md +2 -2
  58. package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-generate-from-image-design/reference/component-mapping.md +60 -74
  59. package/templates/react/igr-ts/projects/empty/index.js +2 -2
  60. package/templates/react/igr-ts/projects/side-nav/files/src/app/app-routes.tsx +5 -0
  61. package/templates/react/igr-ts/projects/side-nav/files/src/app/app.css +82 -0
  62. package/templates/react/igr-ts/projects/side-nav/files/src/app/app.tsx +104 -0
  63. package/templates/react/igr-ts/projects/side-nav/files/src/app/home/home.tsx +69 -0
  64. package/templates/react/igr-ts/projects/side-nav/files/src/app/home/style.module.css +105 -0
  65. package/templates/react/igr-ts/projects/{top-nav → side-nav}/index.d.ts +2 -2
  66. package/templates/react/igr-ts/projects/{top-nav → side-nav}/index.js +7 -7
  67. package/templates/react/igr-ts/projects/side-nav-auth/files/index.html +19 -0
  68. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/app-routes.tsx +24 -0
  69. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/app.css +84 -0
  70. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/app.tsx +124 -0
  71. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/AuthContext.tsx +73 -0
  72. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/AuthGuard.tsx +14 -0
  73. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/components/Login.module.css +93 -0
  74. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/components/Login.tsx +69 -0
  75. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/components/LoginBar.module.css +42 -0
  76. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/components/LoginBar.tsx +44 -0
  77. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/components/LoginDialog.module.css +14 -0
  78. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/components/LoginDialog.tsx +49 -0
  79. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/components/Register.module.css +74 -0
  80. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/components/Register.tsx +67 -0
  81. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/models/external-login.ts +10 -0
  82. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/models/login.ts +4 -0
  83. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/models/register-info.ts +6 -0
  84. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/models/user.ts +19 -0
  85. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/pages/Profile.module.css +87 -0
  86. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/pages/Profile.tsx +42 -0
  87. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/pages/RedirectFacebook.tsx +44 -0
  88. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/pages/RedirectGoogle.tsx +40 -0
  89. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/pages/RedirectMicrosoft.tsx +40 -0
  90. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/authentication.ts +37 -0
  91. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/external-auth-config.ts +44 -0
  92. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/externalAuth.ts +272 -0
  93. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/fakeBackend.ts +88 -0
  94. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/jwtUtil.ts +10 -0
  95. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/pkce.ts +29 -0
  96. package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/userStore.ts +39 -0
  97. package/templates/react/igr-ts/projects/side-nav-auth/index.d.ts +15 -0
  98. package/templates/react/igr-ts/projects/side-nav-auth/index.js +46 -0
  99. package/templates/react/igr-ts/projects/side-nav-mini/files/src/app/app-routes.tsx +5 -0
  100. package/templates/react/igr-ts/projects/side-nav-mini/files/src/app/app.css +109 -0
  101. package/templates/react/igr-ts/projects/side-nav-mini/files/src/app/app.test.tsx +20 -0
  102. package/templates/react/igr-ts/projects/side-nav-mini/files/src/app/app.tsx +81 -0
  103. package/templates/react/igr-ts/projects/side-nav-mini/files/src/app/home/home.tsx +69 -0
  104. package/templates/react/igr-ts/projects/side-nav-mini/files/src/app/home/style.module.css +105 -0
  105. package/templates/react/igr-ts/projects/side-nav-mini/index.d.ts +15 -0
  106. package/templates/react/igr-ts/projects/side-nav-mini/index.js +46 -0
  107. package/templates/react/igr-ts/projects/side-nav-mini-auth/files/src/app/app.css +106 -0
  108. package/templates/react/igr-ts/projects/side-nav-mini-auth/files/src/app/app.tsx +101 -0
  109. package/templates/react/igr-ts/projects/side-nav-mini-auth/index.d.ts +15 -0
  110. package/templates/react/igr-ts/projects/side-nav-mini-auth/index.js +50 -0
  111. package/templates/react/igr-ts/radio-group/default/index.js +2 -1
  112. package/templates/react/igr-ts/rating/default/index.js +2 -1
  113. package/templates/react/igr-ts/ripple/default/index.js +2 -1
  114. package/templates/react/igr-ts/slider/default/index.js +2 -1
  115. package/templates/react/igr-ts/switch/default/index.js +2 -1
  116. package/templates/react/igr-ts/tabs/default/index.js +2 -1
  117. package/templates/react/igr-ts/text-area/default/index.js +2 -1
  118. package/templates/react/igr-ts/tree/default/index.js +2 -1
  119. package/templates/webcomponents/igc-ts/grid/default/index.js +1 -1
  120. package/templates/webcomponents/igc-ts/grid/grid-editing/index.js +1 -1
  121. package/templates/webcomponents/igc-ts/grid/grid-summaries/index.js +1 -1
  122. package/templates/webcomponents/igc-ts/projects/_base/files/package.json +1 -1
  123. package/templates/webcomponents/igc-ts/projects/_base/files/src/app/app.ts +6 -1
  124. package/templates/webcomponents/igc-ts/projects/_base/files/styles.css +1 -0
  125. package/templates/webcomponents/igc-ts/projects/_base_with_home/files/index.html +2 -0
  126. package/templates/webcomponents/igc-ts/projects/_base_with_home/files/package.json +2 -2
  127. package/templates/webcomponents/igc-ts/projects/_base_with_home/files/src/app/home/home.ts +103 -9
  128. package/templates/webcomponents/igc-ts/projects/_base_with_home/files/src/assets/wc.png +0 -0
  129. package/templates/webcomponents/igc-ts/projects/ai-config/files/skills/igniteui-wc-choose-components/SKILL.md +122 -160
  130. package/templates/webcomponents/igc-ts/projects/ai-config/files/skills/igniteui-wc-customize-component-theme/SKILL.md +83 -311
  131. package/templates/webcomponents/igc-ts/projects/ai-config/files/skills/igniteui-wc-customize-component-theme/references/mcp-setup.md +69 -0
  132. package/templates/webcomponents/igc-ts/projects/ai-config/files/skills/igniteui-wc-generate-from-image-design/SKILL.md +4 -1
  133. package/templates/webcomponents/igc-ts/projects/ai-config/files/skills/igniteui-wc-generate-from-image-design/references/component-mapping.md +60 -61
  134. package/templates/webcomponents/igc-ts/projects/ai-config/files/skills/igniteui-wc-generate-from-image-design/references/gotchas.md +15 -11
  135. package/templates/webcomponents/igc-ts/projects/ai-config/files/skills/igniteui-wc-optimize-bundle-size/SKILL.md +23 -274
  136. package/templates/webcomponents/igc-ts/projects/empty/index.js +1 -1
  137. package/templates/webcomponents/igc-ts/projects/side-nav/files/index.html +21 -0
  138. package/templates/webcomponents/igc-ts/projects/side-nav/files/src/app/app-routing.ts +9 -0
  139. package/templates/webcomponents/igc-ts/projects/side-nav/files/src/app/app.ts +192 -22
  140. package/templates/webcomponents/igc-ts/projects/side-nav/files/src/app/home/home.ts +175 -0
  141. package/templates/webcomponents/igc-ts/projects/side-nav/index.js +1 -1
  142. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/index.html +25 -0
  143. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/app-routing.ts +37 -0
  144. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/app.ts +251 -0
  145. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/login-bar/login-bar.ts +124 -0
  146. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/login-dialog/login-dialog.ts +253 -0
  147. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/models/external-login.ts +10 -0
  148. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/models/login.ts +4 -0
  149. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/models/register-info.ts +6 -0
  150. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/models/user.ts +19 -0
  151. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/services/authentication.ts +37 -0
  152. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/services/external-auth-config.ts +44 -0
  153. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/services/externalAuth.ts +272 -0
  154. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/services/fakeBackend.ts +88 -0
  155. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/services/jwtUtil.ts +10 -0
  156. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/services/pkce.ts +29 -0
  157. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/services/userStore.ts +39 -0
  158. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/profile/profile.ts +142 -0
  159. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/redirect/redirect-facebook.ts +57 -0
  160. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/redirect/redirect-google.ts +53 -0
  161. package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/redirect/redirect-microsoft.ts +53 -0
  162. package/templates/webcomponents/igc-ts/projects/side-nav-auth/index.d.ts +15 -0
  163. package/templates/webcomponents/igc-ts/projects/side-nav-auth/index.js +46 -0
  164. package/templates/webcomponents/igc-ts/projects/side-nav-mini/files/src/app/app-routing.ts +13 -0
  165. package/templates/webcomponents/igc-ts/projects/side-nav-mini/files/src/app/app.ts +238 -0
  166. package/templates/webcomponents/igc-ts/projects/side-nav-mini/index.d.ts +14 -0
  167. package/templates/webcomponents/igc-ts/projects/side-nav-mini/index.js +45 -0
  168. package/templates/webcomponents/igc-ts/projects/side-nav-mini-auth/files/src/app/app.ts +258 -0
  169. package/templates/webcomponents/igc-ts/projects/side-nav-mini-auth/index.d.ts +15 -0
  170. package/templates/webcomponents/igc-ts/projects/side-nav-mini-auth/index.js +50 -0
  171. package/templates/webcomponents/igc-ts/tree/default/index.js +1 -1
  172. package/templates/react/igr-ts/projects/top-nav/files/src/app/app.css +0 -62
  173. package/templates/react/igr-ts/projects/top-nav/files/src/app/app.tsx +0 -18
  174. package/templates/react/igr-ts/projects/top-nav/files/src/components/navigation-header/index.tsx +0 -19
  175. /package/templates/react/igr-ts/projects/{top-nav → side-nav}/files/src/app/app.test.tsx +0 -0
@@ -0,0 +1,40 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { ExternalAuth } from '../services/externalAuth';
4
+ import { useAuth } from '../AuthContext';
5
+
6
+ /** Handles the OAuth redirect from Google. Exchanges the authorization code for a user profile. */
7
+ export default function RedirectGoogle() {
8
+ const { loginWith } = useAuth();
9
+ const navigate = useNavigate();
10
+ const [error, setError] = useState('');
11
+
12
+ useEffect(() => {
13
+ (async () => {
14
+ try {
15
+ const externalUser = await ExternalAuth.handleRedirect('google');
16
+ const err = await loginWith(externalUser);
17
+ if (err) {
18
+ setError(err);
19
+ } else {
20
+ navigate('/auth/profile');
21
+ }
22
+ } catch (e: any) {
23
+ console.error('Google sign-in failed:', e);
24
+ setError('Google sign-in failed. Please try again.');
25
+ }
26
+ })();
27
+ // eslint-disable-next-line react-hooks/exhaustive-deps
28
+ }, []);
29
+
30
+ if (error) {
31
+ return (
32
+ <div style={{ padding: '2rem', color: '#d32f2f' }}>
33
+ <p>{error}</p>
34
+ <button onClick={() => navigate('/')}>Back to Home</button>
35
+ </div>
36
+ );
37
+ }
38
+
39
+ return <p style={{ padding: '2rem' }}>Signing in with Google…</p>;
40
+ }
@@ -0,0 +1,40 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { ExternalAuth } from '../services/externalAuth';
4
+ import { useAuth } from '../AuthContext';
5
+
6
+ /** Handles the OAuth redirect from Microsoft. Exchanges the authorization code for a user profile. */
7
+ export default function RedirectMicrosoft() {
8
+ const { loginWith } = useAuth();
9
+ const navigate = useNavigate();
10
+ const [error, setError] = useState('');
11
+
12
+ useEffect(() => {
13
+ (async () => {
14
+ try {
15
+ const externalUser = await ExternalAuth.handleRedirect('microsoft');
16
+ const err = await loginWith(externalUser);
17
+ if (err) {
18
+ setError(err);
19
+ } else {
20
+ navigate('/auth/profile');
21
+ }
22
+ } catch (e: any) {
23
+ console.error('Microsoft sign-in failed:', e);
24
+ setError('Microsoft sign-in failed. Please try again.');
25
+ }
26
+ })();
27
+ // eslint-disable-next-line react-hooks/exhaustive-deps
28
+ }, []);
29
+
30
+ if (error) {
31
+ return (
32
+ <div style={{ padding: '2rem', color: '#d32f2f' }}>
33
+ <p>{error}</p>
34
+ <button onClick={() => navigate('/')}>Back to Home</button>
35
+ </div>
36
+ );
37
+ }
38
+
39
+ return <p style={{ padding: '2rem' }}>Signing in with Microsoft…</p>;
40
+ }
@@ -0,0 +1,37 @@
1
+ import type { Login } from '../models/login';
2
+ import type { RegisterInfo } from '../models/register-info';
3
+ import type { ExternalLogin } from '../models/external-login';
4
+ import type { LoginResult } from '../models/user';
5
+ import { parseUser } from './jwtUtil';
6
+ import { fakeLogin, fakeRegister, fakeExtLogin } from './fakeBackend';
7
+
8
+ /** Authentication service — swap fakeLogin/fakeRegister for real API calls when ready. */
9
+ export const Authentication = {
10
+ async login(data: Login): Promise<LoginResult> {
11
+ try {
12
+ const token = await fakeLogin(data);
13
+ return { user: parseUser(token) };
14
+ } catch (e: any) {
15
+ return { error: e.message };
16
+ }
17
+ },
18
+
19
+ async register(data: RegisterInfo): Promise<LoginResult> {
20
+ try {
21
+ const token = await fakeRegister(data);
22
+ return { user: parseUser(token) };
23
+ } catch (e: any) {
24
+ return { error: e.message };
25
+ }
26
+ },
27
+
28
+ /** Send user info from a social provider to the external login endpoint. */
29
+ async loginWith(data: ExternalLogin): Promise<LoginResult> {
30
+ try {
31
+ const token = fakeExtLogin(data);
32
+ return { user: parseUser(token) };
33
+ } catch (e: any) {
34
+ return { error: e.message };
35
+ }
36
+ }
37
+ };
@@ -0,0 +1,44 @@
1
+ // Social login configuration.
2
+ // To enable a provider, set its entry in oauthConfig below with your real credentials
3
+ // from the provider's developer console.
4
+ //
5
+ // Redirect URIs to register in each provider's app settings:
6
+ // {your-origin}/auth/redirect-google
7
+ // {your-origin}/auth/redirect-facebook
8
+ // {your-origin}/auth/redirect-microsoft
9
+ //
10
+ // Developer consoles:
11
+ // Google: https://console.cloud.google.com/apis/credentials
12
+ // Microsoft: https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps
13
+ // Facebook: https://developers.facebook.com/apps
14
+
15
+ export type OAuthProvider = 'google' | 'facebook' | 'microsoft';
16
+
17
+ export interface OAuthConfig {
18
+ google?: { clientId: string };
19
+
20
+ // tenantId defaults to 'common' (multi-tenant). Set it for single-tenant apps.
21
+ // IMPORTANT: The redirect URI must be registered as a SPA redirect URI in Azure
22
+ // (not "Web"), otherwise the token exchange will fail with a CORS error.
23
+ // See: https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow
24
+ microsoft?: { clientId: string; tenantId?: string };
25
+
26
+ // Facebook login uses the JS SDK (popup flow). The SDK script must be loaded in
27
+ // index.html (see below). In the Facebook app dashboard you must also:
28
+ // - Enable "Login with the JavaScript SDK"
29
+ // - Add your domain to "Allowed Domains for the JavaScript SDK"
30
+ // - Add the redirect URI to "Valid OAuth Redirect URIs"
31
+ // - Serve the app over HTTPS
32
+ // See: https://developers.facebook.com/docs/facebook-login/web
33
+ facebook?: { clientId: string };
34
+ }
35
+
36
+ // Active OAuth configuration — fill in the providers you want to enable, for example:
37
+ //
38
+ // export const oauthConfig: OAuthConfig = {
39
+ // google: { clientId: 'YOUR_GOOGLE_CLIENT_ID' },
40
+ // microsoft: { clientId: 'YOUR_AZURE_APP_CLIENT_ID', tenantId: 'common' },
41
+ // // Note: Facebook requires HTTPS even for local dev - use ngrok or a local SSL proxy.
42
+ // facebook: { clientId: 'YOUR_FACEBOOK_APP_ID' },
43
+ // };
44
+ export const oauthConfig: OAuthConfig = {};
@@ -0,0 +1,272 @@
1
+ import type { ExternalLogin } from '../models/external-login';
2
+ import type { OAuthProvider } from './external-auth-config';
3
+ import { oauthConfig } from './external-auth-config';
4
+ import { generateCodeVerifier, generateCodeChallenge, buildAuthUrl } from './pkce';
5
+
6
+ // sessionStorage keys
7
+ const VERIFIER_KEY = '_pkce_verifier';
8
+ const STATE_KEY = '_oauth_state';
9
+ const FB_USER_KEY = '_fb_user';
10
+ const ACTIVE_PROVIDER_KEY = '_ext_active_provider';
11
+
12
+ // Declared by the Facebook JS SDK (loaded via script tag in index.html)
13
+ declare const FB: any;
14
+
15
+ // Set to true once FB.init() has been called in this session.
16
+ // Prevents FB.logout() from being called before initialization.
17
+ let fbInitialized = false;
18
+
19
+ /**
20
+ * Decode a JWT payload segment. Handles Base64URL encoding (no padding, - and _ chars)
21
+ * which `atob()` does not accept natively - missing padding causes `InvalidCharacterError`.
22
+ */
23
+ function decodeJwtPayload(token: string): any {
24
+ const base64url = token.split('.')[1];
25
+ const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
26
+ const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, '=');
27
+ return JSON.parse(atob(padded));
28
+ }
29
+
30
+ /**
31
+ * Waits until the Facebook JS SDK has loaded and is available on window.
32
+ * The SDK is loaded with `async defer` so it may not be ready when login() is called.
33
+ */
34
+ function waitForFB(): Promise<void> {
35
+ return new Promise(resolve => {
36
+ if (typeof (window as any).FB !== 'undefined') { resolve(); return; }
37
+ const id = setInterval(() => {
38
+ if (typeof (window as any).FB !== 'undefined') { clearInterval(id); resolve(); }
39
+ }, 50);
40
+ });
41
+ }
42
+
43
+ /**
44
+ * External (social) authentication service.
45
+ * Supports Google and Microsoft via OIDC/PKCE, and Facebook via the JS SDK.
46
+ *
47
+ * Usage: call login(provider) to start the flow; call handleRedirect(provider)
48
+ * on the matching redirect page to complete it and retrieve the user profile.
49
+ */
50
+ export const ExternalAuth = {
51
+ /** Returns true if any provider (or the specific provider) is configured. */
52
+ hasProvider(provider?: OAuthProvider): boolean {
53
+ if (provider) {
54
+ return provider in oauthConfig && (oauthConfig as any)[provider] != null;
55
+ }
56
+ return Object.values(oauthConfig).some(v => v != null);
57
+ },
58
+
59
+ /** Initiate login for the given provider. Redirects the page to the provider's auth endpoint. */
60
+ async login(provider: OAuthProvider): Promise<void> {
61
+ localStorage.setItem(ACTIVE_PROVIDER_KEY, provider);
62
+ if (provider === 'google') {
63
+ const cfg = oauthConfig.google!;
64
+ const verifier = generateCodeVerifier();
65
+ const challenge = await generateCodeChallenge(verifier);
66
+ sessionStorage.setItem(VERIFIER_KEY, verifier);
67
+ const state = crypto.randomUUID();
68
+ sessionStorage.setItem(STATE_KEY, state);
69
+ const redirectUri = `${window.location.origin}/auth/redirect-google`;
70
+ window.location.href = buildAuthUrl('https://accounts.google.com/o/oauth2/v2/auth', {
71
+ response_type: 'code',
72
+ client_id: cfg.clientId,
73
+ redirect_uri: redirectUri,
74
+ scope: 'openid profile email',
75
+ code_challenge: challenge,
76
+ code_challenge_method: 'S256',
77
+ state,
78
+ });
79
+ } else if (provider === 'microsoft') {
80
+ const cfg = oauthConfig.microsoft!;
81
+ const tenantId = cfg.tenantId ?? 'common';
82
+ const verifier = generateCodeVerifier();
83
+ const challenge = await generateCodeChallenge(verifier);
84
+ sessionStorage.setItem(VERIFIER_KEY, verifier);
85
+ const state = crypto.randomUUID();
86
+ sessionStorage.setItem(STATE_KEY, state);
87
+ const redirectUri = `${window.location.origin}/auth/redirect-microsoft`;
88
+ window.location.href = buildAuthUrl(
89
+ `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`,
90
+ {
91
+ response_type: 'code',
92
+ client_id: cfg.clientId,
93
+ redirect_uri: redirectUri,
94
+ scope: 'openid profile email',
95
+ code_challenge: challenge,
96
+ code_challenge_method: 'S256',
97
+ state,
98
+ }
99
+ );
100
+ } else if (provider === 'facebook') {
101
+ const cfg = oauthConfig.facebook!;
102
+ // Wait for the SDK to load (it is included with `async defer` in index.html
103
+ // and may not be available yet when the user clicks the login button).
104
+ await waitForFB();
105
+ FB.init({ appId: cfg.clientId, xfbml: false, version: 'v3.1' });
106
+ fbInitialized = true;
107
+ FB.login(
108
+ (response: any) => {
109
+ if (response.authResponse) {
110
+ FB.api(
111
+ '/me?fields=id,email,name,first_name,last_name,picture',
112
+ (res: any) => {
113
+ const user: ExternalLogin = {
114
+ id: res.id,
115
+ name: res.name,
116
+ given_name: res.first_name,
117
+ family_name: res.last_name,
118
+ email: res.email,
119
+ // Facebook returns picture as an object: { data: { url, width, height } }
120
+ picture: res.picture?.data?.url,
121
+ externalToken: FB.getAuthResponse()?.accessToken ?? '',
122
+ };
123
+ sessionStorage.setItem(FB_USER_KEY, JSON.stringify(user));
124
+ window.location.href = '/auth/redirect-facebook';
125
+ }
126
+ );
127
+ }
128
+ },
129
+ { scope: 'public_profile,email' }
130
+ );
131
+ }
132
+ },
133
+
134
+ /**
135
+ * Complete the OAuth redirect flow and return the external user profile.
136
+ * Call this from the /auth/redirect-{provider} page.
137
+ *
138
+ * For Google/Microsoft: exchanges the authorization code (PKCE) for tokens.
139
+ * For Facebook: reads the profile stored during the FB.login() popup flow.
140
+ */
141
+ async handleRedirect(provider: OAuthProvider): Promise<ExternalLogin> {
142
+ if (provider === 'facebook') {
143
+ const stored = sessionStorage.getItem(FB_USER_KEY);
144
+ if (!stored) throw new Error('No Facebook user data found. Please try again.');
145
+ sessionStorage.removeItem(FB_USER_KEY);
146
+ return JSON.parse(stored) as ExternalLogin;
147
+ }
148
+
149
+ const params = new URLSearchParams(window.location.search);
150
+ const code = params.get('code');
151
+ if (!code) throw new Error('Missing authorization code in redirect URL.');
152
+
153
+ // Validate the state parameter to prevent CSRF attacks.
154
+ const returnedState = params.get('state');
155
+ const savedState = sessionStorage.getItem(STATE_KEY);
156
+ sessionStorage.removeItem(STATE_KEY);
157
+ if (!returnedState || returnedState !== savedState) {
158
+ throw new Error('OAuth state mismatch. The request may have been tampered with.');
159
+ }
160
+
161
+ const verifier = sessionStorage.getItem(VERIFIER_KEY);
162
+ if (!verifier) throw new Error('Missing PKCE code verifier. Please try again.');
163
+ sessionStorage.removeItem(VERIFIER_KEY);
164
+
165
+ if (provider === 'google') {
166
+ const cfg = oauthConfig.google!;
167
+ const redirectUri = `${window.location.origin}/auth/redirect-google`;
168
+ const res = await fetch('https://oauth2.googleapis.com/token', {
169
+ method: 'POST',
170
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
171
+ body: new URLSearchParams({
172
+ grant_type: 'authorization_code',
173
+ client_id: cfg.clientId,
174
+ redirect_uri: redirectUri,
175
+ code,
176
+ code_verifier: verifier,
177
+ }),
178
+ });
179
+ if (!res.ok) throw new Error('Google token exchange failed.');
180
+ const data = await res.json();
181
+ // Decode the id_token to extract user claims - no extra userinfo request needed
182
+ const payload = decodeJwtPayload(data.id_token);
183
+ return {
184
+ id: payload.sub,
185
+ name: payload.name,
186
+ given_name: payload.given_name,
187
+ family_name: payload.family_name,
188
+ email: payload.email,
189
+ picture: payload.picture,
190
+ externalToken: data.access_token,
191
+ };
192
+ }
193
+
194
+ if (provider === 'microsoft') {
195
+ const cfg = oauthConfig.microsoft!;
196
+ const tenantId = cfg.tenantId ?? 'common';
197
+ const redirectUri = `${window.location.origin}/auth/redirect-microsoft`;
198
+ const res = await fetch(
199
+ `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
200
+ {
201
+ method: 'POST',
202
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
203
+ body: new URLSearchParams({
204
+ grant_type: 'authorization_code',
205
+ client_id: cfg.clientId,
206
+ redirect_uri: redirectUri,
207
+ code,
208
+ code_verifier: verifier,
209
+ }),
210
+ }
211
+ );
212
+ if (!res.ok) throw new Error('Microsoft token exchange failed.');
213
+ const data = await res.json();
214
+ const payload = decodeJwtPayload(data.id_token);
215
+ return {
216
+ id: payload.oid ?? payload.sub,
217
+ name: payload.name,
218
+ email: payload.email ?? payload.preferred_username,
219
+ externalToken: data.access_token,
220
+ };
221
+ }
222
+
223
+ throw new Error(`Unknown provider: ${provider}`);
224
+ },
225
+
226
+ /**
227
+ * Sign out from the active external provider (if any) and clear its stored state.
228
+ * Call this alongside clearing local user state on logout.
229
+ */
230
+ logout(): void {
231
+ const provider = localStorage.getItem(ACTIVE_PROVIDER_KEY) as OAuthProvider | null;
232
+ localStorage.removeItem(ACTIVE_PROVIDER_KEY);
233
+ sessionStorage.removeItem(VERIFIER_KEY);
234
+ sessionStorage.removeItem(FB_USER_KEY);
235
+
236
+ if (!provider) return;
237
+
238
+ if (provider === 'google') {
239
+ // Redirect to Google's end-session endpoint to clear the Google session.
240
+ // The user is returned to the app root after sign-out.
241
+ const cfg = oauthConfig.google;
242
+ if (cfg) {
243
+ window.location.href = `https://accounts.google.com/logout`;
244
+ return;
245
+ }
246
+ }
247
+
248
+ if (provider === 'microsoft') {
249
+ const cfg = oauthConfig.microsoft;
250
+ if (cfg) {
251
+ const tenantId = cfg.tenantId ?? 'common';
252
+ const postLogoutRedirectUri = encodeURIComponent(window.location.origin);
253
+ window.location.href =
254
+ `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/logout` +
255
+ `?post_logout_redirect_uri=${postLogoutRedirectUri}`;
256
+ return;
257
+ }
258
+ }
259
+
260
+ if (provider === 'facebook') {
261
+ // Only call FB.logout() when the SDK was initialised in this session.
262
+ // Calling it on a fresh page load (before FB.init) throws an error.
263
+ try {
264
+ if (fbInitialized && typeof FB !== 'undefined') {
265
+ FB.logout();
266
+ }
267
+ } catch {
268
+ // SDK not loaded - nothing to do
269
+ }
270
+ }
271
+ },
272
+ };
@@ -0,0 +1,88 @@
1
+ // ⚠ DEVELOPMENT ONLY — simulates POST /login, /register, /extlogin using localStorage.
2
+ // Before going to production: remove this interceptor and replace with calls to your real API.
3
+ import type { Login } from '../models/login';
4
+ import type { RegisterInfo } from '../models/register-info';
5
+ import type { ExternalLogin } from '../models/external-login';
6
+
7
+ const USERS_KEY = '_fake_users';
8
+
9
+ interface StoredUser {
10
+ given_name: string;
11
+ family_name: string;
12
+ email: string;
13
+ passwordHash: string;
14
+ externalId?: string;
15
+ }
16
+
17
+ function getUsers(): StoredUser[] {
18
+ try {
19
+ return JSON.parse(localStorage.getItem(USERS_KEY) ?? '[]');
20
+ } catch {
21
+ return [];
22
+ }
23
+ }
24
+
25
+ function saveUsers(users: StoredUser[]): void {
26
+ localStorage.setItem(USERS_KEY, JSON.stringify(users));
27
+ }
28
+
29
+ async function hashPassword(password: string): Promise<string> {
30
+ const data = new TextEncoder().encode(password);
31
+ const digest = await crypto.subtle.digest('SHA-256', data);
32
+ return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('');
33
+ }
34
+
35
+ function makeJwt(payload: object): string {
36
+ const header = btoa(JSON.stringify({ alg: 'none', typ: 'JWT' }));
37
+ const body = btoa(JSON.stringify({ exp: Date.now() / 1000 + 3600, ...payload }));
38
+ return `${header}.${body}.`;
39
+ }
40
+
41
+ export async function fakeLogin(data: Login): Promise<string> {
42
+ const users = getUsers();
43
+ const passwordHash = await hashPassword(data.password);
44
+ const user = users.find(u => u.email === data.email && u.passwordHash === passwordHash);
45
+ if (!user) {
46
+ throw new Error('Invalid email or password.');
47
+ }
48
+ return makeJwt({ name: `${user.given_name} ${user.family_name}`, given_name: user.given_name, family_name: user.family_name, email: user.email });
49
+ }
50
+
51
+ export async function fakeRegister(data: RegisterInfo): Promise<string> {
52
+ const users = getUsers();
53
+ if (users.find(u => u.email === data.email)) {
54
+ throw new Error('An account with this email already exists.');
55
+ }
56
+ const newUser: StoredUser = {
57
+ given_name: data.given_name,
58
+ family_name: data.family_name,
59
+ email: data.email,
60
+ passwordHash: await hashPassword(data.password)
61
+ };
62
+ saveUsers([...users, newUser]);
63
+ return makeJwt({ name: `${data.given_name} ${data.family_name}`, given_name: data.given_name, family_name: data.family_name, email: data.email });
64
+ }
65
+ /** Upsert a user from a social (external) auth provider and return a JWT. */
66
+ export function fakeExtLogin(data: ExternalLogin): string {
67
+ const users = getUsers();
68
+ const existing = users.find(u => u.email === data.email && data.email != null)
69
+ ?? users.find(u => u.externalId === data.id);
70
+ const given_name = data.given_name ?? data.name?.split(' ')[0] ?? '';
71
+ const family_name = data.family_name ?? data.name?.split(' ').slice(1).join(' ') ?? '';
72
+ // Resolve email: prefer what the provider returned, fall back to what we stored previously.
73
+ const email = data.email ?? existing?.email;
74
+ if (existing) {
75
+ // Update profile fields from provider (name/picture may change).
76
+ // Also store externalId if this user was originally created by email (first social login).
77
+ existing.given_name = given_name;
78
+ existing.family_name = family_name;
79
+ if (!existing.externalId) existing.externalId = data.id;
80
+ saveUsers(users);
81
+ } else {
82
+ if (!email) {
83
+ throw new Error('Cannot create an account without an email address.');
84
+ }
85
+ saveUsers([...users, { given_name, family_name, email, passwordHash: '', externalId: data.id }]);
86
+ }
87
+ return makeJwt({ name: data.name, given_name, family_name, email, picture: data.picture });
88
+ }
@@ -0,0 +1,10 @@
1
+ import type { UserJWT } from '../models/user';
2
+
3
+ /** Parse the payload of a JWT string into a UserJWT object. */
4
+ export function parseUser(token: string): UserJWT & { token: string } {
5
+ const base64url = token.split('.')[1];
6
+ const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
7
+ const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, '=');
8
+ const decoded = JSON.parse(atob(padded));
9
+ return { ...decoded, token };
10
+ }
@@ -0,0 +1,29 @@
1
+ // PKCE (Proof Key for Code Exchange) utilities for OAuth 2.0 Authorization Code Flow.
2
+ // https://tools.ietf.org/html/rfc7636
3
+
4
+ function base64UrlEncode(bytes: Uint8Array): string {
5
+ return btoa(String.fromCharCode(...bytes))
6
+ .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
7
+ }
8
+
9
+ /** Generate a cryptographically random PKCE code verifier (43–128 chars, URL-safe). */
10
+ export function generateCodeVerifier(): string {
11
+ const bytes = crypto.getRandomValues(new Uint8Array(32));
12
+ return base64UrlEncode(bytes);
13
+ }
14
+
15
+ /** Compute the S256 code challenge from a code verifier. */
16
+ export async function generateCodeChallenge(verifier: string): Promise<string> {
17
+ const data = new TextEncoder().encode(verifier);
18
+ const digest = await crypto.subtle.digest('SHA-256', data);
19
+ return base64UrlEncode(new Uint8Array(digest));
20
+ }
21
+
22
+ /** Build a URL with query parameters from a plain object. */
23
+ export function buildAuthUrl(endpoint: string, params: Record<string, string>): string {
24
+ const url = new URL(endpoint);
25
+ for (const [k, v] of Object.entries(params)) {
26
+ url.searchParams.set(k, v);
27
+ }
28
+ return url.toString();
29
+ }
@@ -0,0 +1,39 @@
1
+ import type { User } from '../models/user';
2
+
3
+ const USER_KEY = 'currentUser';
4
+
5
+ /**
6
+ * Simple localStorage-backed user store.
7
+ *
8
+ * NOTE: Storing the full user object in localStorage can be susceptible to XSS attacks.
9
+ * Consider additional security measures before going to production.
10
+ */
11
+ export const UserStore = {
12
+ getUser(): User | null {
13
+ try {
14
+ const raw = localStorage.getItem(USER_KEY);
15
+ if (!raw) return null;
16
+ const parsed: User = JSON.parse(raw);
17
+ // Discard expired tokens so a stale session is never silently restored.
18
+ if (parsed.exp && Date.now() / 1000 > parsed.exp) {
19
+ localStorage.removeItem(USER_KEY);
20
+ return null;
21
+ }
22
+ return parsed;
23
+ } catch {
24
+ return null;
25
+ }
26
+ },
27
+
28
+ setUser(user: User): void {
29
+ localStorage.setItem(USER_KEY, JSON.stringify(user));
30
+ },
31
+
32
+ clearUser(): void {
33
+ localStorage.removeItem(USER_KEY);
34
+ },
35
+
36
+ getInitials(user: User): string {
37
+ return (user.given_name.charAt(0) + (user.family_name?.charAt(0) ?? '')).toUpperCase();
38
+ }
39
+ };
@@ -0,0 +1,15 @@
1
+ import { ProjectTemplate } from "@igniteui/cli-core";
2
+ import { SideNavIgrTsProject } from "../side-nav";
3
+ export declare class SideNavAuthIgrTsProject extends SideNavIgrTsProject implements ProjectTemplate {
4
+ id: string;
5
+ name: string;
6
+ description: string;
7
+ dependencies: string[];
8
+ framework: string;
9
+ projectType: string;
10
+ hasExtraConfiguration: boolean;
11
+ isHidden: boolean;
12
+ get templatePaths(): string[];
13
+ }
14
+ declare const _default: SideNavAuthIgrTsProject;
15
+ export default _default;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.SideNavAuthIgrTsProject = void 0;
27
+ const path = __importStar(require("path"));
28
+ const side_nav_1 = require("../side-nav");
29
+ class SideNavAuthIgrTsProject extends side_nav_1.SideNavIgrTsProject {
30
+ constructor() {
31
+ super(...arguments);
32
+ this.id = "side-nav-auth";
33
+ this.name = "Side navigation + login";
34
+ this.description = "Side navigation extended with user authentication module";
35
+ this.dependencies = [];
36
+ this.framework = "react";
37
+ this.projectType = "igr-ts";
38
+ this.hasExtraConfiguration = false;
39
+ this.isHidden = true;
40
+ }
41
+ get templatePaths() {
42
+ return [...super.templatePaths, path.join(__dirname, "files")];
43
+ }
44
+ }
45
+ exports.SideNavAuthIgrTsProject = SideNavAuthIgrTsProject;
46
+ exports.default = new SideNavAuthIgrTsProject();
@@ -0,0 +1,5 @@
1
+ import Home from './home/home';
2
+
3
+ export const routes = [
4
+ { path: '/', element: <Home />, text: 'Home', icon: 'home' }
5
+ ];