From cd6c35879737cb93e4a2f96ef9f18252020ebb34 Mon Sep 17 00:00:00 2001 From: ingvord Date: Thu, 19 Sep 2024 17:12:59 +0200 Subject: [PATCH 1/5] Add metadataTypes endpoint - readonly; available for everyone --- src/datasets/datasets.controller.ts | 23 ++++++++++++++++++ src/datasets/datasets.service.ts | 37 +++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/src/datasets/datasets.controller.ts b/src/datasets/datasets.controller.ts index 0d7706ee1..33d798bc4 100644 --- a/src/datasets/datasets.controller.ts +++ b/src/datasets/datasets.controller.ts @@ -888,6 +888,29 @@ export class DatasetsController { return this.datasetsService.metadataKeys(parsedFilters); } + // GET /datasets/metadataTypes + // @UseGuards(PoliciesGuard) + // @CheckPolicies((ability: AppAbility) => + // ability.can(Action.DatasetRead, DatasetClass), + // ) + @Get("/metadataTypes") + @ApiOperation({ + summary: "Retrieve all available scientific metadata field types.", + description: + "This endpoint returns the field names and their corresponding data types from the `scientificMetadata` fields of all datasets in the system. It aggregates all the unique field names and their associated types from the dataset collection.", + }) + @ApiResponse({ + status: 200, + type: Array, + description: + "Returns an array of objects where each object contains a metadata field name and its associated type.", + }) + async metadataTypes(): Promise { + const result = this.datasetsService.getScientificMetadataTypes(); + + return result; + } + // GET /datasets/findOne @UseGuards(PoliciesGuard) @CheckPolicies("datasets", (ability: AppAbility) => diff --git a/src/datasets/datasets.service.ts b/src/datasets/datasets.service.ts index c78974c1e..b49de4269 100644 --- a/src/datasets/datasets.service.ts +++ b/src/datasets/datasets.service.ts @@ -476,6 +476,43 @@ export class DatasetsService { } } + async getScientificMetadataTypes() { + // TODO performance research required, say 1K records, 100K and 1M + return this.datasetModel.aggregate([ + { + // Step 1: Convert the scientificMetadata object into an array of key-value pairs + $project: { + scientificMetadataArray: { $objectToArray: "$scientificMetadata" }, + }, + }, + { + // Step 2: Unwind the array to treat each field individually + $unwind: "$scientificMetadataArray", + }, + { + // Step 3: Group by the field name and accumulate types + $group: { + _id: "$scientificMetadataArray.k", // Group by field name (key) + types: { $addToSet: { $type: "$scientificMetadataArray.v.value" } }, // Add the data type of the value + }, + }, + { + // Step 4: Convert the array of types into a string for each field name + $project: { + _id: 0, // Don't include the default _id + metadataKey: "$_id", + metadataType: { + $cond: { + if: { $eq: [{ $size: "$types" }, 1] }, // If only one type is present + then: { $arrayElemAt: ["$types", 0] }, // Use the type + else: "mixed", // If there are multiple types, mark it as 'mixed' + }, + }, + }, + }, + ]); + } + async isElasticSearchDBEmpty() { if (!this.ESClient) return; const count = await this.ESClient.getCount(); From 0271aa38c02b8ccc388d02015ffaf060e8cdcc20 Mon Sep 17 00:00:00 2001 From: Ingvord Date: Wed, 2 Oct 2024 15:14:54 +0200 Subject: [PATCH 2/5] Revert "Merge pull request #1404 from SciCatProject/Separate-Search-UI-Logic-from-Frontend" This reverts commit 09d2c756d52286c0b687a5e5a72d627718362ea9, reversing changes made to 38780fd17b8837cf393af3c318e78cb2a2eb8ad1. --- src/auth/auth.service.ts | 28 --- src/config/default-filters.config.json | 11 ++ src/config/frontend.config.json | 187 ++++++++---------- src/users/dto/update-user-settings.dto.ts | 25 ++- .../create-user-settings.interceptor.ts | 50 +++++ .../default-user-settings.interceptor.ts | 33 ++++ src/users/schemas/user-settings.schema.ts | 51 ++++- src/users/users.controller.spec.ts | 87 +++----- src/users/users.controller.ts | 70 +++---- src/users/users.service.ts | 45 +---- test/LoginUtils.js | 8 +- test/TestData.js | 31 --- test/UserAuthorization.js | 12 +- test/Users.js | 46 +---- 14 files changed, 322 insertions(+), 362 deletions(-) create mode 100644 src/config/default-filters.config.json create mode 100644 src/users/interceptors/create-user-settings.interceptor.ts create mode 100644 src/users/interceptors/default-user-settings.interceptor.ts diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 4d77c2461..3ee2933ae 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -10,7 +10,6 @@ import { flattenObject, parseBoolean } from "src/common/utils"; import { Issuer } from "openid-client"; import { ReturnedAuthLoginDto } from "./dto/returnedLogin.dto"; import { ReturnedUserDto } from "src/users/dto/returned-user.dto"; -import { CreateUserSettingsDto } from "src/users/dto/create-user-settings.dto"; @Injectable() export class AuthService { @@ -44,7 +43,6 @@ export class AuthService { async login(user: Omit): Promise { const expiresIn = this.configService.get("jwt.expiresIn"); const accessToken = this.jwtService.sign(user, { expiresIn }); - await this.postLoginTasks(user); return { access_token: accessToken, id: accessToken, @@ -124,30 +122,4 @@ export class AuthService { return { logout: "successful" }; } - /** - * postLoginTasks: Executes additional tasks after user login. - * - * - Checks if the user has userSettings record. - * - If user has no userSetting, it creates default userSetting for the user. - * @param user - The logged-in user (without password). - */ - async postLoginTasks(user: Omit) { - if (!user) return; - - const userId = user._id; - - const userSettings = await this.usersService.findByIdUserSettings(userId); - - if (!userSettings) { - Logger.log( - `Adding default settings to user ${user.username} with userId: ${user._id}`, - "postLoginTasks", - ); - const createUserSettingsDto: CreateUserSettingsDto = { - userId, - externalSettings: {}, - }; - await this.usersService.createUserSettings(userId, createUserSettingsDto); - } - } } diff --git a/src/config/default-filters.config.json b/src/config/default-filters.config.json new file mode 100644 index 000000000..848bb1970 --- /dev/null +++ b/src/config/default-filters.config.json @@ -0,0 +1,11 @@ +[ + { "type": "LocationFilterComponent", "visible": true }, + { "type": "PidFilterComponent", "visible": true }, + { "type": "PidFilterContainsComponent", "visible": false }, + { "type": "PidFilterStartsWithComponent", "visible": false }, + { "type": "GroupFilterComponent", "visible": true }, + { "type": "TypeFilterComponent", "visible": true }, + { "type": "KeywordFilterComponent", "visible": true }, + { "type": "DateRangeFilterComponent", "visible": true }, + { "type": "TextFilterComponent", "visible": true } +] \ No newline at end of file diff --git a/src/config/frontend.config.json b/src/config/frontend.config.json index 213c69a5e..39ec77d8b 100644 --- a/src/config/frontend.config.json +++ b/src/config/frontend.config.json @@ -26,7 +26,87 @@ "jsonMetadataEnabled": true, "jupyterHubUrl": "", "landingPage": "doi.ess.eu/detail/", - "lbBaseURL": "http://localhost:3000", + "lbBaseURL": "http://127.0.0.1:3000", + "localColumns": [ + { + "name": "select", + "order": 0, + "type": "standard", + "enabled": true + }, + { + "name": "pid", + "order": 1, + "type": "standard", + "enabled": true + }, + { + "name": "datasetName", + "order": 2, + "type": "standard", + "enabled": true + }, + { + "name": "runNumber", + "order": 3, + "type": "standard", + "enabled": true + }, + { + "name": "sourceFolder", + "order": 4, + "type": "standard", + "enabled": true + }, + { + "name": "size", + "order": 5, + "type": "standard", + "enabled": true + }, + { + "name": "creationTime", + "order": 6, + "type": "standard", + "enabled": true + }, + { + "name": "type", + "order": 7, + "type": "standard", + "enabled": true + }, + { + "name": "image", + "order": 8, + "type": "standard", + "enabled": true + }, + { + "name": "metadata", + "order": 9, + "type": "standard", + "enabled": false + }, + { + "name": "proposalId", + "order": 10, + "type": "standard", + "enabled": true + }, + { + "name": "ownerGroup", + "order": 11, + "type": "standard", + "enabled": false + }, + { + "name": "dataStatus", + "order": 12, + "type": "standard", + "enabled": false + } + ], "logbookEnabled": true, "loginFormEnabled": true, "maxDirectDownloadSize": 5000000000, @@ -100,108 +180,5 @@ "enabled": "#Selected", "authorization": ["#datasetAccess", "#datasetPublic"] } - ], - "labelMaps": { - "filters": { - "LocationFilter": "Location", - "PidFilter": "Pid", - "GroupFilter": "Group", - "TypeFilter": "Type", - "KeywordFilter": "Keyword", - "DateRangeFilter": "Start Date - End Date", - "TextFilter": "Text" - } - }, - "defaultDatasetsListSettings": { - "columns": [ - { - "name": "select", - "order": 0, - "type": "standard", - "enabled": true - }, - { - "name": "pid", - "order": 1, - "type": "standard", - "enabled": true - }, - { - "name": "datasetName", - "order": 2, - "type": "standard", - "enabled": true - }, - { - "name": "runNumber", - "order": 3, - "type": "standard", - "enabled": true - }, - { - "name": "sourceFolder", - "order": 4, - "type": "standard", - "enabled": true - }, - { - "name": "size", - "order": 5, - "type": "standard", - "enabled": true - }, - { - "name": "creationTime", - "order": 6, - "type": "standard", - "enabled": true - }, - { - "name": "type", - "order": 7, - "type": "standard", - "enabled": true - }, - { - "name": "image", - "order": 8, - "type": "standard", - "enabled": true - }, - { - "name": "metadata", - "order": 9, - "type": "standard", - "enabled": false - }, - { - "name": "proposalId", - "order": 10, - "type": "standard", - "enabled": true - }, - { - "name": "ownerGroup", - "order": 11, - "type": "standard", - "enabled": false - }, - { - "name": "dataStatus", - "order": 12, - "type": "standard", - "enabled": false - } - ], - "filters": [ - { "LocationFilter": true }, - { "PidFilter": true }, - { "GroupFilter": true }, - { "TypeFilter": true }, - { "KeywordFilter": true }, - { "DateRangeFilter": true }, - { "TextFilter": true } - ], - "conditions": [] - } + ] } diff --git a/src/users/dto/update-user-settings.dto.ts b/src/users/dto/update-user-settings.dto.ts index 63794c6ea..8458e4e84 100644 --- a/src/users/dto/update-user-settings.dto.ts +++ b/src/users/dto/update-user-settings.dto.ts @@ -1,7 +1,15 @@ import { ApiProperty, PartialType } from "@nestjs/swagger"; -import { IsNumber, IsObject, IsOptional } from "class-validator"; +import { + FilterConfig, + ScientificCondition, +} from "../schemas/user-settings.schema"; +import { IsArray, IsNumber } from "class-validator"; export class UpdateUserSettingsDto { + @ApiProperty() + @IsArray() + readonly columns: Record[]; + @ApiProperty({ type: Number, required: false, default: 25 }) @IsNumber() readonly datasetCount?: number; @@ -10,14 +18,13 @@ export class UpdateUserSettingsDto { @IsNumber() readonly jobCount?: number; - @ApiProperty({ - type: Object, - required: false, - default: {}, - }) - @IsOptional() - @IsObject() - readonly externalSettings?: Record; + @ApiProperty() + @IsArray() + readonly filters: FilterConfig[]; + + @ApiProperty() + @IsArray() + readonly conditions: ScientificCondition[]; } export class PartialUpdateUserSettingsDto extends PartialType( diff --git a/src/users/interceptors/create-user-settings.interceptor.ts b/src/users/interceptors/create-user-settings.interceptor.ts new file mode 100644 index 000000000..e22d4f881 --- /dev/null +++ b/src/users/interceptors/create-user-settings.interceptor.ts @@ -0,0 +1,50 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + Logger, + NestInterceptor, +} from "@nestjs/common"; +import { Observable, tap } from "rxjs"; +import { CreateUserSettingsDto } from "../dto/create-user-settings.dto"; +import { UsersService } from "../users.service"; +import { FILTER_CONFIGS } from "../schemas/user-settings.schema"; + +@Injectable() +export class CreateUserSettingsInterceptor implements NestInterceptor { + constructor(private usersService: UsersService) {} + async intercept( + context: ExecutionContext, + next: CallHandler, + ): Promise> { + return next.handle().pipe( + tap(async () => { + const res = context.switchToHttp().getResponse(); + const user = res.req.user; + if (!user) { + return; + } + const userId = user._id; + const userSettings = + await this.usersService.findByIdUserSettings(userId); + if (!userSettings) { + Logger.log( + `Adding default settings to user ${user.username}`, + "CreateUserSettingsInterceptor", + ); + const createUserSettingsDto: CreateUserSettingsDto = { + userId, + columns: [], + filters: FILTER_CONFIGS, + conditions: [], + }; + return this.usersService.createUserSettings( + userId, + createUserSettingsDto, + ); + } + return; + }), + ); + } +} diff --git a/src/users/interceptors/default-user-settings.interceptor.ts b/src/users/interceptors/default-user-settings.interceptor.ts new file mode 100644 index 000000000..61b8049e9 --- /dev/null +++ b/src/users/interceptors/default-user-settings.interceptor.ts @@ -0,0 +1,33 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + Logger, + NestInterceptor, +} from "@nestjs/common"; +import { map, Observable } from "rxjs"; +import { UsersService } from "../users.service"; +import { FILTER_CONFIGS } from "../schemas/user-settings.schema"; +import { UpdateUserSettingsDto } from "../dto/update-user-settings.dto"; + +@Injectable() +export class DefaultUserSettingsInterceptor implements NestInterceptor { + constructor(private usersService: UsersService) {} + async intercept( + context: ExecutionContext, + next: CallHandler, + ): Promise> { + return next.handle().pipe( + map(async () => { + Logger.log("DefaultUserSettingsInterceptor"); + const defaultUserSettings: UpdateUserSettingsDto = { + columns: [], + filters: FILTER_CONFIGS, + conditions: [], + }; + console.log(defaultUserSettings); + return defaultUserSettings; + }), + ); + } +} diff --git a/src/users/schemas/user-settings.schema.ts b/src/users/schemas/user-settings.schema.ts index e5d524936..9cfe2cae2 100644 --- a/src/users/schemas/user-settings.schema.ts +++ b/src/users/schemas/user-settings.schema.ts @@ -2,9 +2,28 @@ import * as mongoose from "mongoose"; import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; import { ApiProperty } from "@nestjs/swagger"; import { Document } from "mongoose"; +import filterConfigs from "../../config/default-filters.config.json"; export type UserSettingsDocument = UserSettings & Document; +// Define possible filter component types as a union of string literals +export type FilterComponentType = + | "LocationFilterComponent" + | "PidFilterComponent" + | "PidFilterContainsComponent" + | "PidFilterStartsWithComponent" + | "GroupFilterComponent" + | "TypeFilterComponent" + | "KeywordFilterComponent" + | "DateRangeFilterComponent" + | "TextFilterComponent"; + +// Define the Filter interface +export interface FilterConfig { + type: FilterComponentType; + visible: boolean; +} + // Define the Condition interface export interface ScientificCondition { field: string; @@ -12,6 +31,8 @@ export interface ScientificCondition { operator: string; } +export const FILTER_CONFIGS: FilterConfig[] = filterConfigs as FilterConfig[]; + @Schema({ collection: "UserSetting", toJSON: { @@ -23,6 +44,14 @@ export class UserSettings { id?: string; + @ApiProperty({ + type: [Object], + default: [], + description: "Array of the users preferred columns in dataset table", + }) + @Prop({ type: [Object], default: [] }) + columns: Record[]; + @ApiProperty({ type: Number, default: 25, @@ -44,13 +73,23 @@ export class UserSettings { userId: string; @ApiProperty({ - type: "object", - default: {}, - description: - "A customizable object for storing the user's external settings, which can contain various nested properties and configurations.", + type: [Object], + default: FILTER_CONFIGS, + description: "Array of filters the user has set", + }) + @Prop({ + type: [{ type: Object }], + default: FILTER_CONFIGS, + }) + filters: FilterConfig[]; + + @ApiProperty({ + type: [Object], + default: [], + description: "Array of conditions the user has set", }) - @Prop({ type: Object, default: {}, required: false }) - externalSettings: Record; + @Prop({ type: [{ type: Object }], default: [] }) + conditions: ScientificCondition[]; } export const UserSettingsSchema = SchemaFactory.createForClass(UserSettings); diff --git a/src/users/users.controller.spec.ts b/src/users/users.controller.spec.ts index 5c111e716..6bedb6203 100644 --- a/src/users/users.controller.spec.ts +++ b/src/users/users.controller.spec.ts @@ -4,8 +4,6 @@ import { CaslModule } from "src/casl/casl.module"; import { UsersController } from "./users.controller"; import { UsersService } from "./users.service"; import { UpdateUserSettingsDto } from "./dto/update-user-settings.dto"; -import { Request } from "express"; -import { UserSettings } from "./schemas/user-settings.schema"; class UsersServiceMock { findByIdUserIdentity(id: string) { @@ -27,13 +25,14 @@ class UsersServiceMock { const mockUserSettings = { _id: "user1", userId: "user1", + columns: [], datasetCount: 25, jobCount: 25, - externalSettings: { - filters: [{ LocationFilter: true }, { PidFilter: true }], - conditions: [{ field: "status", value: "active", operator: "equals" }], - columns: [], - }, + filters: [ + { type: "LocationFilterComponent", visible: true }, + { type: "PidFilterComponent", visible: true }, + ], + conditions: [{ field: "status", value: "active", operator: "equals" }], }; class AuthServiceMock {} @@ -55,6 +54,7 @@ describe("UsersController", () => { controller = module.get(UsersController); usersService = module.get(UsersService); + // bypass authorization jest .spyOn(controller as UsersController, "checkUserAuthorization") .mockImplementation(() => Promise.resolve()); @@ -65,74 +65,45 @@ describe("UsersController", () => { }); it("should return user settings with filters and conditions", async () => { - const userId = "user1"; - mockUserSettings._id = userId; - - const mockRequest: Partial = { - user: { _id: userId }, - }; + jest + .spyOn(usersService, "findByIdUserSettings") + .mockResolvedValue(mockUserSettings); - const result = await controller.getSettings(mockRequest as Request, userId); + const userId = "user1"; + const result = await controller.getSettings( + { user: { _id: userId } }, + userId, + ); - // Assert expect(result).toEqual(mockUserSettings); - expect(result?.externalSettings?.filters).toBeDefined(); - expect( - (result?.externalSettings?.filters as Record).length, - ).toBeGreaterThan(0); - expect(result?.externalSettings?.conditions).toBeDefined(); - expect( - (result?.externalSettings?.conditions as Record).length, - ).toBeGreaterThan(0); + expect(result.filters).toBeDefined(); + expect(result.filters.length).toBeGreaterThan(0); + expect(result.conditions).toBeDefined(); + expect(result.conditions.length).toBeGreaterThan(0); }); it("should update user settings with filters and conditions", async () => { - const userId = "user-id"; - mockUserSettings._id = userId; - const updatedSettings = { ...mockUserSettings, - externalSettings: { - filters: [{ PidFilter: true }], - conditions: [ - { field: "status", value: "inactive", operator: "equals" }, - ], - columns: [], - }, - }; - - const mockRequest: Partial = { - user: { _id: userId }, - }; - - const expectedResponse: UserSettings = { - ...updatedSettings, - _id: userId, - userId: userId, - datasetCount: updatedSettings.datasetCount, - jobCount: updatedSettings.jobCount, - externalSettings: updatedSettings.externalSettings, + filters: [{ type: "PidFilterContainsComponent", visible: false }], + conditions: [{ field: "status", value: "inactive", operator: "equals" }], }; jest .spyOn(usersService, "findOneAndUpdateUserSettings") - .mockResolvedValue(expectedResponse); + .mockResolvedValue(updatedSettings); + const userId = "user-id"; const result = await controller.updateSettings( - mockRequest as Request, + { user: { _id: userId } }, userId, updatedSettings, ); - expect(result).toEqual(expectedResponse); - expect(result?.externalSettings?.filters).toBeDefined(); - expect( - (result?.externalSettings?.filters as Record).length, - ).toBe(1); - expect(result?.externalSettings?.conditions).toBeDefined(); - expect( - (result?.externalSettings?.conditions as Record) - .length, - ).toBe(1); + expect(result).toEqual(updatedSettings); + expect(result.filters).toBeDefined(); + expect(result.filters.length).toBe(1); + expect(result.conditions).toBeDefined(); + expect(result.conditions.length).toBe(1); }); }); diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index 0747b450c..1e1dcceed 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -7,10 +7,12 @@ import { Req, Patch, Delete, + UseInterceptors, Put, Body, ForbiddenException, HttpCode, + CanActivate, } from "@nestjs/common"; import { ApiBearerAuth, @@ -30,8 +32,12 @@ import { Request } from "express"; import { JWTUser } from "../auth/interfaces/jwt-user.interface"; import { UserSettings } from "./schemas/user-settings.schema"; import { CreateUserSettingsDto } from "./dto/create-user-settings.dto"; -import { PartialUpdateUserSettingsDto } from "./dto/update-user-settings.dto"; +import { + PartialUpdateUserSettingsDto, + UpdateUserSettingsDto, +} from "./dto/update-user-settings.dto"; import { User } from "./schemas/user.schema"; +import { CreateUserSettingsInterceptor } from "./interceptors/create-user-settings.interceptor"; import { AuthService } from "src/auth/auth.service"; import { CredentialsDto } from "src/auth/dto/credentials.dto"; import { LocalAuthGuard } from "src/auth/guards/local-auth.guard"; @@ -42,6 +48,8 @@ import { CreateCustomJwt } from "./dto/create-custom-jwt.dto"; import { AuthenticatedPoliciesGuard } from "../casl/guards/auth-check.guard"; import { ReturnedUserDto } from "./dto/returned-user.dto"; import { ReturnedAuthLoginDto } from "src/auth/dto/returnedLogin.dto"; +import { PoliciesGuard } from "src/casl/guards/policies.guard"; +import { DefaultUserSettingsInterceptor } from "./interceptors/default-user-settings.interceptor"; @ApiBearerAuth() @ApiTags("users") @@ -95,25 +103,26 @@ export class UsersController { @UseGuards(LocalAuthGuard) @Post("login") @ApiOperation({ - summary: - "This endpoint is deprecated and will be removed soon. Use /auth/login instead", - description: - "This endpoint is deprecated and will be removed soon. Use /auth/login instead", + summary: "Functional accounts login.", + description: "It allows to login with functional (local) accounts.", }) @ApiResponse({ status: 201, type: ReturnedAuthLoginDto, description: - "This endpoint is deprecated and will be removed soon. Use /auth/login instead", + "Create a new JWT token for anonymous or the user that is currently logged in", }) - async login(@Req() req: Record): Promise { - return null; + async login( + @Req() req: Record, + ): Promise { + return await this.authService.login(req.user as Omit); } @UseGuards(AuthenticatedPoliciesGuard) @CheckPolicies("users", (ability: AppAbility) => ability.can(Action.UserReadOwn, User), ) + @UseInterceptors(CreateUserSettingsInterceptor) @Get("/my/self") @ApiOperation({ summary: "Returns the information of the user currently logged in.", @@ -175,6 +184,7 @@ export class UsersController { ability.can(Action.UserReadOwn, User) || ability.can(Action.UserReadAny, User), ) + @UseInterceptors(CreateUserSettingsInterceptor) @Get("/:id") async findById( @Req() request: Request, @@ -284,43 +294,19 @@ export class UsersController { async patchSettings( @Req() request: Request, @Param("id") id: string, - @Body() updateUserSettingsDto: PartialUpdateUserSettingsDto, + updateUserSettingsDto: PartialUpdateUserSettingsDto, ): Promise { await this.checkUserAuthorization( request, [Action.UserUpdateAny, Action.UserUpdateOwn], id, ); - return this.usersService.findOneAndPatchUserSettings( + return this.usersService.findOneAndUpdateUserSettings( id, updateUserSettingsDto, ); } - @UseGuards(AuthenticatedPoliciesGuard) - @CheckPolicies( - "users", - (ability: AppAbility) => - ability.can(Action.UserUpdateOwn, User) || - ability.can(Action.UserUpdateAny, User), - ) - @Patch("/:id/settings/external") - async patchExternalSettings( - @Req() request: Request, - @Param("id") id: string, - @Body() externalSettings: Record, - ): Promise { - await this.checkUserAuthorization( - request, - [Action.UserUpdateAny, Action.UserUpdateOwn], - id, - ); - return this.usersService.findOneAndPatchUserExternalSettings( - id, - externalSettings, - ); - } - @UseGuards(AuthenticatedPoliciesGuard) @CheckPolicies( "users", @@ -341,6 +327,22 @@ export class UsersController { return this.usersService.findOneAndDeleteUserSettings(id); } + @UseInterceptors(DefaultUserSettingsInterceptor) + @UseGuards( + class ByPassAuthenticatedPoliciesGuard + extends PoliciesGuard + implements CanActivate + { + async canActivate(): Promise { + return Promise.resolve(true); + } + }, + ) + @Get("/settings/default") + async getDefaultSettings(): Promise { + return Promise.resolve(new UserSettings()); + } + @UseGuards(AuthenticatedPoliciesGuard) @CheckPolicies("users", (ability: AppAbility) => { return ( diff --git a/src/users/users.service.ts b/src/users/users.service.ts index c94394aa0..7369730d7 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -273,57 +273,18 @@ export class UsersService implements OnModuleInit { } async findByIdUserSettings(userId: string): Promise { - const result = await this.userSettingsModel - .findOne({ userId }) - .lean() - .exec(); - - return result; + return this.userSettingsModel.findOne({ userId }).exec(); } async findOneAndUpdateUserSettings( userId: string, updateUserSettingsDto: UpdateUserSettingsDto | PartialUpdateUserSettingsDto, ): Promise { - const result = await this.userSettingsModel - .findOneAndUpdate({ userId }, updateUserSettingsDto, { - new: true, - upsert: true, - setDefaultsOnInsert: true, - }) + return this.userSettingsModel + .findOneAndUpdate({ userId }, updateUserSettingsDto, { new: true }) .exec(); - - return result; } - async findOneAndPatchUserSettings( - userId: string, - updateUserSettingsDto: UpdateUserSettingsDto | PartialUpdateUserSettingsDto, - ): Promise { - const result = await this.userSettingsModel - .findOneAndUpdate( - { userId }, - { $set: updateUserSettingsDto }, - { new: true }, - ) - .exec(); - return result; - } - - async findOneAndPatchUserExternalSettings( - userId: string, - externalSettings: Record, - ): Promise { - const updateQuery: Record = {}; - - for (const [key, value] of Object.entries(externalSettings)) { - updateQuery[`externalSettings.${key}`] = value; - } - const result = await this.userSettingsModel - .findOneAndUpdate({ userId }, { $set: updateQuery }, { new: true }) - .exec(); - return result; - } async findOneAndDeleteUserSettings(userId: string): Promise { return this.userSettingsModel.findOneAndDelete({ userId }).exec(); } diff --git a/test/LoginUtils.js b/test/LoginUtils.js index d5694d7ff..0a1cb950e 100644 --- a/test/LoginUtils.js +++ b/test/LoginUtils.js @@ -4,7 +4,7 @@ var request = require("supertest"); exports.getToken = function (appUrl, user) { return new Promise((resolve, reject) => { request(appUrl) - .post("/api/v3/auth/Login?include=user") + .post("/api/v3/Users/Login?include=user") .send(user) .set("Accept", "application/json") .end((err, res) => { @@ -20,17 +20,17 @@ exports.getToken = function (appUrl, user) { exports.getIdAndToken = function (appUrl, user) { return new Promise((resolve, reject) => { request(appUrl) - .post("/api/v3/auth/Login?include=user") + .post("/api/v3/Users/Login?include=user") .send(user) .set("Accept", "application/json") .end((err, res) => { if (err) { reject(err); } else { - resolve({ userId: res.body.userId, token: res.body.id }); + resolve({userId:res.body.userId, token:res.body.id}); } }); - }); + }); }; exports.getTokenAD = function (appUrl, user, cb) { diff --git a/test/TestData.js b/test/TestData.js index 2f5f0a22e..538f657bf 100644 --- a/test/TestData.js +++ b/test/TestData.js @@ -43,37 +43,6 @@ const TestData = { accessGroups: [], }, - userSettingsCorrect: { - datasetCount: 10, - jobCount: 25, - externalSettings: { - columns: [ - { - name: "select", - order: 0, - type: "standard", - enabled: true, - }, - ], - filters: [ - { - LocationFilter: true, - }, - ], - conditions: [ - { - condition: { - lhs: "test", - relation: "GREATER_THAN", - rhs: 1, - unit: "", - }, - enabled: true, - }, - ], - }, - }, - ProposalCorrectComplete: { proposalId: "20170267", pi_email: "pi@uni.edu", diff --git a/test/UserAuthorization.js b/test/UserAuthorization.js index 012b09d1f..a8fb227f0 100644 --- a/test/UserAuthorization.js +++ b/test/UserAuthorization.js @@ -21,7 +21,7 @@ let accessTokenAdminIngestor = null, userIdArchiveManager = null; describe("2300: User Authorization: test that user authorization are correct", () => { - beforeEach(async () => { + beforeEach(async() => { const loginResponseIngestor = await utils.getIdAndToken(appUrl, { username: "adminIngestor", password: TestData.Accounts["adminIngestor"]["password"], @@ -34,7 +34,7 @@ describe("2300: User Authorization: test that user authorization are correct", ( password: TestData.Accounts["user1"]["password"], }); userIdUser1 = loginResponseUser1.userId; - accessTokenUser1 = loginResponseUser1.token; + accessTokenUser1 = loginResponseUser1.token; const loginResponseUser2 = await utils.getIdAndToken(appUrl, { username: "user2", @@ -47,8 +47,8 @@ describe("2300: User Authorization: test that user authorization are correct", ( username: "user3", password: TestData.Accounts["user3"]["password"], }); - userIdUser3 = loginResponseUser3.userId; - accessTokenUser3 = loginResponseUser3.token; + userIdUser3 = loginResponseUser3.userId + accessTokenUser3 = loginResponseUser3.token const loginResponseUser4 = await utils.getIdAndToken(appUrl, { username: "user4", @@ -56,7 +56,7 @@ describe("2300: User Authorization: test that user authorization are correct", ( }); userIdUser4 = loginResponseUser4.userId; accessTokenUser4 = loginResponseUser4.token; - + const loginResponseAdmin = await utils.getIdAndToken(appUrl, { username: "admin", password: TestData.Accounts["admin"]["password"], @@ -71,7 +71,7 @@ describe("2300: User Authorization: test that user authorization are correct", ( userIdArchiveManager = loginResponseArchiveManager.userId; accessTokenArchiveManager = loginResponseArchiveManager.token; }); - + afterEach((done) => { sandbox.restore(); done(); diff --git a/test/Users.js b/test/Users.js index a6e81ec91..c4f8d7f66 100644 --- a/test/Users.js +++ b/test/Users.js @@ -10,7 +10,7 @@ let userIdUser1 = null, describe("2350: Users: Login with functional accounts", () => { it("0010: Admin ingestor login fails with incorrect credentials", async () => { return request(appUrl) - .post("/api/v3/auth/Login?include=user") + .post("/api/v3/Users/Login?include=user") .send({ username: "adminIngestor", password: TestData.Accounts["user1"]["password"], @@ -23,7 +23,7 @@ describe("2350: Users: Login with functional accounts", () => { it("0020: Login should succeed with correct credentials", async () => { return request(appUrl) - .post("/api/v3/auth/Login?include=user") + .post("/api/v3/Users/Login?include=user") .send({ username: "adminIngestor", password: TestData.Accounts["adminIngestor"]["password"], @@ -38,19 +38,18 @@ describe("2350: Users: Login with functional accounts", () => { }); describe("2360: Users settings", () => { - beforeEach(async () => { + beforeEach(async() => { const loginResponseUser1 = await utils.getIdAndToken(appUrl, { username: "user1", password: TestData.Accounts["user1"]["password"], }); userIdUser1 = loginResponseUser1.userId; - accessTokenUser1 = loginResponseUser1.token; + accessTokenUser1 = loginResponseUser1.token; }); - it("0020: Update users settings with valid value should success ", async () => { + it("0010: Update users settings with valid value should success ", async () => { return request(appUrl) .put(`/api/v3/Users/${userIdUser1}/settings`) - .send(TestData.userSettingsCorrect) .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenUser1}` }) .expect(TestData.SuccessfulPatchStatusCode) @@ -59,39 +58,8 @@ describe("2360: Users settings", () => { res.body.should.have.property("userId", userIdUser1); res.body.should.have.property("datasetCount"); res.body.should.have.property("jobCount"); - res.body.should.have.property("externalSettings"); - }); - }); - - it("0030: Patch users settings with valid value should success ", async () => { - return request(appUrl) - .patch(`/api/v3/Users/${userIdUser1}/settings`) - .send(TestData.userSettingsCorrect) - .set("Accept", "application/json") - .set({ Authorization: `Bearer ${accessTokenUser1}` }) - .expect(TestData.SuccessfulPatchStatusCode) - .expect("Content-Type", /json/) - .then((res) => { - res.body.should.have.property("userId", userIdUser1); - res.body.should.have.property("datasetCount"); - res.body.should.have.property("jobCount"); - res.body.should.have.property("externalSettings"); - }); - }); - - it("0040: Patch users external settings with valid value should success ", async () => { - return request(appUrl) - .patch(`/api/v3/Users/${userIdUser1}/settings/external`) - .send(TestData.userSettingsCorrect.externalSettings) - .set("Accept", "application/json") - .set({ Authorization: `Bearer ${accessTokenUser1}` }) - .expect(TestData.SuccessfulPatchStatusCode) - .expect("Content-Type", /json/) - .then((res) => { - res.body.should.have.property("userId", userIdUser1); - res.body.should.have.property("datasetCount"); - res.body.should.have.property("jobCount"); - res.body.should.have.property("externalSettings"); + res.body.should.have.property("filters"); + res.body.should.have.property("conditions"); }); }); }); From 31d3cd8b16acccba7d7487d2c70698beae7011bd Mon Sep 17 00:00:00 2001 From: ingvord Date: Tue, 12 Nov 2024 10:41:42 +0100 Subject: [PATCH 3/5] Progress #1141: cache metadataTypes --- package.json | 1 + src/app.module.ts | 9 +++++++++ src/datasets/datasets.controller.ts | 2 ++ 3 files changed, 12 insertions(+) diff --git a/package.json b/package.json index c2d233fcf..b151cbbf8 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@elastic/elasticsearch": "^8.15.0", "@nestjs-modules/mailer": "^2.0.2", "@nestjs/axios": "^3.0.0", + "@nestjs/cache-manager": "^2.3.0", "@nestjs/common": "^10.3.8", "@nestjs/config": "^3.0.0", "@nestjs/core": "^10.3.8", diff --git a/src/app.module.ts b/src/app.module.ts index 04604d824..a9b882744 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -32,6 +32,7 @@ import { EventEmitterModule } from "@nestjs/event-emitter"; import { AdminModule } from "./admin/admin.module"; import { HealthModule } from "./health/health.module"; import { LoggerModule } from "./loggers/logger.module"; +import { CacheModule } from "@nestjs/cache-manager"; @Module({ imports: [ @@ -94,6 +95,14 @@ import { LoggerModule } from "./loggers/logger.module"; UsersModule, AdminModule, HealthModule, + CacheModule.register({ + ttl: 3600, // cache duration in seconds (1 hour) + max: 100, // maximum number of items in cache + // Uncomment and configure Redis as needed + // store: redisStore, + // host: 'localhost', + // port: 6379, + }), ], controllers: [], providers: [ diff --git a/src/datasets/datasets.controller.ts b/src/datasets/datasets.controller.ts index 33d798bc4..1a9db50dc 100644 --- a/src/datasets/datasets.controller.ts +++ b/src/datasets/datasets.controller.ts @@ -98,6 +98,7 @@ import { JWTUser } from "src/auth/interfaces/jwt-user.interface"; import { LogbooksService } from "src/logbooks/logbooks.service"; import configuration from "src/config/configuration"; import { DatasetType } from "./dataset-type.enum"; +import { CACHE_MANAGER, CacheInterceptor } from "@nestjs/cache-manager"; @ApiBearerAuth() @ApiExtraModels( @@ -905,6 +906,7 @@ export class DatasetsController { description: "Returns an array of objects where each object contains a metadata field name and its associated type.", }) + @UseInterceptors(CacheInterceptor) async metadataTypes(): Promise { const result = this.datasetsService.getScientificMetadataTypes(); From 680d0bca62b56dc9215d5f552f90a989a2f690f0 Mon Sep 17 00:00:00 2001 From: ingvord Date: Tue, 12 Nov 2024 10:52:23 +0100 Subject: [PATCH 4/5] Progress #1141: cache metadataTypes --- src/datasets/datasets.module.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/datasets/datasets.module.ts b/src/datasets/datasets.module.ts index 2f5758761..c7da91bf2 100644 --- a/src/datasets/datasets.module.ts +++ b/src/datasets/datasets.module.ts @@ -13,6 +13,7 @@ import { LogbooksModule } from "src/logbooks/logbooks.module"; import { PoliciesService } from "src/policies/policies.service"; import { PoliciesModule } from "src/policies/policies.module"; import { ElasticSearchModule } from "src/elastic-search/elastic-search.module"; +import { CacheModule } from "@nestjs/cache-manager"; @Module({ imports: [ @@ -62,6 +63,14 @@ import { ElasticSearchModule } from "src/elastic-search/elastic-search.module"; inject: [PoliciesService], }, ]), + CacheModule.register({ + ttl: 3600, // cache duration in seconds (1 hour) + max: 100, // maximum number of items in cache + // Uncomment and configure Redis as needed + // store: redisStore, + // host: 'localhost', + // port: 6379, + }), ], exports: [DatasetsService], controllers: [DatasetsController], From e7f587f3d13fbdbf22b1603d9a87bfc87df41a73 Mon Sep 17 00:00:00 2001 From: ingvord Date: Fri, 22 Nov 2024 18:08:06 +0100 Subject: [PATCH 5/5] Progress #1141: fix cache time --- src/datasets/datasets.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/datasets/datasets.module.ts b/src/datasets/datasets.module.ts index c7da91bf2..b95506f7e 100644 --- a/src/datasets/datasets.module.ts +++ b/src/datasets/datasets.module.ts @@ -64,7 +64,7 @@ import { CacheModule } from "@nestjs/cache-manager"; }, ]), CacheModule.register({ - ttl: 3600, // cache duration in seconds (1 hour) + ttl: 3600000, // cache duration in seconds (1 hour) max: 100, // maximum number of items in cache // Uncomment and configure Redis as needed // store: redisStore,