From 5f551bdd904c118174d0a5e13f95833d3d8d7033 Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 16 May 2023 19:13:22 +0200 Subject: [PATCH] feat: add dev login (#195) Co-authored-by: Jasper Herzberg --- .github/workflows/ci.yml | 12 ++- .github/workflows/next-deployment.yml | 8 +- apps/api/dev-tokens.md | 14 +++ apps/spa-e2e/.env.template | 1 - apps/spa-e2e/README.md | 18 ++++ apps/spa-e2e/src/auth.setup.ts | 36 ++++++-- apps/spa-e2e/src/auth.spec.ts | 24 +----- apps/spa-e2e/src/page-objects/login.po.ts | 22 +++-- apps/spa-e2e/src/test-users.ts | 23 ++--- apps/spa/src/app/app.module.ts | 38 ++------ .../spa/src/environments/environment.model.ts | 8 ++ .../spa/src/environments/environment.template | 21 +---- apps/spa/src/environments/environment.ts | 22 +---- .../src/lib/decorators/user.decorator.spec.ts | 4 +- .../lib/interceptors/auth.interceptor.spec.ts | 14 ++- .../src/lib/interceptors/auth.interceptor.ts | 13 ++- libs/api/test-helpers/src/index.ts | 2 +- .../src/lib/execution-context.test-helper.ts | 18 +++- libs/spa/auth/README.md | 8 +- libs/spa/auth/src/index.ts | 4 +- libs/spa/auth/src/lib/auth.module.ts | 59 +++++++++++-- .../src/lib/components/auth.component.spec.ts | 10 ++- .../auth/src/lib/components/auth.component.ts | 6 +- .../components/dev-login.component.spec.ts | 57 ++++++++++++ .../src/lib/components/dev-login.component.ts | 86 +++++++++++++++++++ libs/spa/auth/src/lib/dev-auth.module.ts | 45 ++++++++++ .../auth/src/lib/guards/auth.guard.spec.ts | 4 +- libs/spa/auth/src/lib/guards/auth.guard.ts | 4 +- .../interceptors/dev-auth.interceptor.spec.ts | 67 +++++++++++++++ .../lib/interceptors/dev-auth.interceptor.ts | 37 ++++++++ .../spa/auth/src/lib/services/auth-service.ts | 13 +++ .../src/lib/services/auth.service.spec.ts | 6 +- .../spa/auth/src/lib/services/auth.service.ts | 8 +- .../src/lib/services/dev-auth.service.spec.ts | 71 +++++++++++++++ .../auth/src/lib/services/dev-auth.service.ts | 81 +++++++++++++++++ migrations.json | 8 -- package.json | 10 +-- 37 files changed, 708 insertions(+), 174 deletions(-) create mode 100644 apps/api/dev-tokens.md delete mode 100644 apps/spa-e2e/.env.template create mode 100644 apps/spa-e2e/README.md create mode 100644 apps/spa/src/environments/environment.model.ts create mode 100644 libs/spa/auth/src/lib/components/dev-login.component.spec.ts create mode 100644 libs/spa/auth/src/lib/components/dev-login.component.ts create mode 100644 libs/spa/auth/src/lib/dev-auth.module.ts create mode 100644 libs/spa/auth/src/lib/interceptors/dev-auth.interceptor.spec.ts create mode 100644 libs/spa/auth/src/lib/interceptors/dev-auth.interceptor.ts create mode 100644 libs/spa/auth/src/lib/services/auth-service.ts create mode 100644 libs/spa/auth/src/lib/services/dev-auth.service.spec.ts create mode 100644 libs/spa/auth/src/lib/services/dev-auth.service.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ba186ca..e44e6c8e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,7 @@ jobs: IS_PRODUCTION: true DEPLOYMENT_NAME: E2E Runner API_URL: http://localhost:3000/ - OAUTH_CONFIG: ${{ secrets.DEV_OAUTH_CONFIG }} + OAUTH_CONFIG: undefined - name: Create API Environment File run: | envsubst < apps/api/src/.env.template > apps/api/src/.env @@ -58,9 +58,15 @@ jobs: - name: Start and prepare MongoDB for E2Es run: ./tools/db/kordis-db.sh init e2edb - name: Run E2Es - run: npm run e2e + run: npm run serve:all:prod & (npx wait-on tcp:3000 && npx wait-on http://localhost:4200 && npx nx e2e spa-e2e) env: - TEST_USERS: ${{ secrets.E2E_TEST_USERS }} + E2E_BASE_URL: http://localhost:4200/ + - uses: actions/upload-artifact@v3 + if: failure() + with: + name: e2e-test-results + path: test-results/ + if-no-files-found: ignore - name: SonarCloud Scan uses: sonarsource/sonarcloud-github-action@master diff --git a/.github/workflows/next-deployment.yml b/.github/workflows/next-deployment.yml index 1a6350a5..2df77032 100644 --- a/.github/workflows/next-deployment.yml +++ b/.github/workflows/next-deployment.yml @@ -61,4 +61,10 @@ jobs: run: npx nx e2e spa-e2e env: E2E_BASE_URL: ${{ needs.deployment.outputs.spaUrl }} - TEST_USERS: ${{ secrets.E2E_TEST_USERS }} + AADB2C_TEST_USERS: ${{ secrets.E2E_TEST_USERS }} + - uses: actions/upload-artifact@v3 + if: failure() + with: + name: e2e-test-results + path: test-results/ + if-no-files-found: ignore diff --git a/apps/api/dev-tokens.md b/apps/api/dev-tokens.md new file mode 100644 index 00000000..79dd469f --- /dev/null +++ b/apps/api/dev-tokens.md @@ -0,0 +1,14 @@ +### Development Tokens + +This file contains some development tokens that can be used to directly call the +API with pre-defined claims. The test users are equivalent to the users used in +[E2Es](../spa-e2e/README.md) and the test users registered in the development +application of our AAD. + +| **Username** | **ID** (`oid`) | **First name** (`first_name`) | **Last name** (`last_name`) | **Emails** (`emails`) | Token | +| ------------ | ------------------------------------ | ----------------------------- | --------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| testuser | c0cc4404-7907-4480-86d3-ba4bfc513c6d | Test | User | testuser@kordis-leitstelle.de | `eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJvaWQiOiIxMjM0IiwiZW1haWxzIjpbInRlc3R1c2VyQHRlc3QuY29tIl0sImdpdmVuX25hbWUiOiJUZXN0IiwiZmFtaWx5X25hbWUiOiJVc2VyIDEifQ.` | + +The claims will be mapped to the +[AuthUser](../../libs/shared/auth/src/lib/auth-user.model.ts) Model in the +[AuthInterceptor](../../libs/api/auth/src/lib/interceptors/auth.interceptor.ts). diff --git a/apps/spa-e2e/.env.template b/apps/spa-e2e/.env.template deleted file mode 100644 index e17a45c4..00000000 --- a/apps/spa-e2e/.env.template +++ /dev/null @@ -1 +0,0 @@ -TEST_USERS='[["username", "password"], ["username", "password"]]' diff --git a/apps/spa-e2e/README.md b/apps/spa-e2e/README.md new file mode 100644 index 00000000..3047e818 --- /dev/null +++ b/apps/spa-e2e/README.md @@ -0,0 +1,18 @@ +# Kordis E2E Tests + +For End-to-End testing we use +[Playwright](https://playwright.dev/docs/api/class-playwright). You can run all +Tests with `npm run e2e`. By default, tests are run in headless mode, you can +adjust the [Playwright configuration](./playwright.config.ts) if needed for +local testing. Make sure that you serve the API and the SPA +`npm run serve:all:prod`. If you want to test against an Azure Active Directory +as OAuth Provider, you have to also specify `AADB2C_TEST_USERS` as env variable +with the test users username and password +`[['testusername', 'testpassword'], ...]` (check the +[auth setup](./src/auth.setup.ts) for more information). In this case, the SPA +environment also needs an OAuth configuration. If you leave it empty, it will +run with the Dev Login, which is the default for local dev workstations. + +We have a set of test users. Each test can be executed in the context of a user +with the [`asUser()`](./src/test-users.ts) function. No need to +explicitly log in or out. diff --git a/apps/spa-e2e/src/auth.setup.ts b/apps/spa-e2e/src/auth.setup.ts index 7fe1bc2c..aecd1ba4 100644 --- a/apps/spa-e2e/src/auth.setup.ts +++ b/apps/spa-e2e/src/auth.setup.ts @@ -1,18 +1,38 @@ import { test as setup } from '@playwright/test'; import { LoginPo } from './page-objects/login.po'; -import { getAuthStoragePath, testUserPasswords } from './test-users'; +import { TestUsernames, getAuthStoragePath, testUsernames } from './test-users'; // Documentation: https://playwright.dev/docs/auth#multiple-signed-in-roles setup('authenticate as testusers', async ({ browser }) => { - for (const [username, password] of testUserPasswords.entries()) { - const context = await browser.newContext(); - const page = await context.newPage(); - await new LoginPo(page).login(username, password); - await page.waitForURL('/protected'); + /** + * If Active Directory B2C Users are set, we use them (e.g. in Next Deployment E2Es), + * otherwise we fall back to our preset users for the DevAuthModule that have the same claims and usernames. + */ + if (process.env.AADB2C_TEST_USERS) { + const testUserPasswords: ReadonlyMap = new Map( + JSON.parse(process.env.AADB2C_TEST_USERS), + ); - await context.storageState({ path: getAuthStoragePath(username) }); - await context.close(); + for (const [username, password] of testUserPasswords.entries()) { + const context = await browser.newContext(); + const page = await context.newPage(); + await new LoginPo(page).loginWithB2C(username, password); + await page.waitForURL('/protected'); + + await context.storageState({ path: getAuthStoragePath(username) }); + await context.close(); + } + } else { + for (const username of testUsernames) { + const context = await browser.newContext(); + const page = await context.newPage(); + await new LoginPo(page).loginViaDevAuth(username); + await page.waitForURL('/protected'); + + await context.storageState({ path: getAuthStoragePath(username) }); + await context.close(); + } } }); diff --git a/apps/spa-e2e/src/auth.spec.ts b/apps/spa-e2e/src/auth.spec.ts index 214742fe..d2eb974d 100644 --- a/apps/spa-e2e/src/auth.spec.ts +++ b/apps/spa-e2e/src/auth.spec.ts @@ -1,7 +1,6 @@ import { expect, test } from '@playwright/test'; -import { LoginPo } from './page-objects/login.po'; -import { getAuthStoragePath, testUserPasswords } from './test-users'; +import { asUser } from './test-users'; test('should get redirected to auth as unauthenticated', async ({ page }) => { await page.goto('/'); @@ -10,27 +9,8 @@ test('should get redirected to auth as unauthenticated', async ({ page }) => { await expect(page).toHaveURL('/auth'); }); -test('should not be able to access /protected as unauthenticated', async ({ - page, -}) => { - await page.goto('/protected'); - await page.waitForURL('/auth'); - - await expect(page).toHaveURL('/auth'); -}); - -test('should be able to login with redirect to /protected', async ({ - page, -}) => { - const loginPo = new LoginPo(page); - await loginPo.login('testuser', testUserPasswords.get('testuser')); - - await page.waitForURL('/protected'); - await expect(page).toHaveURL('/protected'); -}); - test.describe('as authenticated', () => { - test.use({ storageState: getAuthStoragePath('testuser') }); + asUser('testuser'); test('should get initially redirected to /protected', async ({ page }) => { await page.goto('/'); diff --git a/apps/spa-e2e/src/page-objects/login.po.ts b/apps/spa-e2e/src/page-objects/login.po.ts index ac189798..6ca6939b 100644 --- a/apps/spa-e2e/src/page-objects/login.po.ts +++ b/apps/spa-e2e/src/page-objects/login.po.ts @@ -13,11 +13,8 @@ export class LoginPo { }; constructor(private readonly page: Page) {} - async login(username: string, password: string): Promise { - await this.page.goto('/auth'); - - const loginBtn = await this.page.waitForSelector(this.selectors.loginBtn); - await loginBtn.click(); + async loginWithB2C(username: string, password: string): Promise { + await this.gotoLoginPage(); const usernameInput = await this.page.waitForSelector( this.selectors.b2c.userIdInput, @@ -33,4 +30,19 @@ export class LoginPo { await passwordInput.type(password); await b2cLoginBtn.click(); } + + async loginViaDevAuth(username: string): Promise { + await this.gotoLoginPage(); + const testUserLoginBtn = await this.page.waitForSelector( + `button[data-username="${username}"]`, + ); + await testUserLoginBtn.click(); + } + + private async gotoLoginPage(): Promise { + await this.page.goto('/auth'); + + const loginBtn = await this.page.waitForSelector(this.selectors.loginBtn); + await loginBtn.click(); + } } diff --git a/apps/spa-e2e/src/test-users.ts b/apps/spa-e2e/src/test-users.ts index 3397a264..063f3b77 100644 --- a/apps/spa-e2e/src/test-users.ts +++ b/apps/spa-e2e/src/test-users.ts @@ -1,21 +1,12 @@ -import { config as configEnv } from 'dotenv'; -import * as path from 'path'; +import { test } from '@playwright/test'; -export type testUsernames = 'testuser'; +export const testUsernames = ['testuser'] as const; +export type TestUsernames = (typeof testUsernames)[number]; -if (!process.env.CI) { - configEnv({ - path: path.resolve(__dirname, '../../.env'), - }); +export function getAuthStoragePath(username: TestUsernames): string { + return `playwright/.auth/${username}.json`; } -/* - You should take a close look at what test user runs what test, so you do not use any user that might execute test that have side effects on your test! - */ -export const testUserPasswords: ReadonlyMap = new Map( - JSON.parse(process.env.TEST_USERS), -); - -export function getAuthStoragePath(username: testUsernames): string { - return `playwright/.auth/${username}.json`; +export function asUser(username: TestUsernames): void { + test.use({ storageState: getAuthStoragePath(username) }); } diff --git a/apps/spa/src/app/app.module.ts b/apps/spa/src/app/app.module.ts index c1447db2..a12b3fe5 100644 --- a/apps/spa/src/app/app.module.ts +++ b/apps/spa/src/app/app.module.ts @@ -1,15 +1,9 @@ import { HttpClientModule } from '@angular/common/http'; -import { NgModule, inject } from '@angular/core'; +import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; -import { Router, RouterModule, Routes } from '@angular/router'; -import { switchMap } from 'rxjs'; +import { RouterModule, Routes } from '@angular/router'; -import { - AuthComponent, - AuthModule, - AuthService, - authGuard, -} from '@kordis/spa/auth'; +import { AuthModule, DevAuthModule, authGuard } from '@kordis/spa/auth'; import { environment } from '../environments/environment'; import { AppComponent } from './app.component'; @@ -21,22 +15,6 @@ const routes: Routes = [ redirectTo: 'protected', pathMatch: 'full', }, - { - path: 'auth', - component: AuthComponent, - canActivate: [ - () => { - const auth = inject(AuthService); - const router = inject(Router); - - return auth.isAuthenticated$.pipe( - switchMap(async (isAuthenticated) => - isAuthenticated ? router.navigate(['/protected']) : true, - ), - ); - }, - ], - }, { path: 'protected', component: ProtectedComponent, @@ -51,10 +29,12 @@ const routes: Routes = [ BrowserModule, HttpClientModule, RouterModule.forRoot(routes), - AuthModule.forRoot( - environment.oauth.config, - environment.oauth.discoveryDocumentUrl, - ), + environment.oauth + ? AuthModule.forRoot( + environment.oauth.config, + environment.oauth.discoveryDocumentUrl, + ) + : DevAuthModule.forRoot(), ], providers: [], bootstrap: [AppComponent], diff --git a/apps/spa/src/environments/environment.model.ts b/apps/spa/src/environments/environment.model.ts new file mode 100644 index 00000000..1f6f13a6 --- /dev/null +++ b/apps/spa/src/environments/environment.model.ts @@ -0,0 +1,8 @@ +import { AuthConfig } from 'angular-oauth2-oidc'; + +export type Environment = { + production: boolean; + apiUrl: string; + deploymentName: string; + oauth?: { discoveryDocumentUrl: string; config: AuthConfig }; +}; diff --git a/apps/spa/src/environments/environment.template b/apps/spa/src/environments/environment.template index a0da0497..1842d0e4 100644 --- a/apps/spa/src/environments/environment.template +++ b/apps/spa/src/environments/environment.template @@ -1,21 +1,8 @@ -export const environment = { +import { Environment } from './environment.model'; + +export const environment: Environment = { production: $IS_PRODUCTION, deploymentName: '$DEPLOYMENT_NAME', apiUrl: '$API_URL', - oauth: { // todo: replace this with /$/OAUTH_CONFIG before merge into main - config: { - redirectUri: window.origin + '/auth', - oidc: true, - responseType: 'code', - clientId: '6b5aa2b3-6237-44ba-8448-252052e73831', - issuer: - 'https://kordisleitstelle.b2clogin.com/5b974891-a530-4e68-ac04-e26a18c3bd46/v2.0/', - tokenEndpoint: - 'https://kordisleitstelle.b2clogin.com/kordisleitstelle.onmicrosoft.com/b2c_1_signin/oauth2/v2.0/token', - scope: 'openid offline_access 6b5aa2b3-6237-44ba-8448-252052e73831', - strictDiscoveryDocumentValidation: false, - }, - discoveryDocumentUrl: - 'https://kordisleitstelle.b2clogin.com/kordisleitstelle.onmicrosoft.com/B2C_1_SignIn/v2.0/.well-known/openid-configuration', - }, + oauth: $OAUTH_CONFIG as any, }; diff --git a/apps/spa/src/environments/environment.ts b/apps/spa/src/environments/environment.ts index 1852f979..68de3f65 100644 --- a/apps/spa/src/environments/environment.ts +++ b/apps/spa/src/environments/environment.ts @@ -1,23 +1,7 @@ -import { AuthConfig } from 'angular-oauth2-oidc'; +import { Environment } from './environment.model'; -export const environment = { +export const environment: Environment = { production: false, deploymentName: 'Dev Local', - apiUrl: 'https://localhost:3333', - oauth: { - config: { - redirectUri: window.origin + '/auth', - oidc: true, - responseType: 'code', - clientId: '6b5aa2b3-6237-44ba-8448-252052e73831', - issuer: - 'https://kordisleitstelle.b2clogin.com/5b974891-a530-4e68-ac04-e26a18c3bd46/v2.0/', - tokenEndpoint: - 'https://kordisleitstelle.b2clogin.com/kordisleitstelle.onmicrosoft.com/b2c_1_signin/oauth2/v2.0/token', - scope: 'openid offline_access 6b5aa2b3-6237-44ba-8448-252052e73831', - strictDiscoveryDocumentValidation: false, - } as AuthConfig, - discoveryDocumentUrl: - 'https://kordisleitstelle.b2clogin.com/kordisleitstelle.onmicrosoft.com/B2C_1_SignIn/v2.0/.well-known/openid-configuration', - }, + apiUrl: 'https://localhost:3000', }; diff --git a/libs/api/auth/src/lib/decorators/user.decorator.spec.ts b/libs/api/auth/src/lib/decorators/user.decorator.spec.ts index 071ccca7..fa49231d 100644 --- a/libs/api/auth/src/lib/decorators/user.decorator.spec.ts +++ b/libs/api/auth/src/lib/decorators/user.decorator.spec.ts @@ -2,7 +2,7 @@ import { createMock } from '@golevelup/ts-jest'; import { KordisRequest } from '@kordis/api/shared'; import { - createContextForRequest, + createGqlContextForRequest, createParamDecoratorFactory, } from '@kordis/api/test-helpers'; import { AuthUser } from '@kordis/shared/auth'; @@ -20,7 +20,7 @@ describe('User Decorator', () => { const req = createMock({ user, }); - const context = createContextForRequest(req); + const context = createGqlContextForRequest(req); const factory = createParamDecoratorFactory(User); const result = factory(null, context); expect(result).toEqual(user); diff --git a/libs/api/auth/src/lib/interceptors/auth.interceptor.spec.ts b/libs/api/auth/src/lib/interceptors/auth.interceptor.spec.ts index e1858eb0..b4cc7d3e 100644 --- a/libs/api/auth/src/lib/interceptors/auth.interceptor.spec.ts +++ b/libs/api/auth/src/lib/interceptors/auth.interceptor.spec.ts @@ -3,7 +3,7 @@ import { CallHandler, UnauthorizedException } from '@nestjs/common'; import { Observable, firstValueFrom, of } from 'rxjs'; import { KordisRequest } from '@kordis/api/shared'; -import { createContextForRequest } from '@kordis/api/test-helpers'; +import { createGqlContextForRequest } from '@kordis/api/test-helpers'; import { AuthUser } from '@kordis/shared/auth'; import { AuthUserExtractorStrategy } from '../auth-user-extractor-strategies/auth-user-extractor.strategy'; @@ -35,7 +35,7 @@ describe('AuthInterceptor', () => { await expect( firstValueFrom( service.intercept( - createContextForRequest(createMock()), + createGqlContextForRequest(createMock()), createMock(), ), ), @@ -56,10 +56,16 @@ describe('AuthInterceptor', () => { }, }); - const ctxMock = createContextForRequest(createMock()); + const gqlCtx = createGqlContextForRequest(createMock()); await expect( - firstValueFrom(service.intercept(ctxMock, handler)), + firstValueFrom(service.intercept(gqlCtx, handler)), + ).resolves.toBeTruthy(); + + const httpCtx = createGqlContextForRequest(createMock()); + + await expect( + firstValueFrom(service.intercept(httpCtx, handler)), ).resolves.toBeTruthy(); }); }); diff --git a/libs/api/auth/src/lib/interceptors/auth.interceptor.ts b/libs/api/auth/src/lib/interceptors/auth.interceptor.ts index ed5a432d..eb27be5c 100644 --- a/libs/api/auth/src/lib/interceptors/auth.interceptor.ts +++ b/libs/api/auth/src/lib/interceptors/auth.interceptor.ts @@ -5,10 +5,10 @@ import { NestInterceptor, UnauthorizedException, } from '@nestjs/common'; -import { GqlExecutionContext } from '@nestjs/graphql'; +import { GqlContextType, GqlExecutionContext } from '@nestjs/graphql'; import { Observable, throwError } from 'rxjs'; -import { KordisGqlContext } from '@kordis/api/shared'; +import { KordisGqlContext, KordisRequest } from '@kordis/api/shared'; import { AuthUserExtractorStrategy } from '../auth-user-extractor-strategies/auth-user-extractor.strategy'; @@ -17,8 +17,13 @@ export class AuthInterceptor implements NestInterceptor { constructor(private readonly authUserExtractor: AuthUserExtractorStrategy) {} intercept(context: ExecutionContext, next: CallHandler): Observable { - const ctx = GqlExecutionContext.create(context); - const req = ctx.getContext().req; + let req: KordisRequest; + if (context.getType() === 'graphql') { + const ctx = GqlExecutionContext.create(context); + req = ctx.getContext().req; + } else { + req = context.switchToHttp().getRequest(); + } const possibleAuthUser = this.authUserExtractor.getUserFromRequest(req); diff --git a/libs/api/test-helpers/src/index.ts b/libs/api/test-helpers/src/index.ts index c040fa94..02ae541b 100644 --- a/libs/api/test-helpers/src/index.ts +++ b/libs/api/test-helpers/src/index.ts @@ -1,2 +1,2 @@ -export { createContextForRequest } from './lib/execution-context.test-helper'; +export { createGqlContextForRequest } from './lib/execution-context.test-helper'; export { createParamDecoratorFactory } from './lib/decorator.test-helper'; diff --git a/libs/api/test-helpers/src/lib/execution-context.test-helper.ts b/libs/api/test-helpers/src/lib/execution-context.test-helper.ts index 5577ba05..0e6b5bb3 100644 --- a/libs/api/test-helpers/src/lib/execution-context.test-helper.ts +++ b/libs/api/test-helpers/src/lib/execution-context.test-helper.ts @@ -1,9 +1,10 @@ import { createMock } from '@golevelup/ts-jest'; import { ExecutionContext } from '@nestjs/common'; +import { HttpArgumentsHost } from '@nestjs/common/interfaces'; import { KordisRequest } from '@kordis/api/shared'; -export function createContextForRequest(req: KordisRequest) { +export function createGqlContextForRequest(req: KordisRequest) { return createMock({ getArgs(): any[] { return [null, null, { req }, null]; @@ -13,3 +14,18 @@ export function createContextForRequest(req: KordisRequest) { }, }); } + +export function createHttpContextForRequest(req: KordisRequest) { + return createMock({ + getType(): string { + return 'http'; + }, + switchToHttp(): HttpArgumentsHost { + return createMock({ + getRequest(): KordisRequest { + return req; + }, + }); + }, + }); +} diff --git a/libs/spa/auth/README.md b/libs/spa/auth/README.md index 1f1d0aa6..6814c4a8 100644 --- a/libs/spa/auth/README.md +++ b/libs/spa/auth/README.md @@ -10,7 +10,13 @@ safely consume the `isAuthenticated$`and `user$` observables through the `AuthService` at any lifecycle level. The `login` method will start the Code Flow and redirect to the given OAuth Provider. As a session store we choose _localStorage_ over the _sessionStorage_ due to its persistence and easier -testing via Playwrights `sessionStorage`. +testing via Playwrights `sessionStorage`. This is only active in production +environments. + +For **local dev environments** we use the `DevAuthModule`. Instead of getting +redirected to the OAuth Provider, the user will be redirected to the development +login, which lets developer login as a test user with a predefined set or a +custom set of claims. ## Running unit tests diff --git a/libs/spa/auth/src/index.ts b/libs/spa/auth/src/index.ts index 1da8ab2c..f4c4c64a 100644 --- a/libs/spa/auth/src/index.ts +++ b/libs/spa/auth/src/index.ts @@ -1,4 +1,4 @@ export { authGuard } from './lib/guards/auth.guard'; -export { AuthService } from './lib/services/auth.service'; -export { AuthComponent } from './lib/components/auth.component'; +export { AUTH_SERVICE, AuthService } from './lib/services/auth-service'; export { AuthModule } from './lib/auth.module'; +export { DevAuthModule } from './lib/dev-auth.module'; diff --git a/libs/spa/auth/src/lib/auth.module.ts b/libs/spa/auth/src/lib/auth.module.ts index 79b641f0..9e1a1932 100644 --- a/libs/spa/auth/src/lib/auth.module.ts +++ b/libs/spa/auth/src/lib/auth.module.ts @@ -1,22 +1,55 @@ import { CommonModule } from '@angular/common'; -import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '@angular/core'; +import { HTTP_INTERCEPTORS } from '@angular/common/http'; +import { + APP_INITIALIZER, + ModuleWithProviders, + NgModule, + inject, +} from '@angular/core'; +import { Router, RouterModule } from '@angular/router'; import { AuthConfig, + DefaultOAuthInterceptor, OAuthModule, OAuthService, OAuthStorage, } from 'angular-oauth2-oidc'; +import { switchMap } from 'rxjs'; import { AuthComponent } from './components/auth.component'; -import { AuthService } from './services/auth.service'; - -const PROVIDERS = Object.freeze([AuthService]); +import { AUTH_SERVICE } from './services/auth-service'; +import { ProdAuthService } from './services/auth.service'; @NgModule({ + imports: [ + CommonModule, + RouterModule.forChild([ + { + path: 'auth', + component: AuthComponent, + canActivate: [ + () => { + const auth = inject(AUTH_SERVICE); + const router = inject(Router); + + return auth.isAuthenticated$.pipe( + switchMap(async (isAuthenticated) => + isAuthenticated ? router.navigate(['/protected']) : true, + ), + ); + }, + ], + }, + ]), + ], declarations: [AuthComponent], - imports: [CommonModule, OAuthModule.forRoot()], - exports: [AuthComponent], - providers: [...PROVIDERS], + exports: [AuthComponent, RouterModule], +}) +export class BaseAuthModule {} + +@NgModule({ + imports: [CommonModule, BaseAuthModule, OAuthModule.forRoot()], + exports: [RouterModule], }) export class AuthModule { static forRoot( @@ -26,7 +59,10 @@ export class AuthModule { return { ngModule: AuthModule, providers: [ - ...PROVIDERS, + { + provide: AUTH_SERVICE, + useClass: ProdAuthService, + }, { provide: OAuthStorage, useFactory: () => localStorage, @@ -41,7 +77,12 @@ export class AuthModule { await oauthService.tryLoginCodeFlow(); }; }, - deps: [OAuthService, AuthService], + deps: [OAuthService, AUTH_SERVICE], + multi: true, + }, + { + provide: HTTP_INTERCEPTORS, + useClass: DefaultOAuthInterceptor, multi: true, }, ], diff --git a/libs/spa/auth/src/lib/components/auth.component.spec.ts b/libs/spa/auth/src/lib/components/auth.component.spec.ts index 2b9bc6bf..57579488 100644 --- a/libs/spa/auth/src/lib/components/auth.component.spec.ts +++ b/libs/spa/auth/src/lib/components/auth.component.spec.ts @@ -1,13 +1,19 @@ +import { createMock } from '@golevelup/ts-jest'; import { SpectatorRouting, createRoutingFactory } from '@ngneat/spectator/jest'; -import { AuthService } from '../services/auth.service'; +import { AUTH_SERVICE, AuthService } from '../services/auth-service'; import { AuthComponent } from './auth.component'; describe('AuthComponent', () => { let spectator: SpectatorRouting; const createComponent = createRoutingFactory({ component: AuthComponent, - componentMocks: [AuthService], + componentProviders: [ + { + provide: AUTH_SERVICE, + useValue: createMock(), + }, + ], }); beforeEach(() => (spectator = createComponent())); diff --git a/libs/spa/auth/src/lib/components/auth.component.ts b/libs/spa/auth/src/lib/components/auth.component.ts index 98877199..ecc8e48d 100644 --- a/libs/spa/auth/src/lib/components/auth.component.ts +++ b/libs/spa/auth/src/lib/components/auth.component.ts @@ -1,8 +1,8 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable, map } from 'rxjs'; -import { AuthService } from '../services/auth.service'; +import { AUTH_SERVICE, AuthService } from '../services/auth-service'; @Component({ selector: 'krd-auth', @@ -66,7 +66,7 @@ export class AuthComponent { constructor( private readonly activatedRoute: ActivatedRoute, - private readonly authService: AuthService, + @Inject(AUTH_SERVICE) private readonly authService: AuthService, ) { this.hasAuthError$ = this.activatedRoute.queryParams.pipe( map((params) => !!params['error']), diff --git a/libs/spa/auth/src/lib/components/dev-login.component.spec.ts b/libs/spa/auth/src/lib/components/dev-login.component.spec.ts new file mode 100644 index 00000000..7803aaab --- /dev/null +++ b/libs/spa/auth/src/lib/components/dev-login.component.spec.ts @@ -0,0 +1,57 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { createMock } from '@golevelup/ts-jest'; +import { SpectatorRouting, createRoutingFactory } from '@ngneat/spectator/jest'; + +import { AUTH_SERVICE } from '../services/auth-service'; +import { DevAuthService } from '../services/dev-auth.service'; +import { DevLoginComponent } from './dev-login.component'; + +describe('DevLoginComponent', () => { + let spectator: SpectatorRouting; + const authServiceMock = createMock(); + const createComponent = createRoutingFactory({ + component: DevLoginComponent, + imports: [ReactiveFormsModule], + componentProviders: [ + { + provide: AUTH_SERVICE, + useValue: authServiceMock, + }, + ], + }); + + beforeEach(() => (spectator = createComponent())); + afterEach(() => jest.clearAllMocks()); + + it('should login as test user when loginAsTestuser is called', () => { + spectator.component.loginAsTestuser(0); + expect(spectator.router.navigate).toHaveBeenCalledWith(['/']); + }); + + it('should not login with custom claims when form is invalid', () => { + const alertSpy = jest.spyOn(window, 'alert'); + alertSpy.mockImplementation(); + + spectator.component.loginWithCustomClaims(); + expect(alertSpy).toHaveBeenCalledWith('Please fill out all claim fields'); + expect(authServiceMock.setSession).not.toHaveBeenCalled(); + expect(spectator.router.navigate).not.toHaveBeenCalled(); + }); + + it('should login with custom claims when form is valid', () => { + spectator.component.customClaimsForm.setValue({ + id: '1234', + firstName: 'Test', + lastName: 'User 1', + email: 'testuser@test.com', + }); + spectator.component.loginWithCustomClaims(); + expect(authServiceMock.setSession).toHaveBeenCalledWith({ + id: '1234', + firstName: 'Test', + lastName: 'User 1', + email: 'testuser@test.com', + }); + expect(spectator.router.navigate).toHaveBeenCalledWith(['/']); + }); +}); diff --git a/libs/spa/auth/src/lib/components/dev-login.component.ts b/libs/spa/auth/src/lib/components/dev-login.component.ts new file mode 100644 index 00000000..a993240f --- /dev/null +++ b/libs/spa/auth/src/lib/components/dev-login.component.ts @@ -0,0 +1,86 @@ +import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; +import { FormBuilder, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; + +import { AuthUser } from '@kordis/shared/auth'; + +import { AUTH_SERVICE } from '../services/auth-service'; +import { DevAuthService } from '../services/dev-auth.service'; + +const TEST_USERS: Readonly = Object.freeze([ + { + firstName: 'Test', + lastName: 'User', + email: 'testuser@test.com', + id: 'testuser@kordis-leitstelle.de', + }, +]); + +@Component({ + selector: 'krd-auth', + styles: [ + ` + button { + @apply rounded bg-blue-500 px-4 py-2 text-white; + } + + input { + @apply rounded-md border p-1.5; + } + `, + ], + template: ` +
+
+ +
+
+ + + + + + + + + +
+
+ `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DevLoginComponent { + customClaimsForm = this.fb.group({ + id: ['', Validators.required], + firstName: ['', Validators.required], + lastName: ['', Validators.required], + email: ['', Validators.required], + }); + + constructor( + @Inject(AUTH_SERVICE) private readonly devAuthService: DevAuthService, + private readonly fb: FormBuilder, + private readonly router: Router, + ) {} + + loginAsTestuser(id: number): void { + this.devAuthService.setSession(TEST_USERS[id]); + void this.router.navigate(['/']); + } + + loginWithCustomClaims(): void { + if (!this.customClaimsForm.valid) { + window.alert('Please fill out all claim fields'); + return; + } + + this.devAuthService.setSession(this.customClaimsForm.value as AuthUser); + void this.router.navigate(['/']); + } +} diff --git a/libs/spa/auth/src/lib/dev-auth.module.ts b/libs/spa/auth/src/lib/dev-auth.module.ts new file mode 100644 index 00000000..c724c98c --- /dev/null +++ b/libs/spa/auth/src/lib/dev-auth.module.ts @@ -0,0 +1,45 @@ +import { CommonModule } from '@angular/common'; +import { HTTP_INTERCEPTORS } from '@angular/common/http'; +import { ModuleWithProviders, NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; + +import { BaseAuthModule } from './auth.module'; +import { DevLoginComponent } from './components/dev-login.component'; +import { DevAuthInterceptor } from './interceptors/dev-auth.interceptor'; +import { AUTH_SERVICE } from './services/auth-service'; +import { DevAuthService } from './services/dev-auth.service'; + +@NgModule({ + declarations: [DevLoginComponent], + imports: [ + CommonModule, + BaseAuthModule, + ReactiveFormsModule, + RouterModule.forChild([ + { + path: 'auth/dev-login', + component: DevLoginComponent, + }, + ]), + ], + exports: [RouterModule], +}) +export class DevAuthModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: DevAuthModule, + providers: [ + { + provide: AUTH_SERVICE, + useClass: DevAuthService, + }, + { + provide: HTTP_INTERCEPTORS, + useClass: DevAuthInterceptor, + multi: true, + }, + ], + }; + } +} diff --git a/libs/spa/auth/src/lib/guards/auth.guard.spec.ts b/libs/spa/auth/src/lib/guards/auth.guard.spec.ts index 9644e0bb..3dfc8a79 100644 --- a/libs/spa/auth/src/lib/guards/auth.guard.spec.ts +++ b/libs/spa/auth/src/lib/guards/auth.guard.spec.ts @@ -8,7 +8,7 @@ import { import { createMock } from '@golevelup/ts-jest'; import { Observable, ReplaySubject, Subject, firstValueFrom } from 'rxjs'; -import { AuthService } from '../services/auth.service'; +import { AUTH_SERVICE, AuthService } from '../services/auth-service'; import { authGuard } from './auth.guard'; describe('AuthGuard', () => { @@ -33,7 +33,7 @@ describe('AuthGuard', () => { TestBed.configureTestingModule({ providers: [ - { provide: AuthService, useValue: authServiceMock }, + { provide: AUTH_SERVICE, useValue: authServiceMock }, { provide: Router, useValue: routerMock }, ], }); diff --git a/libs/spa/auth/src/lib/guards/auth.guard.ts b/libs/spa/auth/src/lib/guards/auth.guard.ts index 561dbe33..3f25b885 100644 --- a/libs/spa/auth/src/lib/guards/auth.guard.ts +++ b/libs/spa/auth/src/lib/guards/auth.guard.ts @@ -2,10 +2,10 @@ import { inject } from '@angular/core'; import { CanActivateFn, Router } from '@angular/router'; import { switchMap } from 'rxjs'; -import { AuthService } from '../services/auth.service'; +import { AUTH_SERVICE } from '../services/auth-service'; export const authGuard: CanActivateFn = () => { - const authService = inject(AuthService); + const authService = inject(AUTH_SERVICE); const router = inject(Router); return authService.isAuthenticated$.pipe( diff --git a/libs/spa/auth/src/lib/interceptors/dev-auth.interceptor.spec.ts b/libs/spa/auth/src/lib/interceptors/dev-auth.interceptor.spec.ts new file mode 100644 index 00000000..8ef25619 --- /dev/null +++ b/libs/spa/auth/src/lib/interceptors/dev-auth.interceptor.spec.ts @@ -0,0 +1,67 @@ +import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; + +import { AUTH_SERVICE } from '../services/auth-service'; +import { DevAuthService } from '../services/dev-auth.service'; +import { DevAuthInterceptor } from './dev-auth.interceptor'; + +describe('DevAuthInterceptor', () => { + let authService: DevAuthService; + let httpClient: HttpClient; + let httpTestingController: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { provide: AUTH_SERVICE, useClass: DevAuthService }, + { + provide: HTTP_INTERCEPTORS, + useClass: DevAuthInterceptor, + multi: true, + }, + ], + }); + + authService = TestBed.inject(AUTH_SERVICE); + httpClient = TestBed.inject(HttpClient); + httpTestingController = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpTestingController.verify(); + localStorage.clear(); + }); + + it('should add an Authorization header with Bearer token when token is present', () => { + const token = + 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJvaWQiOiIxYTJiM2M0ZCIsImVtYWlscyI6WyJqb2huLmRvZUBleGFtcGxlLmNvbSJdLCJnaXZlbl9uYW1lIjoiSm9obiIsImZhbWlseV9uYW1lIjoiRG9lIn0.'; + authService.setSession({ + id: '1a2b3c4d', + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + }); + + httpClient.get('/test').subscribe(); + + const httpRequest = httpTestingController.expectOne('/test'); + + expect(httpRequest.request.headers.has('Authorization')).toBeTruthy(); + expect(httpRequest.request.headers.get('Authorization')).toBe( + `Bearer ${token}`, + ); + }); + + it('should not add an Authorization header when token is not present', () => { + httpClient.get('/test').subscribe(); + + const httpRequest = httpTestingController.expectOne('/test'); + + expect(httpRequest.request.headers.has('Authorization')).toBeFalsy(); + }); +}); diff --git a/libs/spa/auth/src/lib/interceptors/dev-auth.interceptor.ts b/libs/spa/auth/src/lib/interceptors/dev-auth.interceptor.ts new file mode 100644 index 00000000..a1529b38 --- /dev/null +++ b/libs/spa/auth/src/lib/interceptors/dev-auth.interceptor.ts @@ -0,0 +1,37 @@ +import { + HttpEvent, + HttpHandler, + HttpInterceptor, + HttpRequest, +} from '@angular/common/http'; +import { Inject, Injectable } from '@angular/core'; +import { Observable, first } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +import { AUTH_SERVICE } from '../services/auth-service'; +import { DevAuthService } from '../services/dev-auth.service'; + +@Injectable() +export class DevAuthInterceptor implements HttpInterceptor { + constructor(@Inject(AUTH_SERVICE) private authService: DevAuthService) {} + + intercept( + req: HttpRequest, + next: HttpHandler, + ): Observable> { + return this.authService.token$.pipe( + first(), + switchMap((token) => { + if (token) { + const authReq = req.clone({ + setHeaders: { + Authorization: `Bearer ${token}`, + }, + }); + return next.handle(authReq); + } + return next.handle(req); + }), + ); + } +} diff --git a/libs/spa/auth/src/lib/services/auth-service.ts b/libs/spa/auth/src/lib/services/auth-service.ts new file mode 100644 index 00000000..85b34126 --- /dev/null +++ b/libs/spa/auth/src/lib/services/auth-service.ts @@ -0,0 +1,13 @@ +import { InjectionToken } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { AuthUser } from '@kordis/shared/auth'; + +export interface AuthService { + readonly user$: Observable; + readonly isAuthenticated$: Observable; + login(): void; + logout(): void; +} + +export const AUTH_SERVICE = new InjectionToken('AUTH_SERVICE'); diff --git a/libs/spa/auth/src/lib/services/auth.service.spec.ts b/libs/spa/auth/src/lib/services/auth.service.spec.ts index a0c4c238..73ee40f9 100644 --- a/libs/spa/auth/src/lib/services/auth.service.spec.ts +++ b/libs/spa/auth/src/lib/services/auth.service.spec.ts @@ -3,14 +3,14 @@ import { createServiceFactory, mockProvider } from '@ngneat/spectator/jest'; import { OAuthEvent, OAuthService } from 'angular-oauth2-oidc'; import { Subject, firstValueFrom } from 'rxjs'; -import { AuthService } from './auth.service'; +import { ProdAuthService } from './auth.service'; describe('AuthService', () => { - let spectator: SpectatorService; + let spectator: SpectatorService; const mockEventSubject$ = new Subject(); const createService = createServiceFactory({ - service: AuthService, + service: ProdAuthService, providers: [ mockProvider(OAuthService, { // eslint-disable-next-line rxjs/finnish,rxjs/suffix-subjects diff --git a/libs/spa/auth/src/lib/services/auth.service.ts b/libs/spa/auth/src/lib/services/auth.service.ts index 73b0d7e0..cbaed151 100644 --- a/libs/spa/auth/src/lib/services/auth.service.ts +++ b/libs/spa/auth/src/lib/services/auth.service.ts @@ -10,10 +10,10 @@ import { import { AuthUser } from '@kordis/shared/auth'; -@Injectable({ - providedIn: 'root', -}) -export class AuthService { +import { AuthService } from './auth-service'; + +@Injectable() +export class ProdAuthService implements AuthService { readonly user$: Observable; readonly isAuthenticated$: Observable; private readonly isAuthenticatedSubject$ = new BehaviorSubject( diff --git a/libs/spa/auth/src/lib/services/dev-auth.service.spec.ts b/libs/spa/auth/src/lib/services/dev-auth.service.spec.ts new file mode 100644 index 00000000..41b20284 --- /dev/null +++ b/libs/spa/auth/src/lib/services/dev-auth.service.spec.ts @@ -0,0 +1,71 @@ +import { Router } from '@angular/router'; +import { SpectatorService, createServiceFactory } from '@ngneat/spectator/jest'; +import { firstValueFrom } from 'rxjs'; + +import { DevAuthService } from './dev-auth.service'; + +describe('DevAuthService', () => { + let spectator: SpectatorService; + let routerSpy: Router; + + const createService = createServiceFactory({ + service: DevAuthService, + mocks: [Router], + }); + + beforeEach(() => { + routerSpy = createService().inject(Router); + spectator = createService(); + }); + + afterEach(() => { + jest.clearAllMocks(); + localStorage.clear(); + }); + + it('should be created', () => { + expect(spectator.service).toBeTruthy(); + }); + + describe('setSession', () => { + it('should save the user and token in local storage and set the user and token subjects', async () => { + const user = { + firstName: 'John', + lastName: 'Doe', + email: 'johndoe@example.com', + id: '1234', + }; + spectator.service.setSession(user); + expect(localStorage.getItem('krdDevUser')).toBe(JSON.stringify(user)); + expect(localStorage.getItem('krdDevToken')).toBeDefined(); + + await expect(firstValueFrom(spectator.service.user$)).resolves.toEqual( + user, + ); + await expect( + firstValueFrom(spectator.service.token$), + ).resolves.toBeDefined(); + }); + }); + + describe('logout', () => { + it('should remove the user and token from local storage and set the user and token subjects to null', async () => { + localStorage.setItem('krdDevUser', JSON.stringify({ id: '1234' })); + localStorage.setItem('krdDevToken', 'token'); + spectator.service.logout(); + expect(localStorage.getItem('krdDevUser')).toBeNull(); + expect(localStorage.getItem('krdDevToken')).toBeNull(); + await expect(firstValueFrom(spectator.service.user$)).resolves.toBeNull(); + await expect( + firstValueFrom(spectator.service.token$), + ).resolves.toBeNull(); + }); + }); + + describe('login', () => { + it('should navigate to the dev login page', () => { + spectator.service.login(); + expect(routerSpy.navigate).toHaveBeenCalledWith(['/auth/dev-login']); + }); + }); +}); diff --git a/libs/spa/auth/src/lib/services/dev-auth.service.ts b/libs/spa/auth/src/lib/services/dev-auth.service.ts new file mode 100644 index 00000000..cc5816b3 --- /dev/null +++ b/libs/spa/auth/src/lib/services/dev-auth.service.ts @@ -0,0 +1,81 @@ +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; +import { BehaviorSubject, Observable, map } from 'rxjs'; + +import { AuthUser } from '@kordis/shared/auth'; + +import { AuthService } from './auth-service'; + +/** + * This is a simple implementation of the AuthService which is intended to be used in development. + * It saves the user and token in the local storage and allows you to set the user manually. There is no OAuth process involved. + */ +@Injectable() +export class DevAuthService implements AuthService { + readonly isAuthenticated$: Observable; + readonly user$: Observable; + readonly token$: Observable; + private readonly tokenSubject$ = new BehaviorSubject(null); + private readonly userSubject$ = new BehaviorSubject(null); + + constructor(private readonly router: Router) { + this.user$ = this.userSubject$.asObservable(); + this.isAuthenticated$ = this.user$.pipe(map((user) => !!user)); + this.token$ = this.tokenSubject$.asObservable(); + + const possibleUserSession = localStorage.getItem('krdDevUser'); + if (possibleUserSession) { + this.userSubject$.next(JSON.parse(possibleUserSession) as AuthUser); + this.tokenSubject$.next(localStorage.getItem('krdDevToken')); + } + } + + setSession(user: AuthUser): void { + const token = this.createDevJwt(this.authUserToTokenPayload(user)); + localStorage.setItem('krdDevUser', JSON.stringify(user)); + localStorage.setItem('krdDevToken', token); + this.userSubject$.next(user); + this.tokenSubject$.next(token); + } + + login(): void { + void this.router.navigate(['/auth/dev-login']); + } + + logout(): void { + localStorage.removeItem('krdDevUser'); + localStorage.removeItem('krdDevToken'); + this.userSubject$.next(null); + this.tokenSubject$.next(null); + } + + private authUserToTokenPayload(authUser: AuthUser): { + emails: [string]; + oid: string; + given_name: string; + family_name: string; + } { + return { + oid: authUser.id, + emails: [authUser.email], + given_name: authUser.firstName, + family_name: authUser.lastName, + }; + } + + private createDevJwt(payload: unknown): string { + const base64UrlEncode = (str: string): string => { + const base64 = btoa(str); + return base64.replace('+', '-').replace('/', '_').replace(/=+$/, ''); + }; + + const header = { + alg: 'none', + typ: 'JWT', + }; + + const encodedHeader = base64UrlEncode(JSON.stringify(header)); + const encodedPayload = base64UrlEncode(JSON.stringify(payload)); + return `${encodedHeader}.${encodedPayload}.`; + } +} diff --git a/migrations.json b/migrations.json index 888001e3..253d03ac 100644 --- a/migrations.json +++ b/migrations.json @@ -32,14 +32,6 @@ "package": "@nx/workspace", "name": "update-16-0-0-add-nx-packages" }, - { - "version": "16.0.0-beta.4", - "description": "Generates a plugin called 'workspace-plugin' containing your workspace generators.", - "cli": "nx", - "implementation": "./src/migrations/update-16-0-0/move-workspace-generators-to-local-plugin", - "package": "@nx/workspace", - "name": "16-0-0-move-workspace-generators-into-local-plugin" - }, { "version": "16.0.0-beta.9", "description": "Fix .babelrc presets if it contains an invalid entry for @nx/web/babel.", diff --git a/package.json b/package.json index 71ad5a41..7996d4c4 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,11 @@ "serve:spa": "nx serve spa", "postinstall": "node ./decorate-angular-cli.js", "prepare": "husky install", - "test:all": "npx nx run-many --all --target=test --parallel", - "lint:all": "npx nx run-many --all --target=lint --parallel", - "serve:all:prod": "npx nx run-many --target=serve --projects=spa,api --prod", - "serve:all": "npx nx run-many --target=serve --projects=spa,api", - "e2e": "export E2E_BASE_URL=http://localhost:4200/ && npm run serve:all:prod & (wait-on tcp:3000 && wait-on http://localhost:4200 && npx nx e2e spa-e2e)" + "test:all": "nx run-many --all --target=test --parallel", + "lint:all": "nx run-many --all --target=lint --parallel", + "serve:all:prod": "nx run-many --target=serve --projects=spa,api --parallel --prod", + "serve:all": "nx run-many --target=serve --projects=spa,api --parallel", + "e2e": "nx e2e spa-e2e" }, "private": true, "devDependencies": {