diff --git a/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.ts b/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.ts
index 6c654af05c..587bb96f18 100644
--- a/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.ts
+++ b/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.ts
@@ -106,7 +106,7 @@ export class RollCallSetupComponent implements OnInit {
} else {
// TODO implement a generic function that finds the property where a entity has relations to another entity type (e.g. `authors` for `Note` when looking for `User`) to allow dynamic checks
this.visibleActivities = this.allActivities.filter((a) =>
- a.isAssignedTo(this.currentUser.value.getId()),
+ a.isAssignedTo(this.currentUser.value?.getId()),
);
if (this.visibleActivities.length === 0) {
this.visibleActivities = this.allActivities.filter(
@@ -156,7 +156,9 @@ export class RollCallSetupComponent implements OnInit {
activity,
this.date,
)) as NoteForActivitySetup;
- event.authors = [this.currentUser.value.getId()];
+ if (this.currentUser.value) {
+ event.authors = [this.currentUser.value.getId()];
+ }
event.isNewFromActivity = true;
return event;
}
@@ -176,7 +178,7 @@ export class RollCallSetupComponent implements OnInit {
score += 1;
}
- if (assignedUsers.includes(this.currentUser.value.getId())) {
+ if (assignedUsers.includes(this.currentUser.value?.getId())) {
score += 2;
}
@@ -190,7 +192,9 @@ export class RollCallSetupComponent implements OnInit {
createOneTimeEvent() {
const newNote = Note.create(new Date());
- newNote.authors = [this.currentUser.value.getId()];
+ if (this.currentUser.value) {
+ newNote.authors = [this.currentUser.value.getId()];
+ }
this.formDialog
.openFormPopup(newNote, [], NoteDetailsComponent)
diff --git a/src/app/core/basic-datatypes/entity-array/display-entity-array/display-entity-array.component.spec.ts b/src/app/core/basic-datatypes/entity-array/display-entity-array/display-entity-array.component.spec.ts
index 2c58eb7dd4..790f63193a 100644
--- a/src/app/core/basic-datatypes/entity-array/display-entity-array/display-entity-array.component.spec.ts
+++ b/src/app/core/basic-datatypes/entity-array/display-entity-array/display-entity-array.component.spec.ts
@@ -99,4 +99,16 @@ describe("DisplayEntityArrayComponent", () => {
expect(component.entities).toEqual(expectedEntities);
});
+
+ it("should load entities of not configured type", async () => {
+ component.config = School.ENTITY_TYPE;
+ const existingChild = testEntities.find(
+ (e) => e.getType() === Child.ENTITY_TYPE,
+ );
+ component.value = [existingChild.getId()];
+
+ await component.ngOnInit();
+
+ expect(component.entities).toEqual([existingChild]);
+ });
});
diff --git a/src/app/core/basic-datatypes/entity-array/display-entity-array/display-entity-array.component.ts b/src/app/core/basic-datatypes/entity-array/display-entity-array/display-entity-array.component.ts
index a2d5589ae7..ae01e714ee 100644
--- a/src/app/core/basic-datatypes/entity-array/display-entity-array/display-entity-array.component.ts
+++ b/src/app/core/basic-datatypes/entity-array/display-entity-array/display-entity-array.component.ts
@@ -30,10 +30,9 @@ export class DisplayEntityArrayComponent
const entityIds: string[] = this.value || [];
if (entityIds.length < this.aggregationThreshold) {
const entityPromises = entityIds.map((entityId) => {
- const type =
- typeof this.config === "string"
- ? this.config
- : Entity.extractTypeFromId(entityId);
+ const type = entityId.includes(":")
+ ? Entity.extractTypeFromId(entityId)
+ : this.config;
return this.entityMapper.load(type, entityId);
});
this.entities = await Promise.all(entityPromises);
diff --git a/src/app/core/basic-datatypes/entity/display-entity/display-entity.component.spec.ts b/src/app/core/basic-datatypes/entity/display-entity/display-entity.component.spec.ts
index 52c11757fc..4f88aa80c9 100644
--- a/src/app/core/basic-datatypes/entity/display-entity/display-entity.component.spec.ts
+++ b/src/app/core/basic-datatypes/entity/display-entity/display-entity.component.spec.ts
@@ -14,21 +14,25 @@ import {
componentRegistry,
ComponentRegistry,
} from "../../../../dynamic-components";
+import {
+ mockEntityMapper,
+ MockEntityMapperService,
+} from "../../../entity/entity-mapper/mock-entity-mapper-service";
+import { LoggingService } from "../../../logging/logging.service";
describe("DisplayEntityComponent", () => {
let component: DisplayEntityComponent;
let fixture: ComponentFixture;
- let mockEntityMapper: jasmine.SpyObj;
+ let entityMapper: MockEntityMapperService;
let mockRouter: jasmine.SpyObj;
beforeEach(async () => {
- mockEntityMapper = jasmine.createSpyObj(["load"]);
- mockEntityMapper.load.and.resolveTo(new Child());
+ entityMapper = mockEntityMapper();
mockRouter = jasmine.createSpyObj(["navigate"]);
await TestBed.configureTestingModule({
imports: [DisplayEntityComponent],
providers: [
- { provide: EntityMapperService, useValue: mockEntityMapper },
+ { provide: EntityMapperService, useValue: entityMapper },
{ provide: EntityRegistry, useValue: entityRegistry },
{ provide: ComponentRegistry, useValue: componentRegistry },
{ provide: Router, useValue: mockRouter },
@@ -48,7 +52,7 @@ describe("DisplayEntityComponent", () => {
it("should use the block component when available", async () => {
const school = new School();
- mockEntityMapper.load.and.resolveTo(school);
+ entityMapper.add(school);
component.entity = new ChildSchoolRelation();
component.id = "schoolId";
@@ -57,10 +61,6 @@ describe("DisplayEntityComponent", () => {
await component.ngOnInit();
expect(component.entityBlockComponent).toEqual(School.getBlockComponent());
- expect(mockEntityMapper.load).toHaveBeenCalledWith(
- school.getType(),
- school.getId(),
- );
expect(component.entityToDisplay).toEqual(school);
});
@@ -71,4 +71,29 @@ describe("DisplayEntityComponent", () => {
expect(mockRouter.navigate).toHaveBeenCalledWith(["/child", "1"]);
});
+
+ it("should show entities which are not of the configured type", async () => {
+ const child = new Child();
+ entityMapper.add(child);
+ component.entityId = child.getId();
+ component.config = School.ENTITY_TYPE;
+
+ await component.ngOnInit();
+
+ expect(component.entityToDisplay).toEqual(child);
+ });
+
+ it("should log a warning if entity cannot be loaded", async () => {
+ const warnSpy = spyOn(TestBed.inject(LoggingService), "warn");
+ const child = new Child("not_existing");
+ component.entityId = child.getId();
+ component.config = School.ENTITY_TYPE;
+
+ await component.ngOnInit();
+
+ expect(warnSpy).toHaveBeenCalledWith(
+ jasmine.stringContaining(child.getId()),
+ );
+ expect(component.entityToDisplay).toBeUndefined();
+ });
});
diff --git a/src/app/core/basic-datatypes/entity/display-entity/display-entity.component.ts b/src/app/core/basic-datatypes/entity/display-entity/display-entity.component.ts
index 8b1cdc8ea5..13d0c839de 100644
--- a/src/app/core/basic-datatypes/entity/display-entity/display-entity.component.ts
+++ b/src/app/core/basic-datatypes/entity/display-entity/display-entity.component.ts
@@ -6,6 +6,7 @@ import { EntityMapperService } from "../../../entity/entity-mapper/entity-mapper
import { Router } from "@angular/router";
import { NgClass, NgIf } from "@angular/common";
import { DynamicComponentDirective } from "../../../config/dynamic-components/dynamic-component.directive";
+import { LoggingService } from "../../../logging/logging.service";
@DynamicComponent("DisplayEntity")
@Component({
@@ -35,21 +36,30 @@ export class DisplayEntityComponent
constructor(
private entityMapper: EntityMapperService,
private router: Router,
+ private logger: LoggingService,
) {
super();
}
async ngOnInit() {
if (!this.entityToDisplay) {
- this.entityType = this.entityType ?? this.config;
this.entityId = this.entityId ?? this.value;
+ this.entityType = this.entityId.includes(":")
+ ? Entity.extractTypeFromId(this.entityId)
+ : this.entityType ?? this.config;
if (!this.entityType || !this.entityId) {
return;
}
- this.entityToDisplay = await this.entityMapper.load(
- this.entityType,
- this.entityId,
- );
+ try {
+ this.entityToDisplay = await this.entityMapper.load(
+ this.entityType,
+ this.entityId,
+ );
+ } catch (e) {
+ this.logger.warn(
+ `[DISPLAY_ENTITY] Could not find entity with ID: ${this.entityId}: ${e}`,
+ );
+ }
}
if (this.entityToDisplay) {
this.entityBlockComponent = this.entityToDisplay
diff --git a/src/app/core/basic-datatypes/entity/edit-single-entity/edit-single-entity.component.spec.ts b/src/app/core/basic-datatypes/entity/edit-single-entity/edit-single-entity.component.spec.ts
index 13f3f4673c..2ed74707e4 100644
--- a/src/app/core/basic-datatypes/entity/edit-single-entity/edit-single-entity.component.spec.ts
+++ b/src/app/core/basic-datatypes/entity/edit-single-entity/edit-single-entity.component.spec.ts
@@ -7,18 +7,20 @@ import { ChildSchoolRelation } from "../../../../child-dev-project/children/mode
import { School } from "../../../../child-dev-project/schools/model/school";
import { MockedTestingModule } from "../../../../utils/mocked-testing.module";
import { FormControl } from "@angular/forms";
+import { Child } from "../../../../child-dev-project/children/model/child";
+import { LoggingService } from "../../../logging/logging.service";
describe("EditSingleEntityComponent", () => {
let component: EditSingleEntityComponent;
let fixture: ComponentFixture;
- let loadTypeSpy: jasmine.Spy;
+ let entityMapper: EntityMapperService;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [EditSingleEntityComponent, MockedTestingModule.withState()],
providers: [EntityFormService],
}).compileComponents();
- loadTypeSpy = spyOn(TestBed.inject(EntityMapperService), "loadType");
+ entityMapper = TestBed.inject(EntityMapperService);
}));
beforeEach(() => {
@@ -45,11 +47,39 @@ describe("EditSingleEntityComponent", () => {
it("should load all entities of the given type as options", async () => {
const school1 = School.create({ name: "First School" });
const school2 = School.create({ name: "Second School " });
- loadTypeSpy.and.resolveTo([school1, school2]);
+ await entityMapper.saveAll([school1, school2]);
+ component.formFieldConfig.additional = School.ENTITY_TYPE;
await component.ngOnInit();
- expect(loadTypeSpy).toHaveBeenCalled();
- expect(component.entities).toEqual([school1, school2]);
+ expect(component.entities).toEqual(
+ jasmine.arrayWithExactContents([school1, school2]),
+ );
+ });
+
+ it("should add selected entity of a not-configured type to available entities", async () => {
+ const someSchools = [new School(), new School()];
+ const selectedChild = new Child();
+ await entityMapper.saveAll(someSchools.concat(selectedChild));
+ component.formFieldConfig.additional = School.ENTITY_TYPE;
+ component.formControl.setValue(selectedChild.getId());
+
+ await component.ngOnInit();
+
+ expect(component.entities).toEqual(
+ jasmine.arrayWithExactContents(someSchools.concat(selectedChild)),
+ );
+ });
+
+ it("should log warning if entity is selected that cannot be found", async () => {
+ const warnSpy = spyOn(TestBed.inject(LoggingService), "warn");
+ component.formFieldConfig.additional = Child.ENTITY_TYPE;
+ component.formControl.setValue("missing_child");
+
+ await component.ngOnInit();
+
+ expect(warnSpy).toHaveBeenCalledWith(
+ jasmine.stringContaining("missing_child"),
+ );
});
});
diff --git a/src/app/core/basic-datatypes/entity/edit-single-entity/edit-single-entity.component.ts b/src/app/core/basic-datatypes/entity/edit-single-entity/edit-single-entity.component.ts
index a4c3bed2bc..3bae23c0ae 100644
--- a/src/app/core/basic-datatypes/entity/edit-single-entity/edit-single-entity.component.ts
+++ b/src/app/core/basic-datatypes/entity/edit-single-entity/edit-single-entity.component.ts
@@ -10,6 +10,7 @@ import { MatFormFieldModule } from "@angular/material/form-field";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { NgIf } from "@angular/common";
import { ErrorHintComponent } from "../../../common-components/error-hint/error-hint.component";
+import { LoggingService } from "../../../logging/logging.service";
@DynamicComponent("EditSingleEntity")
@Component({
@@ -34,15 +35,30 @@ export class EditSingleEntityComponent
entities: Entity[] = [];
entityToId = (e: Entity) => e?.getId();
- constructor(private entityMapperService: EntityMapperService) {
+ constructor(
+ private entityMapper: EntityMapperService,
+ private logger: LoggingService,
+ ) {
super();
}
async ngOnInit() {
super.ngOnInit();
- this.entities = await this.entityMapperService.loadType(
- this.formFieldConfig.additional,
+ const availableEntities = await this.entityMapper.loadType(this.additional);
+ const selected = this.formControl.value;
+ if (selected && !availableEntities.some((e) => e.getId() === selected)) {
+ try {
+ const type = Entity.extractTypeFromId(selected);
+ const entity = await this.entityMapper.load(type, selected);
+ availableEntities.push(entity);
+ } catch (e) {
+ this.logger.warn(
+ `[EDIT_SINGLE_ENTITY] Could not find entity with ID: ${selected}: ${e}`,
+ );
+ }
+ }
+ this.entities = availableEntities.sort((e1, e2) =>
+ e1.toString().localeCompare(e2.toString()),
);
- this.entities.sort((e1, e2) => e1.toString().localeCompare(e2.toString()));
}
}
diff --git a/src/app/core/common-components/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/common-components/basic-autocomplete/basic-autocomplete.component.ts
index 9dddf24bca..44c2b11594 100644
--- a/src/app/core/common-components/basic-autocomplete/basic-autocomplete.component.ts
+++ b/src/app/core/common-components/basic-autocomplete/basic-autocomplete.component.ts
@@ -44,7 +44,6 @@ interface SelectableOption {
selected: boolean;
}
-/** Custom `MatFormFieldControl` for telephone number input. */
@Component({
selector: "app-basic-autocomplete",
templateUrl: "basic-autocomplete.component.html",
diff --git a/src/app/core/common-components/entities-table/list-paginator/list-paginator.component.spec.ts b/src/app/core/common-components/entities-table/list-paginator/list-paginator.component.spec.ts
index 8e5203f631..f126fd971c 100644
--- a/src/app/core/common-components/entities-table/list-paginator/list-paginator.component.spec.ts
+++ b/src/app/core/common-components/entities-table/list-paginator/list-paginator.component.spec.ts
@@ -1,17 +1,9 @@
-import {
- ComponentFixture,
- fakeAsync,
- TestBed,
- tick,
- waitForAsync,
-} from "@angular/core/testing";
+import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { ListPaginatorComponent } from "./list-paginator.component";
import { MatTableDataSource } from "@angular/material/table";
import { PageEvent } from "@angular/material/paginator";
-import { MockedTestingModule } from "../../../../utils/mocked-testing.module";
-import { EntityMapperService } from "../../../entity/entity-mapper/entity-mapper.service";
-import { User } from "../../../user/user";
+import { NoopAnimationsModule } from "@angular/platform-browser/animations";
describe("ListPaginatorComponent", () => {
let component: ListPaginatorComponent;
@@ -19,7 +11,7 @@ describe("ListPaginatorComponent", () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
- imports: [ListPaginatorComponent, MockedTestingModule.withState()],
+ imports: [ListPaginatorComponent, NoopAnimationsModule],
}).compileComponents();
}));
@@ -30,44 +22,38 @@ describe("ListPaginatorComponent", () => {
fixture.detectChanges();
});
+ afterEach(() => localStorage.clear());
+
it("should create", () => {
expect(component).toBeTruthy();
});
- it("should save pagination settings in the user entity", fakeAsync(() => {
+ it("should save pagination settings in the local storage", () => {
component.idForSavingPagination = "table-id";
- const saveEntitySpy = spyOn(TestBed.inject(EntityMapperService), "save");
component.onPaginateChange({ pageSize: 20, pageIndex: 1 } as PageEvent);
- tick();
- expect(saveEntitySpy).toHaveBeenCalledWith(component.user);
- expect(component.user.paginatorSettingsPageSize["table-id"]).toEqual(20);
- }));
+ expect(
+ localStorage.getItem(component.LOCAL_STORAGE_KEY + "table-id"),
+ ).toEqual("20");
+ });
- it("should update pagination when the idForSavingPagination changed", fakeAsync(() => {
- const userPaginationSettings = {
- c1: 11,
- c2: 12,
- };
- component.user = {
- paginatorSettingsPageSize: userPaginationSettings,
- } as Partial as User;
+ it("should update pagination when the idForSavingPagination changed", () => {
+ localStorage.setItem(component.LOCAL_STORAGE_KEY + "c1", "11");
+ localStorage.setItem(component.LOCAL_STORAGE_KEY + "c2", "12");
component.idForSavingPagination = "c1";
component.ngOnChanges({ idForSavingPagination: undefined });
- tick();
fixture.detectChanges();
- expect(component.pageSize).toBe(userPaginationSettings.c1);
- expect(component.paginator.pageSize).toBe(userPaginationSettings.c1);
+ expect(component.pageSize).toBe(11);
+ expect(component.paginator.pageSize).toBe(11);
component.idForSavingPagination = "c2";
component.ngOnChanges({ idForSavingPagination: undefined });
- tick();
fixture.detectChanges();
- expect(component.pageSize).toBe(userPaginationSettings.c2);
- expect(component.paginator.pageSize).toBe(userPaginationSettings.c2);
- }));
+ expect(component.pageSize).toBe(12);
+ expect(component.paginator.pageSize).toBe(12);
+ });
});
diff --git a/src/app/core/common-components/entities-table/list-paginator/list-paginator.component.ts b/src/app/core/common-components/entities-table/list-paginator/list-paginator.component.ts
index e674843a9a..4c5cb91171 100644
--- a/src/app/core/common-components/entities-table/list-paginator/list-paginator.component.ts
+++ b/src/app/core/common-components/entities-table/list-paginator/list-paginator.component.ts
@@ -12,9 +12,6 @@ import {
PageEvent,
} from "@angular/material/paginator";
import { MatTableDataSource } from "@angular/material/table";
-import { User } from "../../../user/user";
-import { EntityMapperService } from "../../../entity/entity-mapper/entity-mapper.service";
-import { CurrentUserSubject } from "../../../session/current-user-subject";
@Component({
selector: "app-list-paginator",
@@ -24,6 +21,7 @@ import { CurrentUserSubject } from "../../../session/current-user-subject";
standalone: true,
})
export class ListPaginatorComponent implements OnChanges, OnInit {
+ readonly LOCAL_STORAGE_KEY = "PAGINATION-";
readonly pageSizeOptions = [10, 20, 50, 100];
@Input() dataSource: MatTableDataSource;
@@ -31,16 +29,8 @@ export class ListPaginatorComponent implements OnChanges, OnInit {
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
- user: User;
pageSize = 10;
- constructor(
- currentUser: CurrentUserSubject,
- private entityMapperService: EntityMapperService,
- ) {
- currentUser.subscribe((val: User) => (this.user = val));
- }
-
ngOnChanges(changes: SimpleChanges): void {
if (changes.hasOwnProperty("idForSavingPagination")) {
this.applyUserPaginationSettings();
@@ -53,33 +43,24 @@ export class ListPaginatorComponent implements OnChanges, OnInit {
onPaginateChange(event: PageEvent) {
this.pageSize = event.pageSize;
- this.updateUserPaginationSettings();
+ this.savePageSize(this.pageSize);
}
- private async applyUserPaginationSettings() {
- if (!this.user) {
- return;
- }
-
- const savedSize =
- this.user?.paginatorSettingsPageSize[this.idForSavingPagination];
+ private applyUserPaginationSettings() {
+ const savedSize = this.getSavedPageSize();
this.pageSize = savedSize && savedSize !== -1 ? savedSize : this.pageSize;
}
- private async updateUserPaginationSettings() {
- if (!this.user) {
- return;
- }
- // The page size is stored in the database, the page index is only in memory
- const hasChangesToBeSaved =
- this.pageSize !==
- this.user.paginatorSettingsPageSize[this.idForSavingPagination];
-
- this.user.paginatorSettingsPageSize[this.idForSavingPagination] =
- this.pageSize;
+ private getSavedPageSize(): number {
+ return Number.parseInt(
+ localStorage.getItem(this.LOCAL_STORAGE_KEY + this.idForSavingPagination),
+ );
+ }
- if (hasChangesToBeSaved) {
- await this.entityMapperService.save(this.user);
- }
+ private savePageSize(size: number) {
+ localStorage.setItem(
+ this.LOCAL_STORAGE_KEY + this.idForSavingPagination,
+ size?.toString(),
+ );
}
}
diff --git a/src/app/core/common-components/entity-form/entity-form.service.spec.ts b/src/app/core/common-components/entity-form/entity-form.service.spec.ts
index 05505e8316..d265b59841 100644
--- a/src/app/core/common-components/entity-form/entity-form.service.spec.ts
+++ b/src/app/core/common-components/entity-form/entity-form.service.spec.ts
@@ -27,6 +27,7 @@ import { EntitySchemaService } from "../../entity/schema/entity-schema.service";
import { FormFieldConfig } from "./FormConfig";
import { User } from "../../user/user";
import { TEST_USER } from "../../user/demo-user-generator.service";
+import { CurrentUserSubject } from "../../session/current-user-subject";
describe("EntityFormService", () => {
let service: EntityFormService;
@@ -264,6 +265,22 @@ describe("EntityFormService", () => {
Entity.schema.delete("test");
});
+ it("should not fail if user entity does not exist and current user value is assigned", () => {
+ TestBed.inject(CurrentUserSubject).next(undefined);
+
+ // simple property
+ Entity.schema.set("user", { defaultValue: PLACEHOLDERS.CURRENT_USER });
+ let form = service.createFormGroup([{ id: "user" }], new Entity());
+ expect(form.get("user")).toHaveValue(null);
+
+ // array property
+ Entity.schema.get("user").dataType = EntityArrayDatatype.dataType;
+ form = service.createFormGroup([{ id: "user" }], new Entity());
+ expect(form.get("user")).toHaveValue(null);
+
+ Entity.schema.delete("user");
+ });
+
it("should not assign default values to existing entities", () => {
Entity.schema.set("test", { defaultValue: 1 });
diff --git a/src/app/core/common-components/entity-form/entity-form.service.ts b/src/app/core/common-components/entity-form/entity-form.service.ts
index 29e744aa17..3833b2973a 100644
--- a/src/app/core/common-components/entity-form/entity-form.service.ts
+++ b/src/app/core/common-components/entity-form/entity-form.service.ts
@@ -201,12 +201,12 @@ export class EntityFormService {
newVal = new Date();
break;
case PLACEHOLDERS.CURRENT_USER:
- newVal = this.currentUser.value.getId();
+ newVal = this.currentUser.value?.getId();
break;
default:
newVal = schema.defaultValue;
}
- if (isArrayDataType(schema.dataType)) {
+ if (newVal && isArrayDataType(schema.dataType)) {
newVal = [newVal];
}
return newVal;
diff --git a/src/app/core/common-components/entity-select/entity-select.component.spec.ts b/src/app/core/common-components/entity-select/entity-select.component.spec.ts
index 4571a4ee94..51873eb7fe 100644
--- a/src/app/core/common-components/entity-select/entity-select.component.spec.ts
+++ b/src/app/core/common-components/entity-select/entity-select.component.spec.ts
@@ -16,6 +16,7 @@ import { HarnessLoader } from "@angular/cdk/testing";
import { TestbedHarnessEnvironment } from "@angular/cdk/testing/testbed";
import { MatAutocompleteHarness } from "@angular/material/autocomplete/testing";
import { EntityMapperService } from "app/core/entity/entity-mapper/entity-mapper.service";
+import { LoggingService } from "../../logging/logging.service";
describe("EntitySelectComponent", () => {
let component: EntitySelectComponent;
@@ -101,7 +102,10 @@ describe("EntitySelectComponent", () => {
it("discards IDs from initial selection that don't correspond to an existing entity", fakeAsync(() => {
component.entityType = User.ENTITY_TYPE;
- component.selection = ["not-existing-entity", testUsers[1].getId()];
+ component.selection = [
+ new Child("not-existing").getId(),
+ testUsers[1].getId(),
+ ];
fixture.detectChanges();
tick();
@@ -132,12 +136,10 @@ describe("EntitySelectComponent", () => {
component.unselectEntity(testUsers[0]);
- const remainingChildren = testUsers
- .filter((c) => c.getId() !== testUsers[0].getId())
- .map((c) => c.getId());
- expect(component.selectionChange.emit).toHaveBeenCalledWith(
- remainingChildren,
- );
+ const remainingUsers = testUsers
+ .map((u) => u.getId())
+ .filter((id) => id !== testUsers[0].getId());
+ expect(component.selectionChange.emit).toHaveBeenCalledWith(remainingUsers);
});
it("adds a new entity if it matches a known entity", fakeAsync(() => {
@@ -324,6 +326,34 @@ describe("EntitySelectComponent", () => {
);
}));
+ it("should show selected entities of type that is not configured", fakeAsync(() => {
+ component.entityType = [User.ENTITY_TYPE];
+ component.selection = [testUsers[0].getId(), testChildren[0].getId()];
+ tick();
+ fixture.detectChanges();
+ expect(component.selectedEntities).toEqual(
+ jasmine.arrayWithExactContents([testUsers[0], testChildren[0]]),
+ );
+ expect(component.allEntities).toEqual(
+ jasmine.arrayWithExactContents(testUsers),
+ );
+ expect(component.filteredEntities).toEqual(
+ jasmine.arrayWithExactContents(testUsers.slice(1)),
+ );
+ }));
+
+ it("should not fail if entity cannot be found", fakeAsync(() => {
+ const warnSpy = spyOn(TestBed.inject(LoggingService), "warn");
+ component.entityType = User.ENTITY_TYPE;
+ component.selection = [testUsers[0].getId(), "missing_user"];
+ tick();
+ fixture.detectChanges();
+ expect(warnSpy).toHaveBeenCalledWith(
+ jasmine.stringContaining("missing_user"),
+ );
+ expect(component.selectedEntities).toEqual([testUsers[0]]);
+ }));
+
it("should be able to select entities from different types", fakeAsync(() => {
component.entityType = [User.ENTITY_TYPE, Child.ENTITY_TYPE];
component.selection = [testUsers[1].getId(), testChildren[0].getId()];
@@ -332,4 +362,32 @@ describe("EntitySelectComponent", () => {
expect(component.selectedEntities).toEqual([testUsers[1], testChildren[0]]);
}));
+
+ it("should not request entities of the defined type which were not found", fakeAsync(() => {
+ const loadSpy = spyOn(
+ TestBed.inject(EntityMapperService),
+ "load",
+ ).and.callThrough();
+
+ component.entityType = User.ENTITY_TYPE;
+ const notExistingUser = new User("not-existing-user");
+ component.selection = [
+ testUsers[1].getId(),
+ testChildren[0].getId(),
+ notExistingUser.getId(),
+ ];
+
+ fixture.detectChanges();
+ tick();
+
+ expect(component.selectedEntities).toEqual([testUsers[1], testChildren[0]]);
+ expect(loadSpy).toHaveBeenCalledWith(
+ Child.ENTITY_TYPE,
+ testChildren[0].getId(),
+ );
+ expect(loadSpy).not.toHaveBeenCalledWith(
+ User.ENTITY_TYPE,
+ notExistingUser.getId(),
+ );
+ }));
});
diff --git a/src/app/core/common-components/entity-select/entity-select.component.ts b/src/app/core/common-components/entity-select/entity-select.component.ts
index 75b92c666b..f8b56310c0 100644
--- a/src/app/core/common-components/entity-select/entity-select.component.ts
+++ b/src/app/core/common-components/entity-select/entity-select.component.ts
@@ -27,6 +27,7 @@ import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { MatTooltipModule } from "@angular/material/tooltip";
import { MatInputModule } from "@angular/material/input";
import { MatCheckboxModule } from "@angular/material/checkbox";
+import { LoggingService } from "../../logging/logging.service";
@Component({
selector: "app-entity-select",
@@ -62,12 +63,12 @@ export class EntitySelectComponent implements OnChanges {
* @throws Error when `type` is not in the entity-map
*/
@Input() set entityType(type: string | string[]) {
- if (!Array.isArray(type)) {
- type = [type];
- }
- this.loadAvailableEntities(type);
+ this._entityType = Array.isArray(type) ? type : [type];
+ this.loadAvailableEntities();
}
+ private _entityType: string[];
+
/**
* The (initial) selection. Can be used in combination with {@link selectionChange}
* to enable two-way binding to an array of strings corresponding to the id's of the entities.
@@ -83,11 +84,35 @@ export class EntitySelectComponent implements OnChanges {
untilDestroyed(this),
filter((isLoading) => !isLoading),
)
- .subscribe((_) => {
- this.selectedEntities = sel
- .map((id) => this.allEntities.find((s) => id === s.getId()))
- .filter((e) => !!e);
- });
+ .subscribe(() => this.initSelectedEntities(sel));
+ }
+
+ private async initSelectedEntities(selected: string[]) {
+ const entities: E[] = [];
+ for (const s of selected) {
+ await this.getEntity(s)
+ .then((entity) => entities.push(entity))
+ .catch((err: Error) =>
+ this.logger.warn(
+ `[ENTITY_SELECT] Error loading selected entity "${s}": ${err.message}`,
+ ),
+ );
+ }
+ this.selectedEntities = entities;
+ // updating autocomplete values
+ this.formControl.setValue(this.formControl.value);
+ }
+
+ private async getEntity(id: string) {
+ const type = Entity.extractTypeFromId(id);
+ const entity = this._entityType.includes(type)
+ ? this.allEntities.find((e) => id === e.getId())
+ : await this.entityMapperService.load(type, id);
+
+ if (!entity) {
+ throw Error(`Entity not found`);
+ }
+ return entity;
}
/** Underlying data-array */
@@ -160,7 +185,10 @@ export class EntitySelectComponent implements OnChanges {
@ViewChild("inputField") inputField: ElementRef;
@ViewChild(MatAutocompleteTrigger) autocomplete: MatAutocompleteTrigger;
- constructor(private entityMapperService: EntityMapperService) {
+ constructor(
+ private entityMapperService: EntityMapperService,
+ private logger: LoggingService,
+ ) {
this.formControl.valueChanges
.pipe(
untilDestroyed(this),
@@ -195,11 +223,11 @@ export class EntitySelectComponent implements OnChanges {
@Input() additionalFilter: (e: E) => boolean = (_) => true;
- private async loadAvailableEntities(types: string[]) {
+ private async loadAvailableEntities() {
this.loading.next(true);
const entities: E[] = [];
- for (const type of types) {
+ for (const type of this._entityType) {
entities.push(...(await this.entityMapperService.loadType(type)));
}
this.allEntities = entities;
diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts
index 55977aa5cf..84b2a62b3a 100644
--- a/src/app/core/config/config-fix.ts
+++ b/src/app/core/config/config-fix.ts
@@ -277,8 +277,7 @@ export const defaultJsonConfig = {
]
}
],
- },
- "permittedUserRoles": ["admin_app"]
+ }
},
"view:help": {
"component": "MarkdownPage",
diff --git a/src/app/core/demo-data/demo-data-initializer.service.spec.ts b/src/app/core/demo-data/demo-data-initializer.service.spec.ts
index 966d0ebf35..6ff9475060 100644
--- a/src/app/core/demo-data/demo-data-initializer.service.spec.ts
+++ b/src/app/core/demo-data/demo-data-initializer.service.spec.ts
@@ -17,12 +17,14 @@ import { LoginState } from "../session/session-states/login-state.enum";
describe("DemoDataInitializerService", () => {
const normalUser: SessionInfo = {
- name: DemoUserGeneratorService.DEFAULT_USERNAME,
+ name: "demo",
+ entityId: "User:demo",
roles: ["user_app"],
};
const adminUser: SessionInfo = {
- name: DemoUserGeneratorService.ADMIN_USERNAME,
- roles: ["user_app", "admin_app", "account_manager"],
+ name: "demo-admin",
+ entityId: "User:demo-admin",
+ roles: ["user_app", "admin_app"],
};
let service: DemoDataInitializerService;
let mockDemoDataService: jasmine.SpyObj;
diff --git a/src/app/core/demo-data/demo-data-initializer.service.ts b/src/app/core/demo-data/demo-data-initializer.service.ts
index d2c47076ee..9eb5c529ed 100644
--- a/src/app/core/demo-data/demo-data-initializer.service.ts
+++ b/src/app/core/demo-data/demo-data-initializer.service.ts
@@ -5,7 +5,6 @@ import { MatDialog } from "@angular/material/dialog";
import { DemoDataGeneratingProgressDialogComponent } from "./demo-data-generating-progress-dialog.component";
import { SessionManagerService } from "../session/session-service/session-manager.service";
import { LocalAuthService } from "../session/auth/local/local-auth.service";
-import { KeycloakAuthService } from "../session/auth/keycloak/keycloak-auth.service";
import { SessionInfo, SessionSubject } from "../session/auth/session-info";
import { LoggingService } from "../logging/logging.service";
import { Database } from "../database/database";
@@ -16,6 +15,7 @@ import { AppSettings } from "../app-settings";
import { LoginStateSubject, SessionType } from "../session/session-type";
import memory from "pouchdb-adapter-memory";
import PouchDB from "pouchdb-browser";
+import { User } from "../user/user";
/**
* This service handles everything related to the demo-mode
@@ -31,10 +31,12 @@ export class DemoDataInitializerService {
private readonly normalUser: SessionInfo = {
name: DemoUserGeneratorService.DEFAULT_USERNAME,
roles: ["user_app"],
+ entityId: `${User.ENTITY_TYPE}:${DemoUserGeneratorService.DEFAULT_USERNAME}`,
};
private readonly adminUser: SessionInfo = {
name: DemoUserGeneratorService.ADMIN_USERNAME,
- roles: ["user_app", "admin_app", KeycloakAuthService.ACCOUNT_MANAGER_ROLE],
+ roles: ["user_app", "admin_app"],
+ entityId: `${User.ENTITY_TYPE}:${DemoUserGeneratorService.ADMIN_USERNAME}`,
};
constructor(
private demoDataService: DemoDataService,
@@ -69,7 +71,6 @@ export class DemoDataInitializerService {
}
private syncDatabaseOnUserChange() {
- // TODO needs to work without access to entity (entity is only available once sync starts)
this.loginState.subscribe((state) => {
if (
state === LoginState.LOGGED_IN &&
diff --git a/src/app/core/entity/default-datatype/view.directive.ts b/src/app/core/entity/default-datatype/view.directive.ts
index c65852f02c..5bc947a4a8 100644
--- a/src/app/core/entity/default-datatype/view.directive.ts
+++ b/src/app/core/entity/default-datatype/view.directive.ts
@@ -1,5 +1,5 @@
import { Entity } from "../model/entity";
-import { Directive, Input, OnChanges } from "@angular/core";
+import { Directive, Input, OnChanges, SimpleChanges } from "@angular/core";
@Directive()
export abstract class ViewDirective implements OnChanges {
@@ -18,7 +18,7 @@ export abstract class ViewDirective implements OnChanges {
* See: https://angularindepth.com/posts/1054/here-is-what-you-need-to-know-about-dynamic-components-in-angular#ngonchanges
*
*/
- ngOnChanges() {
+ ngOnChanges(changes?: SimpleChanges) {
this.isPartiallyAnonymized =
this.entity?.anonymized &&
this.entity?.getSchema()?.get(this.id)?.anonymize === "retain-anonymized";
diff --git a/src/app/core/entity/entity-mapper/mock-entity-mapper-service.ts b/src/app/core/entity/entity-mapper/mock-entity-mapper-service.ts
index 1d3769c89d..dc9d9cfbc6 100644
--- a/src/app/core/entity/entity-mapper/mock-entity-mapper-service.ts
+++ b/src/app/core/entity/entity-mapper/mock-entity-mapper-service.ts
@@ -84,7 +84,11 @@ export class MockEntityMapperService extends EntityMapperService {
const entityId = Entity.createPrefixedId(entityType, id);
const result = this.data.get(entityType)?.get(entityId);
if (!result) {
- throw new HttpErrorResponse({ status: 404 });
+ throw new HttpErrorResponse({
+ url: "MockEntityMapperService",
+ status: 404,
+ statusText: `${entityType}:${entityId} not found`,
+ });
}
return result;
}
@@ -115,12 +119,7 @@ export class MockEntityMapperService extends EntityMapperService {
): Promise {
const ctor = this.resolveConstructor(entityType);
const type = new ctor().getType();
- const entity = this.get(type, id) as T;
- if (!entity) {
- throw Error(`Entity ${id} does not exist in MockEntityMapper`);
- } else {
- return entity;
- }
+ return this.get(type, id) as T;
}
async loadType(
diff --git a/src/app/core/session/auth/keycloak/account-page/account-page.component.ts b/src/app/core/session/auth/keycloak/account-page/account-page.component.ts
index 2d69614f40..01bca2d16e 100644
--- a/src/app/core/session/auth/keycloak/account-page/account-page.component.ts
+++ b/src/app/core/session/auth/keycloak/account-page/account-page.component.ts
@@ -45,7 +45,6 @@ export class AccountPageComponent implements OnInit {
return;
}
- // TODO can we use keycloak for this?
this.authService.setEmail(this.email.value).subscribe({
next: () =>
this.alertService.addInfo(
diff --git a/src/app/core/session/auth/keycloak/keycloak-auth.service.spec.ts b/src/app/core/session/auth/keycloak/keycloak-auth.service.spec.ts
index 907b604ec4..fb47bc08b4 100644
--- a/src/app/core/session/auth/keycloak/keycloak-auth.service.spec.ts
+++ b/src/app/core/session/auth/keycloak/keycloak-auth.service.spec.ts
@@ -56,14 +56,18 @@ describe("KeycloakAuthService", () => {
return expectAsync(service.login()).toBeResolvedTo({
name: "test",
roles: ["user_app"],
+ entityId: "User:test",
});
});
- it("should throw an error if username claim is not available", () => {
- const tokenWithoutUser =
- "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJpcjdiVjZoOVkxazBzTGh2aFRxWThlMzgzV3NMY0V3cmFsaC1TM2NJUTVjIn0.eyJleHAiOjE2OTE1Njc4NzcsImlhdCI6MTY5MTU2NzU3NywianRpIjoiNzNiNGUzODEtMzk4My00ZjI1LWE1ZGYtOTRlOTYxYmU3MjgwIiwiaXNzIjoiaHR0cHM6Ly9rZXljbG9hay5hYW0tZGlnaXRhbC5uZXQvcmVhbG1zL2RldiIsInN1YiI6IjI0YWM1Yzg5LTU3OGMtNDdmOC1hYmQ5LTE1ZjRhNmQ4M2JiNSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFwcCIsInNlc3Npb25fc3RhdGUiOiIwYjVhNGQ0OS0wOTFhLTQzOGYtOTEwNi1mNmZjYmQyMDM1Y2EiLCJzY29wZSI6ImVtYWlsIiwic2lkIjoiMGI1YTRkNDktMDkxYS00MzhmLTkxMDYtZjZmY2JkMjAzNWNhIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJfY291Y2hkYi5yb2xlcyI6WyJkZWZhdWx0LXJvbGVzLWRldiIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iLCJ1c2VyX2FwcCJdfQ.EvF1wc32KwdDCUGboviRYGqKv2C3yK5B1WL_hCm-IGg58DoE_XGOchVVfbFrtphXD3yQa8uAaY58jWb6SeZQt0P92qtn5ulZOqcs3q2gQfrvxkxafMWffpCsxusVLBuGJ4B4EgoRGp_puQJIJE4p5KBwcA_u0PznFDFyLzPD18AYXevGWKLP5L8Zfgitf3Lby5AtCoOKHM7u6F_hDGSvLw-YlHEZBupqJzbpsjOs2UF1_woChMm2vbllZgIaUu9bbobcWi1mZSfNP3r9Ojk2t0NauOiKXDqtG5XyBLYMTC6wZXxsoCPGhOAwDr9LffkLDl-zvZ-0f_ujTpU8M2jzsg";
- mockKeycloak.getToken.and.resolveTo(tokenWithoutUser);
- return expectAsync(service.login()).toBeRejected();
+ it("should use `sub` if `username` is not available", () => {
+ const tokenWithoutUsername =
+ "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJycVB3eFM4U1hXZ2FGOFBDbDZrYWFkVUxOYWQyaEloX21vQjhmTDdUVnJJIn0.eyJleHAiOjE3MDcyMTk0MTgsImlhdCI6MTcwNzIxNTgxOCwiYXV0aF90aW1lIjoxNzA3MjE1MDQxLCJqdGkiOiI0OWZjMjEyZS0wNGMwLTRmOWItOTAwZi1mYmVlYWE5ZGZmZjUiLCJpc3MiOiJodHRwczovL2tleWNsb2FrLmFhbS1kaWdpdGFsLm5ldC9yZWFsbXMvZGV2Iiwic3ViIjoiODQ0MGFkZDAtOTdhOS00M2VkLWFmMGItMTE2YzBmYWI3ZTkwIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYXBwIiwibm9uY2UiOiI2N2I5N2U1NS1kMTY2LTQ3YjUtYTE4NC0zZDk1ZmIxMDQxM2UiLCJzZXNzaW9uX3N0YXRlIjoiZDZiYzQ2NTMtNmRmMC00M2NmLTliMWItNjgwODVmYTMyMTAzIiwic2NvcGUiOiJvcGVuaWQgZW1haWwiLCJzaWQiOiJkNmJjNDY1My02ZGYwLTQzY2YtOWIxYi02ODA4NWZhMzIxMDMiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIl9jb3VjaGRiLnJvbGVzIjpbInVzZXJfYXBwIl19.AK5qz9keozPFwBMl4xtBVt2T42AfkAdSvX5s6kSdZBdjfqnWazi3RB4YmQ-Rfik7z_uUhXayx2i72S557d3fo1G9YttLkormB2vZ-zM0GJeYXlGmG1jLUc8w3cQARdLBTrBsgWSGo2ZnZJ-eExn8UhwG5d5BUCl-IU-KJHB1C5R3sSTgXOpkED4WRaoxPOZORr40W263tHJjjNcPECUOtmpQvY0sGUbKHGWpqgWZNXE_G75DMHd0lEBeE924sIeEZcw0Y6TpjBwJULe89EVeI6sr4qhFKjNfn_2miB1HyOOM3jxUfUngR0ju0dJpm5Jmmcyr0Pah0QiA8OWVPKEZgQ\n";
+ mockKeycloak.getToken.and.resolveTo(tokenWithoutUsername);
+ return expectAsync(service.login()).toBeResolvedTo({
+ name: "8440add0-97a9-43ed-af0b-116c0fab7e90",
+ roles: ["user_app"],
+ });
});
it("should call keycloak for a password reset", () => {
diff --git a/src/app/core/session/auth/keycloak/keycloak-auth.service.ts b/src/app/core/session/auth/keycloak/keycloak-auth.service.ts
index 81158e0435..4dd986844f 100644
--- a/src/app/core/session/auth/keycloak/keycloak-auth.service.ts
+++ b/src/app/core/session/auth/keycloak/keycloak-auth.service.ts
@@ -1,10 +1,13 @@
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
-import { parseJwt } from "../../../../utils/utils";
import { environment } from "../../../../../environments/environment";
import { SessionInfo } from "../session-info";
import { KeycloakService } from "keycloak-angular";
+import { LoggingService } from "../../../logging/logging.service";
+import { Entity } from "../../../entity/model/entity";
+import { User } from "../../../user/user";
+import { ParsedJWT, parseJwt } from "../../../../session/session-utils";
/**
* Handles the remote session with keycloak
@@ -22,6 +25,7 @@ export class KeycloakAuthService {
constructor(
private httpClient: HttpClient,
private keycloak: KeycloakService,
+ private logger: LoggingService,
) {}
/**
@@ -53,21 +57,30 @@ export class KeycloakAuthService {
}
private processToken(token: string): SessionInfo {
- if (!token) {
- throw new Error();
- }
this.accessToken = token;
this.logSuccessfulAuth();
- const parsedToken = parseJwt(this.accessToken);
- if (!parsedToken.username) {
- throw new Error(
- `Login error: User is not correctly set up (userId: ${parsedToken.sub})`,
- );
- }
- return {
- name: parsedToken.username,
+ const parsedToken: ParsedJWT = parseJwt(this.accessToken);
+
+ const sessionInfo: SessionInfo = {
+ name: parsedToken.username ?? parsedToken.sub,
roles: parsedToken["_couchdb.roles"],
};
+
+ if (parsedToken.username) {
+ sessionInfo.entityId = parsedToken.username.includes(":")
+ ? parsedToken.username
+ : Entity.createPrefixedId(User.ENTITY_TYPE, parsedToken.username);
+ } else {
+ this.logger.debug(
+ `User not linked with an entity (userId: ${sessionInfo.name})`,
+ );
+ }
+
+ if (parsedToken.email) {
+ sessionInfo.email = parsedToken.email;
+ }
+
+ return sessionInfo;
}
/**
diff --git a/src/app/core/session/auth/session-info.ts b/src/app/core/session/auth/session-info.ts
index f8472cedb7..8b255b1345 100644
--- a/src/app/core/session/auth/session-info.ts
+++ b/src/app/core/session/auth/session-info.ts
@@ -7,17 +7,27 @@ import { BehaviorSubject } from "rxjs";
*/
export interface SessionInfo {
/**
- * ID of an in-app entity.
- * This can be used to retrieve an ID to which the logged-in user is linked.
+ * Name of user account.
+ */
+ name: string;
+
+ /**
+ * List of roles the logged-in user hold.
+ */
+ roles: string[];
+
+ /**
+ * ID of the entity which is connected with the user account.
*
* This is either a full ID or (e.g. Child:123) or only the last part.
* In the later case it refers to the `User` entity.
*/
- name?: string;
+ entityId?: string;
+
/**
- * a list of roles the logged-in user hold.
+ * Email address of a user
*/
- roles: string[];
+ email?: string;
}
/**
diff --git a/src/app/core/session/login/login.component.html b/src/app/core/session/login/login.component.html
index dee49cd7ae..a427346c3e 100644
--- a/src/app/core/session/login/login.component.html
+++ b/src/app/core/session/login/login.component.html
@@ -89,7 +89,7 @@
[disabled]="!enableOfflineLogin"
>
- {{ user.name }}
+ {{ user.email ?? user.name }}
diff --git a/src/app/core/session/session-service/session-manager.service.spec.ts b/src/app/core/session/session-service/session-manager.service.spec.ts
index 5bfa9ccac0..4a5c784d40 100644
--- a/src/app/core/session/session-service/session-manager.service.spec.ts
+++ b/src/app/core/session/session-service/session-manager.service.spec.ts
@@ -38,6 +38,7 @@ import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.se
import { mockEntityMapper } from "../../entity/entity-mapper/mock-entity-mapper-service";
import { User } from "../../user/user";
import { TEST_USER } from "../../user/demo-user-generator.service";
+import { Child } from "../../../child-dev-project/children/model/child";
describe("SessionManagerService", () => {
let service: SessionManagerService;
@@ -122,7 +123,11 @@ describe("SessionManagerService", () => {
const currentUser = TestBed.inject(CurrentUserSubject);
// first login with existing user entity
- mockKeycloak.login.and.resolveTo({ name: TEST_USER, roles: [] });
+ mockKeycloak.login.and.resolveTo({
+ name: TEST_USER,
+ roles: [],
+ entityId: loggedInUser.getId(),
+ });
await service.remoteLogin();
expect(currentUser.value).toEqual(loggedInUser);
@@ -130,17 +135,53 @@ describe("SessionManagerService", () => {
await service.logout();
expect(currentUser.value).toBeUndefined();
+ const adminUser = new User("admin-user");
// login, user entity not available yet
- mockKeycloak.login.and.resolveTo({ name: "admin-user", roles: ["admin"] });
+ mockKeycloak.login.and.resolveTo({
+ name: "admin-user",
+ roles: ["admin"],
+ entityId: adminUser.getId(),
+ });
await service.remoteLogin();
expect(currentUser.value).toBeUndefined();
// user entity available -> user should be set
- const adminUser = new User("admin-user");
await entityMapper.save(adminUser);
expect(currentUser.value).toEqual(adminUser);
});
+ it("should not initialize the user entity if no entityId is set", async () => {
+ const loadSpy = spyOn(TestBed.inject(EntityMapperService), "load");
+
+ mockKeycloak.login.and.resolveTo({ name: "some-user", roles: [] });
+ await service.remoteLogin();
+
+ expect(loadSpy).not.toHaveBeenCalled();
+ expect(loginStateSubject.value).toBe(LoginState.LOGGED_IN);
+ expect(TestBed.inject(CurrentUserSubject).value).toBeUndefined();
+ });
+
+ it("should allow other entities to log in", async () => {
+ const loggedInChild = new Child("123");
+ const childSession: SessionInfo = {
+ name: loggedInChild.getId(),
+ roles: [],
+ entityId: loggedInChild.getId(),
+ };
+ mockKeycloak.login.and.resolveTo(childSession);
+ const otherChild = new Child("456");
+ await TestBed.inject(EntityMapperService).saveAll([
+ loggedInChild,
+ otherChild,
+ ]);
+
+ await service.remoteLogin();
+
+ expect(sessionInfo.value).toBe(childSession);
+ expect(loginStateSubject.value).toBe(LoginState.LOGGED_IN);
+ expect(TestBed.inject(CurrentUserSubject).value).toEqual(loggedInChild);
+ });
+
it("should automatically login, if the session is still valid", async () => {
await service.remoteLogin();
diff --git a/src/app/core/session/session-service/session-manager.service.ts b/src/app/core/session/session-service/session-manager.service.ts
index 245c7c3076..1255bda115 100644
--- a/src/app/core/session/session-service/session-manager.service.ts
+++ b/src/app/core/session/session-service/session-manager.service.ts
@@ -24,7 +24,6 @@ import { LoginState } from "../session-states/login-state.enum";
import { Router } from "@angular/router";
import { KeycloakAuthService } from "../auth/keycloak/keycloak-auth.service";
import { LocalAuthService } from "../auth/local/local-auth.service";
-import { User } from "../../user/user";
import { AppSettings } from "../../app-settings";
import { PouchDatabase } from "../../database/pouch-database";
import { environment } from "../../../../environments/environment";
@@ -34,6 +33,7 @@ import { CurrentUserSubject } from "../current-user-subject";
import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service";
import { filter } from "rxjs/operators";
import { Subscription } from "rxjs";
+import { Entity } from "../../entity/model/entity";
/**
* This service handles the user session.
@@ -96,21 +96,27 @@ export class SessionManagerService {
return this.initializeUser(user);
}
- private async initializeUser(user: SessionInfo) {
- await this.initializeDatabaseForCurrentUser(user);
- this.sessionInfo.next(user);
+ private async initializeUser(session: SessionInfo) {
+ await this.initializeDatabaseForCurrentUser(session);
+ this.sessionInfo.next(session);
this.loginStateSubject.next(LoginState.LOGGED_IN);
+ if (session.entityId) {
+ this.initUserEntity(session.entityId);
+ }
+ }
+ private initUserEntity(entityId: string) {
+ const entityType = Entity.extractTypeFromId(entityId);
this.entityMapper
- .load(User, user.name)
+ .load(entityType, entityId)
.then((res) => this.currentUser.next(res))
.catch(() => undefined);
this.updateSubscription = this.entityMapper
- .receiveUpdates(User)
+ .receiveUpdates(entityType)
.pipe(
filter(
({ entity }) =>
- entity.getId() === user.name || entity.getId(true) === user.name,
+ entity.getId() === entityId || entity.getId(true) === entityId,
),
)
.subscribe(({ entity }) => this.currentUser.next(entity));
@@ -138,7 +144,7 @@ export class SessionManagerService {
}
// resetting app state
this.sessionInfo.next(undefined);
- this.updateSubscription.unsubscribe();
+ this.updateSubscription?.unsubscribe();
this.currentUser.next(undefined);
this.loginStateSubject.next(LoginState.LOGGED_OUT);
this.remoteLoggedIn = false;
diff --git a/src/app/core/user/user-account/user-account.component.html b/src/app/core/user/user-account/user-account.component.html
index e8c12b90ac..76b983aad3 100644
--- a/src/app/core/user/user-account/user-account.component.html
+++ b/src/app/core/user/user-account/user-account.component.html
@@ -32,5 +32,14 @@
+
+
diff --git a/src/app/core/user/user-account/user-account.component.scss b/src/app/core/user/user-account/user-account.component.scss
index 6fbc04ac53..438d7b9d2b 100644
--- a/src/app/core/user/user-account/user-account.component.scss
+++ b/src/app/core/user/user-account/user-account.component.scss
@@ -1,30 +1,11 @@
-/*
- * This file is part of ndb-core.
- *
- * ndb-core is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * ndb-core is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with ndb-core. If not, see .
- */
-
-@use "@angular/material/core/style/elevation" as mat-elevation;
-@use "variables/sizes";
-
-.info-field {
- @include mat-elevation.elevation(1);
- font-style: italic;
- margin: sizes.$small;
- padding: sizes.$small;
-}
-
.min-width-1-3 {
max-width: 600px;
}
+
+.entity-box {
+ border: 1px solid lightgrey;
+ border-radius: 10px;
+ width: fit-content;
+ display: flex;
+ padding: 10px;
+}
diff --git a/src/app/core/user/user-account/user-account.component.ts b/src/app/core/user/user-account/user-account.component.ts
index e9436e7347..e63a81b213 100644
--- a/src/app/core/user/user-account/user-account.component.ts
+++ b/src/app/core/user/user-account/user-account.component.ts
@@ -25,6 +25,9 @@ import { MatTooltipModule } from "@angular/material/tooltip";
import { MatInputModule } from "@angular/material/input";
import { AccountPageComponent } from "../../session/auth/keycloak/account-page/account-page.component";
import { CurrentUserSubject } from "../../session/current-user-subject";
+import { AsyncPipe, NgIf } from "@angular/common";
+import { DisplayEntityComponent } from "../../basic-datatypes/entity/display-entity/display-entity.component";
+import { SessionSubject } from "../../session/auth/session-info";
/**
* User account form to allow the user to view and edit information.
@@ -40,6 +43,9 @@ import { CurrentUserSubject } from "../../session/current-user-subject";
MatTooltipModule,
MatInputModule,
AccountPageComponent,
+ AsyncPipe,
+ DisplayEntityComponent,
+ NgIf,
],
standalone: true,
})
@@ -49,8 +55,12 @@ export class UserAccountComponent implements OnInit {
passwordChangeDisabled = false;
tooltipText;
+ entityId = this.sessionInfo.value.entityId;
- constructor(private currentUser: CurrentUserSubject) {}
+ constructor(
+ private currentUser: CurrentUserSubject,
+ private sessionInfo: SessionSubject,
+ ) {}
ngOnInit() {
this.checkIfPasswordChangeAllowed();
diff --git a/src/app/core/user/user-security/user-security.component.html b/src/app/core/user/user-security/user-security.component.html
index 47e250ff09..6427c51a6b 100644
--- a/src/app/core/user/user-security/user-security.component.html
+++ b/src/app/core/user/user-security/user-security.component.html
@@ -88,16 +88,6 @@
User is currently disabled and will not be able to login to the app