Skip to content
Draft
7 changes: 6 additions & 1 deletion examples/angular-kitchensink/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@
"input": "public"
}
],
"styles": ["src/styles.css"]
"styles": ["src/styles.css"],
"server": "src/main.server.ts",
"outputMode": "server",
"ssr": {
"entry": "src/server.ts"
}
},
"configurations": {
"production": {
Expand Down
14 changes: 10 additions & 4 deletions examples/angular-kitchensink/package.json

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

temporary, only for testing locally

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
"test": "ng test",
"serve:ssr:angular": "node dist/angular/server/server.mjs"
},
"prettier": {
"printWidth": 100,
Expand All @@ -22,22 +23,27 @@
},
"private": true,
"dependencies": {
"oidc-spa": "latest",
"zod": "^4.1.11",
"oidc-spa": "file:../../dist",
Comment thread
luca-peruzzo marked this conversation as resolved.
"@angular/common": "^20.3.0",
"@angular/compiler": "^20.3.0",
"@angular/core": "^20.3.0",
"@angular/forms": "^20.3.0",
"@angular/platform-browser": "^20.3.0",
"@angular/platform-server": "^20.3.0",
"@angular/router": "^20.3.0",
"@angular/ssr": "^20.3.3",
"express": "^5.1.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0"
"tslib": "^2.3.0",
"zod": "^4.1.11"
},
"devDependencies": {
"@angular/build": "^20.3.2",
"@angular/cli": "^20.3.2",
"@angular/compiler-cli": "^20.3.0",
"@types/express": "^5.0.1",
"@types/jasmine": "~5.1.0",
"@types/node": "^20.17.19",
"jasmine-core": "~5.9.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
Expand Down
10 changes: 10 additions & 0 deletions examples/angular-kitchensink/src/app/app.config.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ApplicationConfig, mergeApplicationConfig } from '@angular/core';
import { provideServerRendering, withRoutes } from '@angular/ssr';
import { appConfig } from './app.config';
import { serverRoutes } from './app.routes.server';

const serverConfig: ApplicationConfig = {
providers: [provideServerRendering(withRoutes(serverRoutes))],
};

export const config = mergeApplicationConfig(appConfig, serverConfig);
34 changes: 8 additions & 26 deletions examples/angular-kitchensink/src/app/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,23 @@
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import {
ApplicationConfig,
inject,
provideBrowserGlobalErrorListeners,
provideZonelessChangeDetection,
} from '@angular/core';
import { HttpClient, provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { todoApiInterceptor } from './services/todo.service';
import { Oidc } from './services/oidc.service';
import { firstValueFrom } from 'rxjs';
import { environment } from '../environments/environment';

type RemoteOidcConfig = {
issuerUri: string;
clientId: string;
};
import { routes } from './app.routes';
import { BearerInterceptor } from './interceptors/bearer.interceptor';
import { provideOidc } from './oidc';

export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZonelessChangeDetection(),
provideHttpClient(withInterceptors([todoApiInterceptor])),
provideHttpClient(withInterceptors([BearerInterceptor])),
provideRouter(routes),
environment.useMockOidc
? Oidc.provideMock({
isUserInitiallyLoggedIn: true,
})
: Oidc.provide(async () => {
const http = inject(HttpClient);
const config = await firstValueFrom(http.get<RemoteOidcConfig>('./oidc-config.json'));

return {
issuerUri: config.issuerUri,
clientId: config.clientId,
debugLogs: true,
};
}),
provideOidc(environment.useMockOidc),
provideClientHydration(withEventReplay()),
],
};
135 changes: 74 additions & 61 deletions examples/angular-kitchensink/src/app/app.html
Original file line number Diff line number Diff line change
@@ -1,66 +1,77 @@
<header style="
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
top: 0;
left: 0;
right: 0;
width: calc(100% - 32px);
padding: 8px 16px;
">
<div>
<span>OIDC-SPA + Angular (Kitchen Sink)</span>
&nbsp;
&nbsp;
&nbsp;
&nbsp;
<span>
<a routerLink="/" routerLinkActive #rla="routerLinkActive" [routerLinkActiveOptions]="{ exact: true }"
[style]="{ fontWeight: rla.isActive ? 'bold' : 'normal' }">Home</a>
&nbsp; &nbsp;
<a routerLink="/protected" routerLinkActive #rla2="routerLinkActive"
[style]="{ fontWeight: rla2.isActive ? 'bold' : 'normal' }">My protected page</a>
<header
style="
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
top: 0;
left: 0;
right: 0;
width: calc(100% - 32px);
padding: 8px 16px;
"
>
<div>
<span>OIDC-SPA + Angular (Kitchen Sink)</span>
&nbsp; &nbsp; &nbsp; &nbsp;
<span>
<a
routerLink="/"
routerLinkActive
#rla="routerLinkActive"
[routerLinkActiveOptions]="{ exact: true }"
[style]="{ fontWeight: rla.isActive ? 'bold' : 'normal' }"
>Home</a
>
&nbsp; &nbsp;
<a
routerLink="/protected"
routerLinkActive
#rla2="routerLinkActive"
[style]="{ fontWeight: rla2.isActive ? 'bold' : 'normal' }"
>My protected page</a
>

@defer (when oidc.prInitialized | async) {
&nbsp; &nbsp;
<a routerLink="/admin-only" routerLinkActive #rla3="routerLinkActive"
[style]="{
@defer (when oidc.prInitialized | async) { &nbsp; &nbsp;
<a
routerLink="/admin-only"
routerLinkActive
#rla3="routerLinkActive"
[style]="{
fontWeight: rla3.isActive ? 'bold' : 'normal',
cursor: canShowAdminLink ? 'pointer' : 'not-allowed',
}"
[attr.aria-disabled]="canShowAdminLink ? null : true"
>Admin only</a>
} @placeholder {
&nbsp; &nbsp;
<a routerLink="/admin-only" routerLinkActive>Admin only</a>
}
</span>
</div>
[attr.aria-disabled]="canShowAdminLink ? null : true"
>Admin only</a
>
} @placeholder { &nbsp; &nbsp;
<a routerLink="/admin-only" routerLinkActive>Admin only</a>
}
</span>
</div>

