Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Login with Keycloak UI #1990

Merged
merged 81 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from 61 commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
9e5c711
added keycloak angular
TheSlimvReal Aug 28, 2023
77bd2a8
using keycloak login flow
TheSlimvReal Aug 28, 2023
3de94c5
cleaned up login component
TheSlimvReal Aug 28, 2023
cd42286
added local login
TheSlimvReal Aug 30, 2023
668b1c5
moved login state to own subject
TheSlimvReal Aug 30, 2023
d4a0da6
moved getCurrentUser function to own service
TheSlimvReal Aug 30, 2023
63953e9
moved sync to remote session
TheSlimvReal Aug 30, 2023
e6a8471
removed further usages of session service
TheSlimvReal Aug 31, 2023
d4f4e1b
removed further usages of session service
TheSlimvReal Aug 31, 2023
f5263cc
moved session check to module constructor
TheSlimvReal Aug 31, 2023
6202cd9
properly updating token in pouchdb requests
TheSlimvReal Aug 31, 2023
9f05096
properly updating token in pouchdb requests
TheSlimvReal Aug 31, 2023
4be06d8
only showing offline login when available
TheSlimvReal Aug 31, 2023
9918859
removed session service interface
TheSlimvReal Sep 5, 2023
1cea779
removed unneeded functions
TheSlimvReal Sep 5, 2023
0b8d09f
moved DB sync to own service
TheSlimvReal Sep 5, 2023
765ff6a
moved session management to synced session service
TheSlimvReal Sep 6, 2023
3397b31
removed auth service and couchdb auth service
TheSlimvReal Sep 6, 2023
f114cb5
fixed sync
TheSlimvReal Sep 6, 2023
602d3bf
moved session check to app initializer
TheSlimvReal Sep 6, 2023
a1c0b0a
moved local auth to own service and removed unused code
TheSlimvReal Sep 6, 2023
92aa158
fixing tests for session services
TheSlimvReal Sep 6, 2023
6d9e0b7
fixed other tests that were affected
TheSlimvReal Sep 6, 2023
3b60377
Merge branch 'master' into keycloak_login
TheSlimvReal Sep 6, 2023
f21af3e
increasing test coverage
TheSlimvReal Sep 6, 2023
529f9ae
fixed tests
TheSlimvReal Sep 18, 2023
101f83e
added tests for keycloak auth service
TheSlimvReal Sep 18, 2023
4741ffc
added tests for keycloak auth service
TheSlimvReal Sep 18, 2023
b4ab0ac
Merge remote-tracking branch 'origin/keycloak_login' into keycloak_login
TheSlimvReal Sep 19, 2023
1627831
Merge branch 'master' into keycloak_login
TheSlimvReal Sep 19, 2023
43306b6
fixed demo mode
TheSlimvReal Sep 19, 2023
46952d5
not blocking UI with login check
TheSlimvReal Sep 20, 2023
8bc8aff
requiring login when online and in synced session
TheSlimvReal Sep 21, 2023
e93b7d7
showing remote login progress in UI
TheSlimvReal Sep 21, 2023
991813c
only initialising keycloak once
TheSlimvReal Sep 21, 2023
fb5c46b
only initialising keycloak once
TheSlimvReal Sep 21, 2023
019bd0d
added component to select a user for offline usage
TheSlimvReal Sep 21, 2023
ab1bbbe
using subject instead of user service
TheSlimvReal Sep 21, 2023
b0cdc13
user dbs in demo mode sync
TheSlimvReal Sep 22, 2023
2e65823
merged local session code into session manager
TheSlimvReal Sep 22, 2023
161439b
Added documentation to session services
TheSlimvReal Sep 22, 2023
265fd66
trying to store remote logout information in offline case
TheSlimvReal Sep 25, 2023
44bc4aa
remote session reset flag is stored and applied on startup
TheSlimvReal Sep 26, 2023
4ad43e2
correctly disabling email field in demo mode
TheSlimvReal Sep 26, 2023
7d9f97c
improved loading screen message
TheSlimvReal Sep 27, 2023
55e708f
Merge branch 'master' into keycloak_login
TheSlimvReal Sep 27, 2023
439b9bc
Merge remote-tracking branch 'origin/keycloak_login' into keycloak_login
TheSlimvReal Sep 27, 2023
fc78083
fixed test
TheSlimvReal Sep 27, 2023
beccec3
logging in before triggering delayed remote logout
TheSlimvReal Sep 27, 2023
8045fab
added login button that allows manual login check
TheSlimvReal Sep 27, 2023
1eb1a8e
fixed storybook
TheSlimvReal Sep 28, 2023
e5c60ba
Merge branch 'master' into keycloak_login
sleidig Oct 5, 2023
b4b63b3
ui tweaks for login screen
sleidig Oct 6, 2023
a76f14a
renamed user subject
TheSlimvReal Oct 16, 2023
f7b3551
extracted common logic
TheSlimvReal Oct 16, 2023
eaf21d3
Merge remote-tracking branch 'origin/keycloak_login' into keycloak_login
TheSlimvReal Oct 16, 2023
ab96d7d
showing offline login after delay
TheSlimvReal Oct 16, 2023
5602df3
session manager prevents remote login in offline cases
TheSlimvReal Oct 18, 2023
e1dad0c
added doc
TheSlimvReal Oct 18, 2023
2d0f1c3
Merge remote-tracking branch 'origin/master' into keycloak_login
TheSlimvReal Oct 19, 2023
637720a
fixed test
TheSlimvReal Oct 19, 2023
82e824d
alternative UX: disable instead of hide offline login
sleidig Oct 23, 2023
de535bd
Merge branch 'master' into keycloak_login
TheSlimvReal Nov 7, 2023
9f3a133
prettier fixes
TheSlimvReal Nov 7, 2023
1ade805
fixed tests
TheSlimvReal Nov 7, 2023
12d29f9
improved test setup
TheSlimvReal Nov 7, 2023
0eef25e
Merge branch 'master' into keycloak_login
TheSlimvReal Nov 7, 2023
60f86ad
loading user profile through `/userinfo` endpoint
TheSlimvReal Nov 8, 2023
e27a81b
Merge branch 'master' into keycloak_login
TheSlimvReal Nov 8, 2023
2b86652
fixed test
TheSlimvReal Nov 8, 2023
6376087
set email does not trigger page reload
TheSlimvReal Nov 8, 2023
4f13f2a
offline login is also available if navigator is offline
TheSlimvReal Nov 8, 2023
834e04d
fix: emails are sent in selected language
TheSlimvReal Nov 13, 2023
bbeda35
Merge branch 'master' into keycloak_login
TheSlimvReal Nov 13, 2023
33f8b2b
fixed test
TheSlimvReal Nov 13, 2023
49d9879
i18n: added translations
TheSlimvReal Nov 13, 2023
343a9f3
adding accept-headers header to all requests
TheSlimvReal Nov 14, 2023
d1c793f
Merge branch 'master' into keycloak_login
TheSlimvReal Nov 22, 2023
83a7ba9
Merge branch 'master' into keycloak_login
TheSlimvReal Nov 22, 2023
92dfaf4
Merge branch 'master' into keycloak_login
TheSlimvReal Nov 28, 2023
0265341
Merge branch 'master' into keycloak_login
TheSlimvReal Nov 29, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
363 changes: 153 additions & 210 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"flag-icons": "^6.10.0",
"hammerjs": "^2.0.8",
"json-query": "^2.2.2",
"keycloak-angular": "^14.1.0",
"keycloak-js": "^22.0.1",
"leaflet": "^1.9.4",
"lodash-es": "^4.17.21",
Expand Down
13 changes: 8 additions & 5 deletions src/app/app-initializers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import { ConfigService } from "./core/config/config.service";
import { RouterService } from "./core/config/dynamic-routing/router.service";
import { EntityConfigService } from "./core/entity/entity-config.service";
import { Router } from "@angular/router";
import { SessionService } from "./core/session/session-service/session.service";
import { AnalyticsService } from "./core/analytics/analytics.service";
import { LoginState } from "./core/session/session-states/login-state.enum";
import { LoggingService } from "./core/logging/logging.service";
import { environment } from "../environments/environment";
import { LoginStateSubject } from "./core/session/session-type";
import { CurrentUserSubject } from "./core/user/user";

