Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into feat/fms
Browse files Browse the repository at this point in the history
  • Loading branch information
JSPRH committed Feb 16, 2024
2 parents 92d68ae + 3a284b6 commit d1c0282
Show file tree
Hide file tree
Showing 120 changed files with 3,796 additions and 213 deletions.
8 changes: 7 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,13 @@
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off"
"@typescript-eslint/no-non-null-assertion": "off",
"jest/expect-expect": [
"error",
{
"assertFunctionNames": ["expect", "expectIterableNotToHaveNext"]
}
]
}
}
]
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
needs: build-and-publish-images
steps:
- name: Trigger Next Deployment
uses: peter-evans/repository-dispatch@v4
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.INFRA_PAT }}
repository: kordis-leitstelle/infrastructure
Expand Down
7 changes: 5 additions & 2 deletions apps/api/.env.template
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
MONGODB_URI=$MONGODB_URI
ENVIRONMENT_NAME=$ENVIRONMENT_NAME
SENTRY_KEY=$SENTRY_KEY# Prod
AUTH_PROVIDER=$AUTH_PROVIDER# Prod, defaults to dev
AADB2C_TENANT_ID=$AADB2C_TENANT_ID# Prod
AADB2C_EXTENSION_APP_ID=$AADB2C_EXTENSION_APP_ID# Prod
AADB2C_CLIENT_SECRET=$AADB2C_CLIENT_SECRET# Prod
AADB2C_TENANT_NAME=$AADB2C_TENANT_NAME# Prod
AADB2C_SIGN_IN_POLICY=$AADB2C_SIGN_IN_POLICY# Prod
AADB2C_CLIENT_ID=$AADB2C_CLIENT_ID# Prod
AADB2C_ISSUER=$AADB2C_ISSUER# Prod
MONGODB_ENCR_KMS_PROVIDER=$MONGODB_ENCR_KMS_PROVIDER# Optional, defaults to "local"
MONGODB_ENCR_KMS_PROVIDER=$MONGODB_ENCR_KMS_PROVIDER# Prod, defaults to "local"
MONGODB_ENCR_KV_NAMESPACE=$MONGODB_ENCR_KV_NAMESPACE# Prod
MONGODB_ENCR_KMS_PROVIDER_CREDS=$MONGODB_ENCR_KMS_PROVIDER_CREDS# Prod
MONGODB_ENCR_MASTER_KEY=$MONGODB_ENCR_MASTER_KEY# Prod

16 changes: 10 additions & 6 deletions apps/api/dev-tokens.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
### 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.
application of our AAD. In dev environments, the users are preregistered in the
in-memory store of the
[DevUserService](../../libs/api/users/src/lib/infra/service/dev-user.service.ts).

<!-- IF YOU CHANGE OR ADD ANY USER, MAKE SURE TO KEEP apps/spa-e2e/src/test-users.ts IN SYNC! -->

