igniteui-cli 15.2.2 → 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.
- package/lib/commands/build.js +7 -12
- package/package.json +4 -4
- package/templates/blazor/igb/projects/ai-config/files/skills/AGENTS.md +0 -5
- package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-components/SKILL.md +2 -0
- package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-components/references/charts.md +7 -35
- package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-components/references/data-display.md +0 -54
- package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-components/references/feedback.md +0 -38
- package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-components/references/form-controls.md +0 -68
- package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-components/references/layout-manager.md +1 -124
- package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-components/references/layout.md +0 -62
- package/templates/blazor/igb/projects/ai-config/files/skills/igniteui-blazor-theming/SKILL.md +1 -1
- package/templates/react/igr-ts/projects/_base/files/package.json +1 -0
- package/templates/react/igr-ts/projects/_base/files/src/app/app.tsx +4 -2
- package/templates/react/igr-ts/projects/_base/files/src/setupTests.ts +12 -0
- package/templates/react/igr-ts/projects/_base/files/styles.css +6 -0
- package/templates/react/igr-ts/projects/_base_with_home/files/index.html +2 -1
- package/templates/react/igr-ts/projects/_base_with_home/files/src/app/home/home.tsx +60 -10
- package/templates/react/igr-ts/projects/_base_with_home/files/src/app/home/style.module.css +79 -20
- package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-components/SKILL.md +0 -8
- package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-components/reference/CHARTS-GRIDS.md +6 -36
- package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-components/reference/COMPONENT-CATALOGUE.md +8 -142
- package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-components/reference/EVENT-HANDLING.md +2 -0
- package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-customize-theme/SKILL.md +7 -14
- package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-customize-theme/reference/CSS-THEMING.md +2 -0
- package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-customize-theme/reference/MCP-SERVER.md +0 -8
- package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-generate-from-image-design/SKILL.md +2 -2
- package/templates/react/igr-ts/projects/ai-config/files/skills/igniteui-react-generate-from-image-design/reference/component-mapping.md +60 -74
- package/templates/react/igr-ts/projects/empty/index.js +2 -2
- package/templates/react/igr-ts/projects/side-nav/files/src/app/app-routes.tsx +5 -0
- package/templates/react/igr-ts/projects/side-nav/files/src/app/app.css +82 -0
- package/templates/react/igr-ts/projects/side-nav/files/src/app/app.tsx +104 -0
- package/templates/react/igr-ts/projects/side-nav/files/src/app/home/home.tsx +69 -0
- package/templates/react/igr-ts/projects/side-nav/files/src/app/home/style.module.css +105 -0
- package/templates/react/igr-ts/projects/{top-nav → side-nav}/index.d.ts +2 -2
- package/templates/react/igr-ts/projects/{top-nav → side-nav}/index.js +7 -7
- package/templates/react/igr-ts/projects/side-nav-auth/files/index.html +19 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/app-routes.tsx +24 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/app.css +84 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/app.tsx +124 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/AuthContext.tsx +73 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/AuthGuard.tsx +14 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/components/Login.module.css +93 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/components/Login.tsx +69 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/components/LoginBar.module.css +42 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/components/LoginBar.tsx +44 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/components/LoginDialog.module.css +14 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/components/LoginDialog.tsx +49 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/components/Register.module.css +74 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/components/Register.tsx +67 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/models/external-login.ts +10 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/models/login.ts +4 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/models/register-info.ts +6 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/models/user.ts +19 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/pages/Profile.module.css +87 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/pages/Profile.tsx +42 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/pages/RedirectFacebook.tsx +44 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/pages/RedirectGoogle.tsx +40 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/pages/RedirectMicrosoft.tsx +40 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/authentication.ts +37 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/external-auth-config.ts +44 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/externalAuth.ts +272 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/fakeBackend.ts +88 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/jwtUtil.ts +10 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/pkce.ts +29 -0
- package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/userStore.ts +39 -0
- package/templates/react/igr-ts/projects/side-nav-auth/index.d.ts +15 -0
- package/templates/react/igr-ts/projects/side-nav-auth/index.js +46 -0
- package/templates/react/igr-ts/projects/side-nav-mini/files/src/app/app-routes.tsx +5 -0
- package/templates/react/igr-ts/projects/side-nav-mini/files/src/app/app.css +109 -0
- package/templates/react/igr-ts/projects/side-nav-mini/files/src/app/app.test.tsx +20 -0
- package/templates/react/igr-ts/projects/side-nav-mini/files/src/app/app.tsx +81 -0
- package/templates/react/igr-ts/projects/side-nav-mini/files/src/app/home/home.tsx +69 -0
- package/templates/react/igr-ts/projects/side-nav-mini/files/src/app/home/style.module.css +105 -0
- package/templates/react/igr-ts/projects/side-nav-mini/index.d.ts +15 -0
- package/templates/react/igr-ts/projects/side-nav-mini/index.js +46 -0
- package/templates/react/igr-ts/projects/side-nav-mini-auth/files/src/app/app.css +106 -0
- package/templates/react/igr-ts/projects/side-nav-mini-auth/files/src/app/app.tsx +101 -0
- package/templates/react/igr-ts/projects/side-nav-mini-auth/index.d.ts +15 -0
- package/templates/react/igr-ts/projects/side-nav-mini-auth/index.js +50 -0
- package/templates/webcomponents/igc-ts/projects/_base/files/src/app/app.ts +6 -1
- package/templates/webcomponents/igc-ts/projects/_base/files/styles.css +1 -0
- package/templates/webcomponents/igc-ts/projects/_base_with_home/files/index.html +2 -0
- package/templates/webcomponents/igc-ts/projects/_base_with_home/files/src/app/home/home.ts +103 -9
- package/templates/webcomponents/igc-ts/projects/_base_with_home/files/src/assets/wc.png +0 -0
- package/templates/webcomponents/igc-ts/projects/ai-config/files/skills/igniteui-wc-choose-components/SKILL.md +122 -160
- package/templates/webcomponents/igc-ts/projects/ai-config/files/skills/igniteui-wc-customize-component-theme/SKILL.md +83 -311
- package/templates/webcomponents/igc-ts/projects/ai-config/files/skills/igniteui-wc-customize-component-theme/references/mcp-setup.md +69 -0
- package/templates/webcomponents/igc-ts/projects/ai-config/files/skills/igniteui-wc-generate-from-image-design/SKILL.md +4 -1
- package/templates/webcomponents/igc-ts/projects/ai-config/files/skills/igniteui-wc-generate-from-image-design/references/component-mapping.md +60 -61
- package/templates/webcomponents/igc-ts/projects/ai-config/files/skills/igniteui-wc-generate-from-image-design/references/gotchas.md +15 -11
- package/templates/webcomponents/igc-ts/projects/ai-config/files/skills/igniteui-wc-optimize-bundle-size/SKILL.md +23 -274
- package/templates/webcomponents/igc-ts/projects/empty/index.js +1 -1
- package/templates/webcomponents/igc-ts/projects/side-nav/files/index.html +21 -0
- package/templates/webcomponents/igc-ts/projects/side-nav/files/src/app/app-routing.ts +9 -0
- package/templates/webcomponents/igc-ts/projects/side-nav/files/src/app/app.ts +192 -22
- package/templates/webcomponents/igc-ts/projects/side-nav/files/src/app/home/home.ts +175 -0
- package/templates/webcomponents/igc-ts/projects/side-nav/index.js +1 -1
- package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/index.html +25 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/app-routing.ts +37 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/app.ts +251 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/login-bar/login-bar.ts +124 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/login-dialog/login-dialog.ts +253 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/models/external-login.ts +10 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/models/login.ts +4 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/models/register-info.ts +6 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/models/user.ts +19 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/services/authentication.ts +37 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/services/external-auth-config.ts +44 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/services/externalAuth.ts +272 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/services/fakeBackend.ts +88 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/services/jwtUtil.ts +10 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/services/pkce.ts +29 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/authentication/services/userStore.ts +39 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/profile/profile.ts +142 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/redirect/redirect-facebook.ts +57 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/redirect/redirect-google.ts +53 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/redirect/redirect-microsoft.ts +53 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-auth/index.d.ts +15 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-auth/index.js +46 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-mini/files/src/app/app-routing.ts +13 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-mini/files/src/app/app.ts +238 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-mini/index.d.ts +14 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-mini/index.js +45 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-mini-auth/files/src/app/app.ts +258 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-mini-auth/index.d.ts +15 -0
- package/templates/webcomponents/igc-ts/projects/side-nav-mini-auth/index.js +50 -0
- package/templates/react/igr-ts/projects/top-nav/files/src/app/app.css +0 -62
- package/templates/react/igr-ts/projects/top-nav/files/src/app/app.tsx +0 -18
- package/templates/react/igr-ts/projects/top-nav/files/src/components/navigation-header/index.tsx +0 -19
- /package/templates/react/igr-ts/projects/{top-nav → side-nav}/files/src/app/app.test.tsx +0 -0
|
@@ -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
|
+
}
|
package/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/pkce.ts
ADDED
|
@@ -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,109 @@
|
|
|
1
|
+
.app {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-flow: column nowrap;
|
|
4
|
+
height: 100%;
|
|
5
|
+
overflow: hidden;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.app__navbar {
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
flex: 0 0 auto;
|
|
12
|
+
height: 56px;
|
|
13
|
+
padding: 0 16px;
|
|
14
|
+
background: #239ef0;
|
|
15
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, .24);
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.app__menu-button {
|
|
20
|
+
display: inline-flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
justify-content: center;
|
|
23
|
+
width: 40px;
|
|
24
|
+
height: 40px;
|
|
25
|
+
padding: 0;
|
|
26
|
+
color: #000;
|
|
27
|
+
border: 0;
|
|
28
|
+
background: transparent;
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.app__menu-button igc-icon {
|
|
33
|
+
font-size: 24px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.app__title {
|
|
37
|
+
margin: 0 0 0 16px;
|
|
38
|
+
color: #000;
|
|
39
|
+
font-size: 1.25rem;
|
|
40
|
+
font-weight: 600;
|
|
41
|
+
line-height: 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.app__body {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex: 1 1 auto;
|
|
47
|
+
min-height: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.app__drawer {
|
|
51
|
+
flex: 0 0 auto;
|
|
52
|
+
height: 100%;
|
|
53
|
+
--menu-full-width: 280px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
igc-nav-drawer-item::part(base) {
|
|
57
|
+
min-height: 48px;
|
|
58
|
+
color: #2d2d2d;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
igc-nav-drawer-item[active]::part(base) {
|
|
62
|
+
background: #e0f2ff;
|
|
63
|
+
color: #0075d2;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
igc-nav-drawer-item[active] igc-icon {
|
|
67
|
+
color: #0075d2;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
igc-nav-drawer-item:not([active]) igc-icon {
|
|
71
|
+
color: #2d2d2d;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.app--mini .app__drawer {
|
|
75
|
+
--menu-full-width: 68px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
igc-nav-drawer.app__drawer::part(base) {
|
|
79
|
+
transition: width 0.3s ease-out;
|
|
80
|
+
overflow: hidden;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.app--mini igc-nav-drawer-item::part(base) {
|
|
84
|
+
justify-content: center;
|
|
85
|
+
width: 40px;
|
|
86
|
+
min-height: 40px;
|
|
87
|
+
padding: 0;
|
|
88
|
+
margin: 4px auto;
|
|
89
|
+
border-radius: 8px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.app--mini igc-nav-drawer-item::part(content) {
|
|
93
|
+
display: none;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.app__content {
|
|
97
|
+
flex: 1 1 auto;
|
|
98
|
+
min-width: 0;
|
|
99
|
+
overflow: auto;
|
|
100
|
+
display: flex;
|
|
101
|
+
justify-content: center;
|
|
102
|
+
align-items: flex-start;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@media (max-width: 1024px) {
|
|
106
|
+
.app__menu-button {
|
|
107
|
+
display: none;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { beforeAll, expect, test } from 'vitest';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
4
|
+
import App from './app';
|
|
5
|
+
import 'element-internals-polyfill';
|
|
6
|
+
import { setupTestMocks } from '../setupTests';
|
|
7
|
+
|
|
8
|
+
beforeAll(() => {
|
|
9
|
+
setupTestMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('renders without crashing', () => {
|
|
13
|
+
const wrapper = render(
|
|
14
|
+
<MemoryRouter>
|
|
15
|
+
<App />
|
|
16
|
+
</MemoryRouter>
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
expect(wrapper).toBeTruthy();
|
|
20
|
+
});
|