export const appInitializers = {
provide: APP_INITIALIZER,
Expand All @@ -22,8 +23,9 @@ export const appInitializers = {
routerService: RouterService,
entityConfigService: EntityConfigService,
router: Router,
sessionService: SessionService,
currentUser: CurrentUserSubject,
analyticsService: AnalyticsService,
loginState: LoginStateSubject,
) =>
async () => {
// Re-trigger services that depend on the config when something changes
Expand All @@ -35,9 +37,9 @@ export const appInitializers = {
});

// update the user context for remote error logging and tracking and load config initially
sessionService.loginState.subscribe((newState) => {
loginState.subscribe((newState) => {
if (newState === LoginState.LOGGED_IN) {
const username = sessionService.getCurrentUser().name;
const username = currentUser.value.name;
LoggingService.setLoggingContextUser(username);
analyticsService.setUser(username);
} else {
Expand All @@ -62,8 +64,9 @@ export const appInitializers = {
RouterService,
EntityConfigService,
Router,
SessionService,
CurrentUserSubject,
AnalyticsService,
LoginStateSubject,
],
multi: true,
};
15 changes: 10 additions & 5 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ import {
entityRegistry,
EntityRegistry,
} from "./core/entity/database-entity.decorator";
import { LOCATION_TOKEN, WINDOW_TOKEN } from "./utils/di-tokens";
import {
LOCATION_TOKEN,
NAVIGATOR_TOKEN,
WINDOW_TOKEN,
} from "./utils/di-tokens";
import { AttendanceModule } from "./child-dev-project/attendance/attendance.module";
import { NotesModule } from "./child-dev-project/notes/notes.module";
import { SchoolsModule } from "./child-dev-project/schools/schools.module";
Expand All @@ -76,7 +80,6 @@ import { RouterModule } from "@angular/router";
import { TodosModule } from "./features/todos/todos.module";
import moment from "moment";
import { getLocaleFirstDayOfWeek } from "@angular/common";
import { SessionService } from "./core/session/session-service/session.service";
import { waitForChangeTo } from "./core/session/session-states/session-utils";
import { LoginState } from "./core/session/session-states/login-state.enum";
import { appInitializers } from "./app-initializers";
Expand All @@ -87,6 +90,7 @@ import { BirthdayDashboardWidgetModule } from "./features/dashboard-widgets/birt
import { ConfigSetupModule } from "./features/config-setup/config-setup.module";
import { MarkdownPageModule } from "./features/markdown-page/markdown-page.module";
import { AdminModule } from "./features/admin/admin.module";
import { LoginStateSubject } from "./core/session/session-type";

/**
* Main entry point of the application.
Expand Down Expand Up @@ -147,6 +151,7 @@ import { AdminModule } from "./features/admin/admin.module";
{ provide: EntityRegistry, useValue: entityRegistry },
{ provide: WINDOW_TOKEN, useValue: window },
{ provide: LOCATION_TOKEN, useValue: window.location },
{ provide: NAVIGATOR_TOKEN, useValue: navigator },
{
provide: LOCALE_ID,
useValue:
Expand All @@ -161,12 +166,12 @@ import { AdminModule } from "./features/admin/admin.module";
},
{
provide: SwRegistrationOptions,
useFactory: (session: SessionService) => ({
useFactory: (loginState: LoginStateSubject) => ({
enabled: environment.production,
registrationStrategy: () =>
session.loginState.pipe(waitForChangeTo(LoginState.LOGGED_IN)),
loginState.pipe(waitForChangeTo(LoginState.LOGGED_IN)),
}),
deps: [SessionService],
deps: [LoginStateSubject],
},
appInitializers,
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { AttendanceService } from "../../attendance.service";
import { Note } from "../../../notes/model/note";
import { EntityMapperService } from "../../../../core/entity/entity-mapper/entity-mapper.service";
import { RecurringActivity } from "../../model/recurring-activity";
import { SessionService } from "../../../../core/session/session-service/session.service";
import { NoteDetailsComponent } from "../../../notes/note-details/note-details.component";
import { FormDialogService } from "../../../../core/form-dialog/form-dialog.service";
import { AlertService } from "../../../../core/alerts/alert.service";
Expand All @@ -27,6 +26,7 @@ import { NgForOf, NgIf } from "@angular/common";
import { MatProgressBarModule } from "@angular/material/progress-bar";
import { ActivityCardComponent } from "../../activity-card/activity-card.component";
import { MatButtonModule } from "@angular/material/button";
import { CurrentUserSubject } from "../../../../core/user/user";

@Component({
selector: "app-roll-call-setup",
Expand Down Expand Up @@ -76,7 +76,7 @@ export class RollCallSetupComponent implements OnInit {
constructor(
private entityMapper: EntityMapperService,
private attendanceService: AttendanceService,
private sessionService: SessionService,
private currentUser: CurrentUserSubject,
private formDialog: FormDialogService,
private alertService: AlertService,
private filerService: FilterService,
Expand Down Expand Up @@ -105,7 +105,7 @@ export class RollCallSetupComponent implements OnInit {
this.visibleActivities = this.allActivities;
} else {
this.visibleActivities = this.allActivities.filter((a) =>
a.isAssignedTo(this.sessionService.getCurrentUser().name),
a.isAssignedTo(this.currentUser.value.name),
);
if (this.visibleActivities.length === 0) {
this.visibleActivities = this.allActivities.filter(
Expand Down Expand Up @@ -155,7 +155,7 @@ export class RollCallSetupComponent implements OnInit {
activity,
this.date,
)) as NoteForActivitySetup;
event.authors = [this.sessionService.getCurrentUser().name];
event.authors = [this.currentUser.value.name];
event.isNewFromActivity = true;
return event;
}
Expand All @@ -175,7 +175,7 @@ export class RollCallSetupComponent implements OnInit {
score += 1;
}

if (assignedUsers.includes(this.sessionService.getCurrentUser().name)) {
if (assignedUsers.includes(this.currentUser.value.name)) {
score += 2;
}

Expand All @@ -189,7 +189,7 @@ export class RollCallSetupComponent implements OnInit {

createOneTimeEvent() {
const newNote = Note.create(new Date());
newNote.authors = [this.sessionService.getCurrentUser().name];
newNote.authors = [this.currentUser.value.name];

this.formDialog
.openFormPopup(newNote, [], NoteDetailsComponent)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { NoteDetailsComponent } from "../note-details/note-details.component";
import { ActivatedRoute } from "@angular/router";
import { EntityMapperService } from "../../../core/entity/entity-mapper/entity-mapper.service";
import { FilterSelectionOption } from "../../../core/filter/filters/filters";
import { SessionService } from "../../../core/session/session-service/session.service";
import { FormDialogService } from "../../../core/form-dialog/form-dialog.service";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { LoggingService } from "../../../core/logging/logging.service";
Expand Down Expand Up @@ -92,7 +91,6 @@ export class NotesManagerComponent implements OnInit {

constructor(
private formDialog: FormDialogService,
private sessionService: SessionService,
private entityMapperService: EntityMapperService,
private route: ActivatedRoute,
private log: LoggingService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import { UnsavedChangesService } from "../../entity-details/form/unsaved-changes
import { ActivationStart, Router } from "@angular/router";
import { Subscription } from "rxjs";
import { filter } from "rxjs/operators";
import { SessionService } from "../../session/session-service/session.service";
import {
EntitySchemaField,
PLACEHOLDERS,
} from "../../entity/schema/entity-schema-field";
import { isArrayDataType } from "../../basic-datatypes/datatype-utils";
import { CurrentUserSubject } from "../../user/user";

/**
* These are utility types that allow to define the type of `FormGroup` the way it is returned by `EntityFormService.create`
Expand All @@ -40,7 +40,7 @@ export class EntityFormService {
private dynamicValidator: DynamicValidatorsService,
private ability: EntityAbility,
private unsavedChanges: UnsavedChangesService,
private session: SessionService,
private currentUser: CurrentUserSubject,
router: Router,
) {
router.events
Expand Down Expand Up @@ -154,7 +154,7 @@ export class EntityFormService {
newVal = new Date();
break;
case PLACEHOLDERS.CURRENT_USER:
newVal = this.session.getCurrentUser().name;
newVal = this.currentUser.value.name;
break;
default:
newVal = schema.defaultValue;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import {
Component,
ViewChild,
Input,
OnChanges,
SimpleChanges,
OnInit,
SimpleChanges,
ViewChild,
} from "@angular/core";
import {
MatPaginator,
MatPaginatorModule,
PageEvent,
} from "@angular/material/paginator";
import { MatTableDataSource } from "@angular/material/table";
import { User } from "../../../user/user";
import { SessionService } from "../../../session/session-service/session.service";
import { CurrentUserSubject, User } from "../../../user/user";
import { EntityMapperService } from "../../../entity/entity-mapper/entity-mapper.service";

@Component({
Expand All @@ -35,7 +34,7 @@ export class ListPaginatorComponent<E> implements OnChanges, OnInit {
pageSize = 10;

constructor(
private sessionService: SessionService,
private currentUser: CurrentUserSubject,
private entityMapperService: EntityMapperService,
) {}

Expand Down Expand Up @@ -83,7 +82,7 @@ export class ListPaginatorComponent<E> implements OnChanges, OnInit {

private async ensureUserIsLoaded(): Promise<boolean> {
if (!this.user) {
const currentUser = this.sessionService.getCurrentUser();
const currentUser = this.currentUser.value;
this.user = await this.entityMapperService
.load(User, currentUser.name)
.catch(() => undefined);
Expand Down
3 changes: 2 additions & 1 deletion src/app/core/core.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NgModule } from "@angular/core";
import { ComponentRegistry } from "../dynamic-components";
import { coreComponents } from "./core-components";
import { User } from "./user/user";
import { CurrentUserSubject, User } from "./user/user";
import { Config } from "./config/config";
import { StringDatatype } from "./basic-datatypes/string/string.datatype";
import { DefaultDatatype } from "./entity/default-datatype/default.datatype";
Expand All @@ -25,6 +25,7 @@ import { CommonModule } from "@angular/common";
*/
@NgModule({
providers: [
CurrentUserSubject,
// base dataTypes
{ provide: DefaultDatatype, useClass: StringDatatype, multi: true },
{ provide: DefaultDatatype, useClass: BooleanDatatype, multi: true },
Expand Down
116 changes: 116 additions & 0 deletions src/app/core/database/sync.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { fakeAsync, TestBed, tick } from "@angular/core/testing";

import { SyncService } from "./sync.service";
import { PouchDatabase } from "./pouch-database";
import { Database } from "./database";
import { LoginStateSubject, SyncStateSubject } from "../session/session-type";
import { LoginState } from "../session/session-states/login-state.enum";
import { KeycloakAuthService } from "../session/auth/keycloak/keycloak-auth.service";
import { HttpStatusCode } from "@angular/common/http";
import PouchDB from "pouchdb-browser";

describe("SyncService", () => {
let service: SyncService;
let loginState: LoginStateSubject;
let mockAuthService: jasmine.SpyObj<KeycloakAuthService>;

beforeEach(() => {
mockAuthService = jasmine.createSpyObj(["login", "addAuthHeader"]);
TestBed.configureTestingModule({
providers: [
{ provide: KeycloakAuthService, useValue: mockAuthService },
{ provide: Database, useClass: PouchDatabase },
LoginStateSubject,
SyncStateSubject,
],
});
service = TestBed.inject(SyncService);
loginState = TestBed.inject(LoginStateSubject);
});

it("should be created", () => {
expect(service).toBeTruthy();
});

it("should restart the sync if it fails at one point", fakeAsync(() => {
let errorCallback, pauseCallback;
const syncHandle = {
on: (action, callback) => {
if (action === "error") {
errorCallback = callback;
}
if (action === "paused") {
pauseCallback = callback;
}
return syncHandle;
},
cancel: () => undefined,
};
const syncSpy = jasmine
.createSpy()
.and.returnValues(Promise.resolve("first"), syncHandle, syncHandle);
spyOn(
TestBed.inject(Database) as PouchDatabase,
"getPouchDB",
).and.returnValue({ sync: syncSpy } as any);

service.startSync();
tick(1000);

// error + logged in -> sync should restart
loginState.next(LoginState.LOGGED_IN);
syncSpy.calls.reset();
errorCallback();
expect(syncSpy).toHaveBeenCalled();

// pause -> no restart required
syncSpy.calls.reset();
pauseCallback();
expect(syncSpy).not.toHaveBeenCalled();

// logout + error -> no restart
syncSpy.calls.reset();
loginState.next(LoginState.LOGGED_OUT);
tick();
errorCallback();
expect(syncSpy).not.toHaveBeenCalled();
}));

it("should try auto-login if fetch fails and fetch again", async () => {
// Make sync call pass
spyOn(
TestBed.inject(Database) as PouchDatabase,
"getPouchDB",
).and.returnValues({ sync: () => Promise.resolve() } as any);
spyOn(PouchDB, "fetch").and.returnValues(
Promise.resolve({
status: HttpStatusCode.Unauthorized,
ok: false,
} as Response),
Promise.resolve({ status: HttpStatusCode.Ok, ok: true } as Response),
);
// providing "valid" token on second call
let calls = 0;
mockAuthService.addAuthHeader.and.callFake((headers) => {
headers.Authorization = calls++ === 1 ? "valid" : "invalid";
});
mockAuthService.login.and.resolveTo();
const initSpy = spyOn(service["remoteDatabase"], "initRemoteDB");
await service.startSync();
// taking fetch function from init call
const fetch = initSpy.calls.mostRecent().args[1];

const url = "/db/_changes";
const opts = { headers: {} };
await expectAsync(fetch(url, opts)).toBeResolved();

expect(PouchDB.fetch).toHaveBeenCalledTimes(2);
expect(PouchDB.fetch).toHaveBeenCalledWith(url, opts);
expect(opts.headers).toEqual({ Authorization: "valid" });
expect(mockAuthService.login).toHaveBeenCalled();
expect(mockAuthService.addAuthHeader).toHaveBeenCalledTimes(2);

// prevent live sync call
service["cancelLiveSync"]();
});
});
Loading
Loading