| **Username** | **ID** (`oid`) | **First name** (`first_name`) | **Last name** (`last_name`) | **Emails** (`emails`) | **Organization** (`organization`) | Token |
| ------------ | ------------------------------------ | ----------------------------- | --------------------------- | ----------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| testuser | c0cc4404-7907-4480-86d3-ba4bfc513c6d | Test | User | [email protected] | testorganization (dff7584efe2c174eee8bae45) | `eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJvaWQiOiIxMjM0IiwiZW1haWxzIjpbInRlc3R1c2VyQHRlc3QuY29tIl0sImdpdmVuX25hbWUiOiJUZXN0IiwiZmFtaWx5X25hbWUiOiJVc2VyIDEifQ.` |
| **Username** | **ID** (`oid`) | **Organization ID** (`extension_OrganizationId`) | **First name** (`first_name`) | **Last name** (`last_name`) | **Emails** (`emails`) | **Role** (`extension_Role`) | Token |
| ------------ | ------------------------------------ | ------------------------------------------------ | ----------------------------- | --------------------------- | --------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| testuser | fbdd030c-bae9-4213-9e04-732b1cc8f5b8 | dff7584efe2c174eee8bae45 | Test | User | [email protected] | User | `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvaWQiOiJmYmRkMDMwYy1iYWU5LTQyMTMtOWUwNC03MzJiMWNjOGY1YjgiLCJnaXZlbl9uYW1lIjoiVGVzdCIsImZhbWlseV9uYW1lIjoiVXNlciIsImV4dGVuc2lvbl9Pcmdhbml6YXRpb25JZCI6ImRmZjc1ODRlZmUyYzE3NGVlZThiYWU0NSIsImV4dGVuc2lvbl9Sb2xlIjoidXNlciIsImVtYWlscyI6WyJ0ZXN0dXNlckBrb3JkaXMtbGVpdHN0ZWxsZS5kZSJdfQ.o94xdOASK5h-sg8BpS82YBOYmgGFS0oUaO0txhPmzXY` |
| testadmin | f60157a8-3054-48e1-937c-82302526c1ed | dff7584efe2c174eee8bae45 | Test | Admin | [email protected] | Admin | `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvaWQiOiJmNjAxNTdhOC0zMDU0LTQ4ZTEtOTM3Yy04MjMwMjUyNmMxZWQiLCJnaXZlbl9uYW1lIjoiVGVzdCIsImZhbWlseV9uYW1lIjoiQWRtaW4iLCJleHRlbnNpb25fT3JnYW5pemF0aW9uSWQiOiJkZmY3NTg0ZWZlMmMxNzRlZWU4YmFlNDUiLCJleHRlbnNpb25fUm9sZSI6ImFkbWluIiwiZW1haWxzIjpbInRlc3RhZG1pbkBrb3JkaXMtbGVpdHN0ZWxsZS5kZSJdfQ._YRyk7zWpXtrTNdRkwydlESkPta8UJYnpa_N7TLM3hw` |
| testorgadmin | d8e3e5f2-fb05-4c47-b869-5a558e1f57e5 | dff7584efe2c174eee8bae45 | Test | Org Admin | [email protected] | Organization Admin | `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvaWQiOiJkOGUzZTVmMi1mYjA1LTRjNDctYjg2OS01YTU1OGUxZjU3ZTUiLCJnaXZlbl9uYW1lIjoiVGVzdCIsImZhbWlseV9uYW1lIjoiT3JnIEFkbWluIiwiZXh0ZW5zaW9uX09yZ2FuaXphdGlvbklkIjoiZGZmNzU4NGVmZTJjMTc0ZWVlOGJhZTQ1IiwiZXh0ZW5zaW9uX1JvbGUiOiJvcmdhbml6YXRpb25fYWRtaW4iLCJlbWFpbHMiOlsidGVzdG9yZ2FkbWluQGtvcmRpcy1sZWl0c3RlbGxlLmRlIl19.-AKJ8UJltumkThrJ3tU76ciyHfKOp3uMrNvO_HvgH3M` |

The claims will be mapped to the
[AuthUser](../../libs/shared/model/src/lib/auth-user.model.ts) Model in the
Expand Down
16 changes: 8 additions & 8 deletions apps/api/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import { ObservabilityModule } from '@kordis/api/observability';
import { OrganizationModule } from '@kordis/api/organization';
import {
MongoEncryptionClientProvider,
SharedKernel,
errorFormatterFactory,
getMongoEncrKmsFromConfig,
} from '@kordis/api/shared';
import { TetraModule } from '@kordis/api/tetra';
import { UsersModule } from '@kordis/api/user';

import { AppResolver } from './app.resolver';
import { AppService } from './app.service';
Expand All @@ -28,10 +28,13 @@ const isNextOrProdEnv = ['next', 'prod'].includes(
process.env.ENVIRONMENT_NAME ?? '',
);

const FEATURE_MODULES = [OrganizationModule, TetraModule];
const FEATURE_MODULES = [
OrganizationModule,
TetraModule,
UsersModule.forRoot(process.env.AUTH_PROVIDER === 'dev' ? 'dev' : 'aadb2c'),
];
const UTILITY_MODULES = [
SharedKernel,
AuthModule.forRoot(isNextOrProdEnv ? 'aadb2c' : 'dev'),
AuthModule.forRoot(process.env.AUTH_PROVIDER === 'aadb2c' ? 'aadb2c' : 'dev'),
ObservabilityModule.forRoot(isNextOrProdEnv ? 'sentry' : 'dev'),
];

