diff --git a/packages/cli/templates/react/igr-ts/projects/side-nav-auth/files/src/app/app-routes.tsx b/packages/cli/templates/react/igr-ts/projects/side-nav-auth/files/src/app/app-routes.tsx
new file mode 100644
index 000000000..1361b746f
--- /dev/null
+++ b/packages/cli/templates/react/igr-ts/projects/side-nav-auth/files/src/app/app-routes.tsx
@@ -0,0 +1,24 @@
+import Home from './home/home';
+import Profile from './authentication/pages/Profile';
+import RedirectGoogle from './authentication/pages/RedirectGoogle';
+import RedirectMicrosoft from './authentication/pages/RedirectMicrosoft';
+import RedirectFacebook from './authentication/pages/RedirectFacebook';
+import { AuthGuard } from './authentication/AuthGuard';
+
+export const routes = [
+ { path: '/', element:
Signed in
+Your account details are available on this protected route.
+{error}
+ +Signing in with Facebook…
; +} diff --git a/packages/cli/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/pages/RedirectGoogle.tsx b/packages/cli/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/pages/RedirectGoogle.tsx new file mode 100644 index 000000000..c746ec51b --- /dev/null +++ b/packages/cli/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/pages/RedirectGoogle.tsx @@ -0,0 +1,40 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { ExternalAuth } from '../services/externalAuth'; +import { useAuth } from '../AuthContext'; + +/** Handles the OAuth redirect from Google. Exchanges the authorization code for a user profile. */ +export default function RedirectGoogle() { + const { loginWith } = useAuth(); + const navigate = useNavigate(); + const [error, setError] = useState(''); + + useEffect(() => { + (async () => { + try { + const externalUser = await ExternalAuth.handleRedirect('google'); + const err = await loginWith(externalUser); + if (err) { + setError(err); + } else { + navigate('/auth/profile'); + } + } catch (e: any) { + console.error('Google sign-in failed:', e); + setError('Google sign-in failed. Please try again.'); + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (error) { + return ( +{error}
+ +Signing in with Google…
; +} diff --git a/packages/cli/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/pages/RedirectMicrosoft.tsx b/packages/cli/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/pages/RedirectMicrosoft.tsx new file mode 100644 index 000000000..96a8fd500 --- /dev/null +++ b/packages/cli/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/pages/RedirectMicrosoft.tsx @@ -0,0 +1,40 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { ExternalAuth } from '../services/externalAuth'; +import { useAuth } from '../AuthContext'; + +/** Handles the OAuth redirect from Microsoft. Exchanges the authorization code for a user profile. */ +export default function RedirectMicrosoft() { + const { loginWith } = useAuth(); + const navigate = useNavigate(); + const [error, setError] = useState(''); + + useEffect(() => { + (async () => { + try { + const externalUser = await ExternalAuth.handleRedirect('microsoft'); + const err = await loginWith(externalUser); + if (err) { + setError(err); + } else { + navigate('/auth/profile'); + } + } catch (e: any) { + console.error('Microsoft sign-in failed:', e); + setError('Microsoft sign-in failed. Please try again.'); + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (error) { + return ( +{error}
+ +Signing in with Microsoft…
; +} diff --git a/packages/cli/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/authentication.ts b/packages/cli/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/authentication.ts new file mode 100644 index 000000000..1ddefb5fd --- /dev/null +++ b/packages/cli/templates/react/igr-ts/projects/side-nav-auth/files/src/app/authentication/services/authentication.ts @@ -0,0 +1,37 @@ +import type { Login } from '../models/login'; +import type { RegisterInfo } from '../models/register-info'; +import type { ExternalLogin } from '../models/external-login'; +import type { LoginResult } from '../models/user'; +import { parseUser } from './jwtUtil'; +import { fakeLogin, fakeRegister, fakeExtLogin } from './fakeBackend'; + +/** Authentication service — swap fakeLogin/fakeRegister for real API calls when ready. */ +export const Authentication = { + async login(data: Login): PromiseSigned in
+Your account details are available on this protected route.
+${this.error}
+ +Signing in with Facebook…
`; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'app-redirect-facebook': RedirectFacebookElement; + } +} diff --git a/packages/cli/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/redirect/redirect-google.ts b/packages/cli/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/redirect/redirect-google.ts new file mode 100644 index 000000000..f88b4c9a7 --- /dev/null +++ b/packages/cli/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/redirect/redirect-google.ts @@ -0,0 +1,53 @@ +import { LitElement, html } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { Router } from '@vaadin/router'; +import { ExternalAuth } from '../authentication/services/externalAuth.js'; +import { Authentication } from '../authentication/services/authentication.js'; +import { UserStore } from '../authentication/services/userStore.js'; +import type { User } from '../authentication/models/user.js'; + +/** Handles the OAuth redirect from Google. Exchanges the authorization code for a user profile. */ +@customElement('app-redirect-google') +export class RedirectGoogleElement extends LitElement { + @state() private error = ''; + + connectedCallback() { + super.connectedCallback(); + this._handleRedirect(); + } + + private async _handleRedirect() { + try { + const externalUser = await ExternalAuth.handleRedirect('google'); + const result = await Authentication.loginWith(externalUser); + if (result.user) { + UserStore.setUser(result.user as User); + this.dispatchEvent(new CustomEvent('auth-change', { bubbles: true, composed: true })); + Router.go('/auth/profile'); + } else { + this.error = result.error ?? 'Google sign-in failed.'; + } + } catch (e: any) { + console.error('Google sign-in failed:', e); + this.error = 'Google sign-in failed. Please try again.'; + } + } + + render() { + if (this.error) { + return html` +${this.error}
+ +Signing in with Google…
`; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'app-redirect-google': RedirectGoogleElement; + } +} diff --git a/packages/cli/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/redirect/redirect-microsoft.ts b/packages/cli/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/redirect/redirect-microsoft.ts new file mode 100644 index 000000000..896b52b47 --- /dev/null +++ b/packages/cli/templates/webcomponents/igc-ts/projects/side-nav-auth/files/src/app/redirect/redirect-microsoft.ts @@ -0,0 +1,53 @@ +import { LitElement, html } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { Router } from '@vaadin/router'; +import { ExternalAuth } from '../authentication/services/externalAuth.js'; +import { Authentication } from '../authentication/services/authentication.js'; +import { UserStore } from '../authentication/services/userStore.js'; +import type { User } from '../authentication/models/user.js'; + +/** Handles the OAuth redirect from Microsoft. Exchanges the authorization code for a user profile. */ +@customElement('app-redirect-microsoft') +export class RedirectMicrosoftElement extends LitElement { + @state() private error = ''; + + connectedCallback() { + super.connectedCallback(); + this._handleRedirect(); + } + + private async _handleRedirect() { + try { + const externalUser = await ExternalAuth.handleRedirect('microsoft'); + const result = await Authentication.loginWith(externalUser); + if (result.user) { + UserStore.setUser(result.user as User); + this.dispatchEvent(new CustomEvent('auth-change', { bubbles: true, composed: true })); + Router.go('/auth/profile'); + } else { + this.error = result.error ?? 'Microsoft sign-in failed.'; + } + } catch (e: any) { + console.error('Microsoft sign-in failed:', e); + this.error = 'Microsoft sign-in failed. Please try again.'; + } + } + + render() { + if (this.error) { + return html` +${this.error}
+ +Signing in with Microsoft…
`; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'app-redirect-microsoft': RedirectMicrosoftElement; + } +} diff --git a/packages/cli/templates/webcomponents/igc-ts/projects/side-nav-auth/index.ts b/packages/cli/templates/webcomponents/igc-ts/projects/side-nav-auth/index.ts new file mode 100644 index 000000000..2a779c4d8 --- /dev/null +++ b/packages/cli/templates/webcomponents/igc-ts/projects/side-nav-auth/index.ts @@ -0,0 +1,19 @@ +import { ProjectTemplate } from "@igniteui/cli-core"; +import * as path from "path"; +import { SideNavProject } from "../side-nav"; + +export class SideNavAuthIgcProject extends SideNavProject implements ProjectTemplate { + public id: string = "side-nav-auth"; + public name = "Side navigation + login"; + public description = "Side navigation extended with user authentication module"; + public dependencies: string[] = []; + public framework: string = "webcomponents"; + public projectType: string = "igc-ts"; + public hasExtraConfiguration: boolean = false; + public isHidden: boolean = true; + + public get templatePaths(): string[] { + return [...super.templatePaths, path.join(__dirname, "files")]; + } +} +export default new SideNavAuthIgcProject(); diff --git a/packages/cli/templates/webcomponents/igc-ts/projects/side-nav-mini-auth/files/src/app/app.ts b/packages/cli/templates/webcomponents/igc-ts/projects/side-nav-mini-auth/files/src/app/app.ts new file mode 100644 index 000000000..169126406 --- /dev/null +++ b/packages/cli/templates/webcomponents/igc-ts/projects/side-nav-mini-auth/files/src/app/app.ts @@ -0,0 +1,255 @@ +import { html, css, LitElement } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { + defineComponents, + IgcIconComponent, + IgcNavDrawerComponent, + IgcNavDrawerItemComponent, + registerIcon, +} from 'igniteui-webcomponents'; +import { Router } from '@vaadin/router'; +import { routes, type AppRoute } from './app-routing.js'; +import { UserStore } from './authentication/services/userStore.js'; +import './authentication/login-bar/login-bar.js'; + +defineComponents( + IgcIconComponent, + IgcNavDrawerComponent, + IgcNavDrawerItemComponent, +); + +const materialIcons = [ + ['home', 'action/svg/production/ic_home_24px.svg'], + ['menu', 'navigation/svg/production/ic_menu_24px.svg'], + ['apps', 'navigation/svg/production/ic_apps_24px.svg'], + ['code', 'action/svg/production/ic_code_24px.svg'], + ['build', 'action/svg/production/ic_build_24px.svg'], + ['palette', 'image/svg/production/ic_palette_24px.svg'], + ['account_circle', 'action/svg/production/ic_account_circle_24px.svg'], + ['lock', 'action/svg/production/ic_lock_24px.svg'], + ['assignment_ind', 'action/svg/production/ic_assignment_ind_24px.svg'], +] as const; + +materialIcons.forEach(([name, path]) => + registerIcon(name, `https://unpkg.com/material-design-icons@3.0.1/${path}`, 'material') +); + +@customElement('app-root') +export default class App extends LitElement { + @state() + private drawerOpen = true; + + @state() + private currentPath = window.location.pathname; + + @state() + private isLoggedIn = Boolean(UserStore.getUser()); + + private mediaQuery?: MediaQueryList; + + static styles = css` + :host { + display: flex; + height: 100%; + } + + .app { + display: flex; + flex-flow: column nowrap; + width: 100%; + height: 100%; + overflow: hidden; + } + + .app__navbar { + display: flex; + align-items: center; + flex: 0 0 auto; + height: 56px; + padding: 0 16px; + background: #239ef0; + box-shadow: 0 2px 4px rgba(0, 0, 0, .24); + box-sizing: border-box; + position: relative; + z-index: 10; + } + + .app__menu-button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + padding: 0; + color: #000; + border: 0; + background: transparent; + cursor: pointer; + } + + .app__menu-button igc-icon { + font-size: 24px; + } + + .app__title { + margin: 0 0 0 16px; + color: #000; + font-size: 1.25rem; + font-weight: 600; + line-height: 1; + } + + .app__navbar-spacer { + flex: 1 1 auto; + } + + .app__body { + display: flex; + flex: 1 1 auto; + min-height: 0; + } + + .app__drawer { + flex: 0 0 auto; + height: 100%; + --menu-full-width: 280px; + } + + .app--mini .app__drawer { + --menu-full-width: 68px; + } + + igc-nav-drawer.app__drawer::part(base) { + transition: width 0.3s ease-out; + overflow: hidden; + } + + .app--mini igc-nav-drawer-item::part(base) { + justify-content: center; + width: 40px; + min-height: 40px; + padding: 0; + margin: 4px auto; + border-radius: 8px; + } + + .app--mini igc-nav-drawer-item::part(content) { + display: none; + } + + igc-nav-drawer-item[active]::part(base) { + background: #e0f2ff; + color: #0075d2; + } + + igc-nav-drawer-item[active] igc-icon { + color: #0075d2; + } + + router-outlet { + flex: 1 1 auto; + display: flex; + align-items: stretch; + justify-content: center; + min-width: 0; + overflow: auto; + } + + @media (max-width: 1024px) { + .app__menu-button { + display: none; + } + } + `; + + render() { + const visibleRoutes = (routes as AppRoute[]).filter((route) => { + if (!route.name) return false; + if ((route as any).requiresAuth && !this.isLoggedIn) return false; + return true; + }); + + return html` +