Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions src/auth/api-key.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy, ApiKe
if (!apiKeyDb) {
throw new UnauthorizedException(ErrorCodes.ApiKeyAuthFailed);
}
if (apiKeyDb.expiresOn < new Date()) {
throw new UnauthorizedException(ErrorCodes.ApiKeyExpired);
}

// Get the permissions and the UserID from the API Key instead of the user
const permissions = await this.permissionService.findPermissionGroupedByLevelForApiKey(apiKeyDb.id);
Expand Down
4 changes: 2 additions & 2 deletions src/controllers/api-key/api-key.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from "@nestjs/common";
import {
ApiBadRequestResponse,
ApiBearerAuth,
ApiForbiddenResponse,
ApiNotFoundResponse,
ApiOperation,
Expand All @@ -38,10 +39,9 @@ import { AuditLog } from "@services/audit-log.service";
import { OrganizationService } from "@services/user-management/organization.service";
import { UpdateApiKeyDto } from "@dto/api-key/update-api-key.dto";
import { checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@helpers/security-helper";
import { ApiAuth } from "@auth/swagger-auth-decorator";

@UseGuards(JwtAuthGuard, RolesGuard)
@ApiAuth()
@ApiBearerAuth()
@UserAdmin()
@ApiForbiddenResponse()
@ApiUnauthorizedResponse()
Expand Down
13 changes: 6 additions & 7 deletions src/controllers/user-management/organization.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@ import {
UseGuards,
} from "@nestjs/common";
import {
ApiBearerAuth,
ApiForbiddenResponse,
ApiNotFoundResponse,
ApiOperation,
ApiTags,
ApiUnauthorizedResponse,
} from "@nestjs/swagger";

import { JwtAuthGuard } from "@auth/jwt-auth.guard";
import { ApplicationAdmin, GatewayAdmin, GlobalAdmin, Read, UserAdmin } from "@auth/roles.decorator";
import { RolesGuard } from "@auth/roles.guard";
import { DeleteResponseDto } from "@dto/delete-application-response.dto";
Expand All @@ -38,21 +37,21 @@ import { OrganizationService } from "@services/user-management/organization.serv
import { AuditLog } from "@services/audit-log.service";
import { ActionType } from "@entities/audit-log-entry";
import { ListAllEntitiesDto } from "@dto/list-all-entities.dto";
import { ApiAuth } from "@auth/swagger-auth-decorator";
import { checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@helpers/security-helper";
import { PermissionType } from "@enum/permission-type.enum";
import { ComposeAuthGuard } from "@auth/compose-auth.guard";

@UseGuards(JwtAuthGuard, RolesGuard)
@ApiAuth()
@UseGuards(ComposeAuthGuard, RolesGuard)
@ApiBearerAuth()
@ApiForbiddenResponse()
@ApiUnauthorizedResponse()
@ApiTags("Organization")
@Controller("organization")
@GlobalAdmin()
export class OrganizationController {
constructor(private organizationService: OrganizationService) {}
private readonly logger = new Logger(OrganizationController.name);

constructor(private organizationService: OrganizationService) {}

@Post()
@ApiOperation({ summary: "Create a new Organization" })
async create(
Expand Down
4 changes: 2 additions & 2 deletions src/controllers/user-management/permission.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
UseGuards,
} from "@nestjs/common";
import {
ApiBearerAuth,
ApiForbiddenResponse,
ApiNotFoundResponse,
ApiOperation,
Expand Down Expand Up @@ -49,11 +50,10 @@ import { PermissionRequestAcceptUser } from "@dto/user-management/add-user-to-pe
import { OrganizationService } from "@services/user-management/organization.service";
import { Organization } from "@entities/organization.entity";
import { User } from "@entities/user.entity";
import { ApiAuth } from "@auth/swagger-auth-decorator";
import { ListAllPermissionsSlimResponseDto } from "@dto/list-all-permissions-slim-response.dto";

@UseGuards(JwtAuthGuard, RolesGuard)
@ApiAuth()
@ApiBearerAuth()
@ApiForbiddenResponse()
@ApiUnauthorizedResponse()
@ApiTags("Permission")
Expand Down
9 changes: 4 additions & 5 deletions src/controllers/user-management/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
Req,
UseGuards,
} from "@nestjs/common";
import { ApiForbiddenResponse, ApiOperation, ApiTags, ApiUnauthorizedResponse } from "@nestjs/swagger";
import { ApiBearerAuth, ApiForbiddenResponse, ApiOperation, ApiTags, ApiUnauthorizedResponse } from "@nestjs/swagger";
import { QueryFailedError } from "typeorm";

import { JwtAuthGuard } from "@auth/jwt-auth.guard";
Expand All @@ -41,20 +41,19 @@ import { ListAllEntitiesDto } from "@dto/list-all-entities.dto";
import { OrganizationService } from "@services/user-management/organization.service";
import { Organization } from "@entities/organization.entity";
import { RejectUserDto } from "@dto/user-management/reject-user.dto";
import { ApiAuth } from "@auth/swagger-auth-decorator";

@UseGuards(JwtAuthGuard, RolesGuard)
@UserAdmin()
@ApiAuth()
@ApiBearerAuth()
@ApiForbiddenResponse()
@ApiUnauthorizedResponse()
@ApiTags("User Management")
@Controller("user")
export class UserController {
constructor(private userService: UserService, private organizationService: OrganizationService) {}

private readonly logger = new Logger(UserController.name);

constructor(private userService: UserService, private organizationService: OrganizationService) {}

@Get("minimal")
@ApiOperation({ summary: "Get all id,names of users" })
async findAllMinimal(): Promise<ListAllUsersMinimalResponseDto> {
Expand Down
3 changes: 3 additions & 0 deletions src/entities/api-key.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,7 @@ export class ApiKey extends DbBaseEntity {
})
@JoinColumn()
systemUser: User;

@Column({ nullable: true })
expiresOn?: Date;
}
6 changes: 6 additions & 0 deletions src/entities/dto/api-key/create-api-key.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ApiProperty } from "@nestjs/swagger";
import { ArrayNotEmpty, ArrayUnique, IsArray, IsString, Length } from "class-validator";
import { IsSwaggerOptional } from "@helpers/optional-validator";
import { ValidateDate } from "@helpers/date.validator";

export class CreateApiKeyDto {
@ApiProperty({ required: true })
Expand All @@ -18,4 +20,8 @@ export class CreateApiKeyDto {
@ArrayNotEmpty()
@ArrayUnique()
permissionIds: number[];

@IsSwaggerOptional()
@ValidateDate()
expiresOn?: Date;
}
3 changes: 2 additions & 1 deletion src/entities/dto/list-all-entities.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,6 @@ export class ListAllEntitiesDto {
| "dataTargets"
| "organizationName"
| "commentOnLocation"
| "statusCheck";
| "statusCheck"
| "expiresOn";
}
7 changes: 6 additions & 1 deletion src/entities/dto/user-management/create-user.dto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Permission } from "@entities/permissions/permission.entity";
import { IsNotBlank } from "@helpers/is-not-blank.validator";
import { ApiProperty } from "@nestjs/swagger";
import { IsEmail, IsString, Length } from "class-validator";
import { IsSwaggerOptional } from "@helpers/optional-validator";
import { ValidateDate } from "@helpers/date.validator";

export class CreateUserDto {
@ApiProperty({ required: true })
Expand All @@ -27,4 +28,8 @@ export class CreateUserDto {

@ApiProperty({ required: false })
permissionIds?: number[];

@IsSwaggerOptional()
@ValidateDate()
expiresOn?: Date;
}
1 change: 1 addition & 0 deletions src/entities/enum/error-codes.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export enum ErrorCodes {
DeleteNotAllowedHasLoRaWANDevices = "MESSAGE.DELETE-NOT-ALLOWED-HAS-LORAWAN-DEVICE",
KOMBITLoginFailed = "MESSAGE.KOMBIT-LOGIN-FAILED",
ApiKeyAuthFailed = "MESSAGE.API-KEY-AUTH-FAILED",
ApiKeyExpired = "MESSAGE.API-KEY-EXPIRED",
TooMuchData = "MESSAGE.TOO-MUCH-DATA",
ApplicationDoesNotExist = "MESSAGE.APPLICATION-DOES-NOT-EXIST",
FailedToCreateOrUpdateIotDevice = "MESSAGE.FAILED-TO-CREATE-OR-UPDATE-IOT-DEVICE",
Expand Down
3 changes: 3 additions & 0 deletions src/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,7 @@ export class User extends DbBaseEntity {

@Column({ default: false })
showWelcomeScreen: boolean;

@Column({ nullable: true })
expiresOn?: Date;
}
16 changes: 16 additions & 0 deletions src/migration/1746172231928-AddedExpiresOnToUserAndApiKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class AddedExpiresOnToUserAndApiKey1746172231928 implements MigrationInterface {
name = 'AddedExpiresOnToUserAndApiKey1746172231928'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "api_key" ADD "expiresOn" TIMESTAMP`);
await queryRunner.query(`ALTER TABLE "user" ADD "expiresOn" TIMESTAMP`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "expiresOn"`);
await queryRunner.query(`ALTER TABLE "api_key" DROP COLUMN "expiresOn"`);
}

}
5 changes: 3 additions & 2 deletions src/modules/user-management/user.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Module, forwardRef } from "@nestjs/common";
import { forwardRef, Module } from "@nestjs/common";

import { SharedModule } from "@modules/shared.module";
import { PermissionModule } from "@modules/user-management/permission.module";
Expand All @@ -9,6 +9,7 @@ import { OrganizationModule } from "./organization.module";
import { ConfigModule } from "@nestjs/config";
import configuration from "@config/configuration";
import { OS2IoTMail } from "@services/os2iot-mail.service";
import { TemporaryAccessService } from "@services/temporary-access.service";

@Module({
imports: [
Expand All @@ -18,7 +19,7 @@ import { OS2IoTMail } from "@services/os2iot-mail.service";
forwardRef(() => OrganizationModule),
],
controllers: [UserController],
providers: [UserService, UserBootstrapperService, OS2IoTMail],
providers: [TemporaryAccessService, UserService, UserBootstrapperService, OS2IoTMail],
exports: [UserService],
})
export class UserModule {}
8 changes: 5 additions & 3 deletions src/services/api-key-management/api-key.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ListAllApiKeysResponseDto } from "@dto/api-key/list-all-api-keys-respon
import { ListAllApiKeysDto } from "@dto/api-key/list-all-api-keys.dto";
import { DeleteResponseDto } from "@dto/delete-application-response.dto";
import { ApiKey } from "@entities/api-key.entity";
import { forwardRef, Inject, Injectable, Logger } from "@nestjs/common";
import { forwardRef, Inject, Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { PermissionService } from "@services/user-management/permission.service";
import { Repository } from "typeorm";
Expand All @@ -21,7 +21,6 @@ export class ApiKeyService {
@Inject(forwardRef(() => PermissionService))
private permissionService: PermissionService
) {}
private readonly logger = new Logger(ApiKeyService.name, { timestamp: true });

findOne(key: string): Promise<ApiKey> {
return this.apiKeyRepository.findOne({
Expand Down Expand Up @@ -61,7 +60,7 @@ export class ApiKeyService {
}

if (query.orderOn && query.sort) {
dbQuery = dbQuery.orderBy(`api_key.${query.orderOn}`, query.sort.toUpperCase() as "ASC" | "DESC");
dbQuery = dbQuery.orderBy(`api_key.${query.orderOn}`, query.sort.toUpperCase() as "ASC" | "DESC", "NULLS LAST");
}

const [data, count] = await dbQuery.getManyAndCount();
Expand All @@ -79,13 +78,15 @@ export class ApiKeyService {
apiKey.name = dto.name;
apiKey.updatedBy = userId;
apiKey.createdBy = userId;
apiKey.expiresOn = dto.expiresOn;

// Create the system user
const systemUser = new User();
systemUser.active = false;
systemUser.isSystemUser = true;
systemUser.passwordHash = uuidv4(); // Random password, user can never log in
systemUser.name = apiKey.name;
systemUser.expiresOn = dto.expiresOn;
apiKey.systemUser = systemUser;

if (dto.permissionIds?.length > 0) {
Expand All @@ -101,6 +102,7 @@ export class ApiKeyService {
const apiKey = await this.findOneByIdWithRelations(id);
apiKey.name = dto.name;
apiKey.updatedBy = userId;
apiKey.expiresOn = dto.expiresOn;

if (dto.permissionIds?.length) {
const permissionsDb = await this.permissionService.findManyByIds(dto.permissionIds);
Expand Down
13 changes: 13 additions & 0 deletions src/services/temporary-access.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Cron, CronExpression } from "@nestjs/schedule";
import { UserService } from "@services/user-management/user.service";
import { Injectable } from "@nestjs/common";

@Injectable()
export class TemporaryAccessService {
constructor(private userService: UserService) {}

@Cron(CronExpression.EVERY_DAY_AT_1AM)
async expireAccess() {
await this.userService.disableExpiredUsers();
}
}
Loading