Expand All @@ -48,9 +51,6 @@ const UTILITY_MODULES = [
driver: ApolloDriver,
useFactory: (config: ConfigService) => ({
autoSchemaFile: true,
subscriptions: {
'graphql-ws': true,
},
playground: config.get('NODE_ENV') !== 'production',
formatError: errorFormatterFactory(
config.get('NODE_ENV') === 'production',
Expand All @@ -59,7 +59,7 @@ const UTILITY_MODULES = [
inject: [ConfigService],
}),
MongooseModule.forRootAsync({
imports: [ConfigModule, SharedKernel],
imports: [ConfigModule],
useFactory: async (
config: ConfigService,
encrManager: MongoEncryptionClientProvider,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ export class GraphqlSubscriptionsController implements OnModuleInit {
constructor(private readonly graphQLSchemaHost: GraphQLSchemaHost) {}

onModuleInit(): void {
const schema = this.graphQLSchemaHost.schema;

this.handler = createHandler({
schema,
schema: this.graphQLSchemaHost.schema,
context: (req) => ({
// pass express request, since the request will be used in the nestjs pipeline
req: req.raw,
}),
});
}

Expand Down
8 changes: 4 additions & 4 deletions apps/api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@ async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule, { cors: true, bufferLogs: true });
app.useLogger(app.get(Logger));
app.useGlobalPipes(new ValidationPipe({
exceptionFactory: (errors) => PresentableValidationException.fromClassValidationErrors(errors),
})
);
exceptionFactory: (errors) => PresentableValidationException.fromClassValidationErrors(errors)
}
));
const config = app.get(ConfigService);
const envPort = config.get('PORT');

const port = envPort ? +envPort : 3000;
await app.listen(port);

Logger.log(`🚀 Application is running on: http://localhost:${port}}`);
Logger.log(`🚀 Kordis is running on: http://localhost:${port}}`);
}

bootstrap();
10 changes: 6 additions & 4 deletions apps/spa-e2e/src/auth.setup.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { test as setup } from '@playwright/test';

import { TEST_USERS } from '@kordis/shared/test-helpers';

import { LoginPo } from './page-objects/login.po';
import { TestUsernames, getAuthStoragePath, testUsernames } from './test-users';
import { TestUsernames, getAuthStoragePath } from './test-users';

// Documentation: https://playwright.dev/docs/auth#multiple-signed-in-roles

Expand All @@ -25,13 +27,13 @@ setup('authenticate as testusers', async ({ browser }) => {
await context.close();
}
} else {
for (const username of testUsernames) {
for (const user of TEST_USERS) {
const context = await browser.newContext();
const page = await context.newPage();
await new LoginPo(page).loginViaDevAuth(username);
await new LoginPo(page).loginViaDevAuth(user.userName);
await page.waitForURL('/protected');

await context.storageState({ path: getAuthStoragePath(username) });
await context.storageState({ path: getAuthStoragePath(user.userName) });
await context.close();
}
}
Expand Down
3 changes: 3 additions & 0 deletions apps/spa-e2e/src/helper.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/*
* Returns a css selector for our test id attribute.
*/
export function testIdSelector(testId: string): string {
return `[data-testid="${testId}"]`;
}
7 changes: 0 additions & 7 deletions apps/spa-e2e/src/smoketest.spec.ts

This file was deleted.

11 changes: 9 additions & 2 deletions apps/spa-e2e/src/test-users.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { test } from '@playwright/test';

export const testUsernames = ['testuser'] as const;
export type TestUsernames = (typeof testUsernames)[number];
import { TEST_USERS } from '@kordis/shared/test-helpers';

export type TestUsernames = (typeof TEST_USERS)[number]['userName'];

export function getAuthStoragePath(username: TestUsernames): string {
return `playwright/.auth/${username}.json`;
}

/*
* Use this function to set the session to a given test user. Each testuser has different claims. You should carefully choose the testuser for your test, to avoid weird states in parallel running tests.
* @see libs/shared/test-helpers/src/lib/test-users.test-helper.ts
* @see apps/api/dev-tokens.md
* @param username The username of the testuser
* */
export function asUser(username: TestUsernames): void {
test.use({ storageState: getAuthStoragePath(username) });
}
8 changes: 7 additions & 1 deletion apps/spa/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { HttpClientModule } from '@angular/common/http';
import de from '@angular/common/locales/de';
import { APP_INITIALIZER, NgModule, isDevMode } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule } from '@angular/router';
import { ServiceWorkerModule } from '@angular/service-worker';
import DOMPurify from 'dompurify';
import { NZ_I18N, de_DE } from 'ng-zorro-antd/i18n';

import { AuthModule, DevAuthModule } from '@kordis/spa/auth';
import { GraphqlModule } from '@kordis/spa/graphql';
import {
NoopObservabilityModule,
SentryObservabilityModule,
Expand All @@ -25,6 +27,7 @@ registerLocaleData(de);
declarations: [AppComponent, ProtectedComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
RouterModule.forRoot(routes),
HttpClientModule,
environment.oauth
Expand All @@ -33,7 +36,10 @@ registerLocaleData(de);
environment.oauth.discoveryDocumentUrl,
)
: DevAuthModule.forRoot(),
// for now, we accept that we have the sentry module and dependencies in our dev bundle as well
GraphqlModule.forRoot(
environment.apiUrl + '/graphql',
environment.apiUrl + '/graphql-stream',
),
environment.sentryKey
? SentryObservabilityModule.forRoot(
environment.sentryKey,
Expand Down
73 changes: 73 additions & 0 deletions apps/spa/src/app/component/app.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { createMock } from '@golevelup/ts-jest';
import { SpectatorRouting, createRoutingFactory } from '@ngneat/spectator/jest';
import { ReplaySubject, of } from 'rxjs';

import { AUTH_SERVICE, DevAuthService } from '@kordis/spa/auth';
import { GraphqlService } from '@kordis/spa/graphql';

import { AppComponent } from './app.component';

// https://github.com/getsentry/sentry-javascript/issues/9448
jest.mock('@sentry/angular-ivy', () => ({
TraceClassDecorator: () => () => {},
}));

describe('AppComponent', () => {
let spectator: SpectatorRouting<AppComponent>;
const gqlService = createMock<GraphqlService>();
const isAuthenticatedStatusSubject$ = new ReplaySubject<boolean>(1);

const createComponent = createRoutingFactory({
component: AppComponent,
providers: [
{
provide: AUTH_SERVICE,
useValue: createMock<DevAuthService>({
isAuthenticated$: isAuthenticatedStatusSubject$.asObservable(),
}),
},
{
provide: GraphqlService,
useValue: gqlService,
},
],
});

afterEach(() => {
jest.clearAllMocks();
});

it('should logout user on deactivated subscription event', () => {
gqlService.subscribe$.mockImplementationOnce(() =>
of({
data: {
currentUserDeactivated: {
userId: 'test',
},
},
}),
);
isAuthenticatedStatusSubject$.next(true);
spectator = createComponent();
const authService = spectator.inject(AUTH_SERVICE, true);
const logoutSpy = jest.spyOn(authService, 'logout');
expect(logoutSpy).toHaveBeenCalled();
});

it('should not logout on deactivated subscription event while not authenticated', () => {
gqlService.subscribe$.mockImplementationOnce(() =>
of({
data: {
currentUserDeactivated: {
userId: 'test',
},
},
}),
);
isAuthenticatedStatusSubject$.next(false);
spectator = createComponent();
const authService = spectator.inject(AUTH_SERVICE, true);
const logoutSpy = jest.spyOn(authService, 'logout');
expect(logoutSpy).not.toHaveBeenCalled();
});
});
35 changes: 33 additions & 2 deletions apps/spa/src/app/component/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NEVER, switchMap } from 'rxjs';

import { AUTH_SERVICE } from '@kordis/spa/auth';
import { GraphqlService, gql } from '@kordis/spa/graphql';
import { TraceComponent } from '@kordis/spa/observability';

@Component({
Expand All @@ -8,4 +12,31 @@ import { TraceComponent } from '@kordis/spa/observability';
changeDetection: ChangeDetectionStrategy.OnPush,
})
@TraceComponent()
export class AppComponent {}
export class AppComponent {
private readonly authService = inject(AUTH_SERVICE);
private readonly gqlService = inject(GraphqlService);

constructor() {
this.logoutOnUserDeactivated();
}

logoutOnUserDeactivated(): void {
this.authService.isAuthenticated$
.pipe(
switchMap((isAuthenticated) => {
if (!isAuthenticated) {
return NEVER;
}
return this.gqlService.subscribe$(gql`
subscription {
currentUserDeactivated {
userId
}
}
`);
}),
takeUntilDestroyed(),
)
.subscribe(() => this.authService.logout());
}
}
9 changes: 4 additions & 5 deletions apps/spa/src/app/update.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ describe('UpdateService', () => {
],
});
TestBed.inject(UpdateService);
Object.defineProperty(window, 'location', {
value: { reload: jest.fn(), hash: {} },
});
windowSpy.mockReturnValue(true);
});

afterEach(() => {
Expand All @@ -34,7 +38,6 @@ describe('UpdateService', () => {
it('should ask for update permission when a new version is ready', () =>
new Promise<void>((done) => {
swUpdateMock.versionUpdates.subscribe(() => {
console.log('hi');
expect(windowSpy).toHaveBeenCalled();
done();
});
Expand All @@ -44,10 +47,6 @@ describe('UpdateService', () => {
}));

it('should reload page if permission given', () => {
Object.defineProperty(window, 'location', {
value: { reload: jest.fn(), hash: {} },
});
windowSpy.mockReturnValue(true);
const reloadSpy = jest.spyOn(window.location, 'reload');

versionUpdateSubject$.next({
Expand Down
Loading

0 comments on commit d1c0282

Please sign in to comment.