@defer (when oidc.prInitialized | async) {
@if (oidc.isUserLoggedIn) {
<div>
<span>Hello {{ oidc.$decodedIdToken().name }}</span>
&nbsp; &nbsp;
<button (click)="oidc.logout({ redirectTo: 'home' })">Logout</button>
</div>
} @else {
<div>
<button (click)="oidc.login()">Login</button>
<button (click)="
@defer (when oidc.prInitialized | async) { @if (oidc.isUserLoggedIn) {
<div>
<span>Hello {{ oidc.$decodedIdToken().name }}</span>
&nbsp; &nbsp;
<button (click)="oidc.logout({ redirectTo: 'home' })">Logout</button>
</div>
} @else {
<div>
<button (click)="oidc.login()">Login</button>
<button
(click)="
oidc.login({
transformUrlBeforeRedirect: keycloakUtils.transformUrlBeforeRedirectForRegister,
})
">
Register
</button>
</div>
}
} @placeholder {
<span style="line-height: 1.35;">Initializing OIDC...</span>
}

"
>
Register
</button>
</div>
} } @placeholder {
<span style="line-height: 1.35">Initializing OIDC...</span>
}
</header>

<br />
Expand All @@ -69,7 +80,8 @@

@if (oidc.$secondsLeftBeforeAutoLogout() ) {
<!-- Full screen overlay, blurred background -->
<div [style]="{
<div
[style]="{
position: 'fixed',
top: 0,
left: 0,
Expand All @@ -81,10 +93,11 @@
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000,
}">
<div>
<p>Are you still there?</p>
<p>You will be logged out in {{ oidc.$secondsLeftBeforeAutoLogout() }}</p>
</div>
}"
>
<div>
<p>Are you still there?</p>
<p>You will be logged out in {{ oidc.$secondsLeftBeforeAutoLogout() }}</p>
</div>
</div>
}
11 changes: 11 additions & 0 deletions examples/angular-kitchensink/src/app/app.routes.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
{ path: '', renderMode: RenderMode.Prerender },
{ path: 'protected', renderMode: RenderMode.Client },
{ path: 'admin-only', renderMode: RenderMode.Client },
{
path: '**',
renderMode: RenderMode.Client,
},
];
24 changes: 4 additions & 20 deletions examples/angular-kitchensink/src/app/app.routes.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { inject } from '@angular/core';
import { Router, Routes, RedirectCommand } from '@angular/router';
import { Public } from './pages/public';
import { Routes } from '@angular/router';
import { AdminGuard } from './guards/admin.guard';
import { Oidc } from './services/oidc.service';

export const routes: Routes = [
{ path: '', component: Public },
{ path: '', loadComponent: () => import('./pages/public').then((c) => c.Public) },
{
path: 'protected',
loadComponent: () => import('./pages/protected').then((c) => c.Protected),
Expand All @@ -13,22 +12,7 @@ export const routes: Routes = [
{
path: 'admin-only',
loadComponent: () => import('./pages/admin-only').then((c) => c.AdminOnly),
canActivate: [
async (route) => {
const oidc = inject(Oidc);
const router = inject(Router);

await Oidc.enforceLoginGuard(route);

if ((oidc.$decodedIdToken().realm_access?.roles ?? []).includes('admin')) {
return true;
}

alert('Only Admins can access this page');

return new RedirectCommand(router.parseUrl('/'));
},
],
canActivate: [AdminGuard],
},
{ path: '**', redirectTo: '' },
];
18 changes: 18 additions & 0 deletions examples/angular-kitchensink/src/app/guards/admin.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { inject } from '@angular/core';
import { CanActivateFn, RedirectCommand, Router } from '@angular/router';
import { DecodedIdToken, Oidc } from '../services/oidc.service';

export const AdminGuard: CanActivateFn = (route, state) => {
const router = inject(Router);
return Oidc.createAuthGuard<DecodedIdToken>(({ oidc }) => {
if (
oidc.isUserLoggedIn &&
(oidc.getDecodedIdToken().realm_access?.roles ?? []).includes('admin')
) {
return true;
}
alert('Only Admins can access this page');

return new RedirectCommand(router.parseUrl('/'));
})(route, state);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { HttpInterceptorFn } from '@angular/common/http';
import { Oidc } from '../services/oidc.service';

export const BearerInterceptor: HttpInterceptorFn = Oidc.createBasicBearerTokenInterceptor({
conditions: [{ urlPattern: /^(https:\/\/jsonplaceholder\.typicode\.com)(\/.*)?$/i }],
});
23 changes: 23 additions & 0 deletions examples/angular-kitchensink/src/app/oidc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { EnvironmentProviders } from '@angular/core';
import { Oidc } from './services/oidc.service';

type RemoteOidcConfig = {
issuerUri: string;
clientId: string;
};

export const provideOidc = (useMockOidc: boolean): EnvironmentProviders =>
useMockOidc
? Oidc.provideMock({
isUserInitiallyLoggedIn: true,
})
: Oidc.provide(async () => {
// should be runned outside angular to prevent http interceptor request piping
const config: RemoteOidcConfig = await fetch('/oidc-config.json').then((res) => res.json());

return {
issuerUri: config.issuerUri,
clientId: config.clientId,
debugLogs: true,
};
});
Loading