Skip to content

Commit f73fe12

Browse files
committed
chore: Update filestorage configuration for identities module
1 parent 0fd136a commit f73fe12

File tree

11 files changed

+184
-8
lines changed

11 files changed

+184
-8
lines changed

src/config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,10 @@ export default (): ConfigInstance => ({
117117
root: process.cwd() + '/storage',
118118
},
119119
},
120-
pictures: {
120+
identities: {
121121
driver: 'local',
122122
config: {
123-
root: process.cwd() + '/storage/pictures',
123+
root: process.cwd() + '/storage/identities',
124124
},
125125
},
126126
},

src/core/filestorage/_dto/filestorage.dto.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ export class FilestorageCreateDto extends CustomFieldsDto {
2626
@Matches(/^\/(\.?[^\/\0]+\/?)+$/, { message: 'Path must be a valid path' })
2727
public path: string
2828

29+
@IsOptional()
30+
@IsString()
31+
@ApiProperty({ type: String, required: false })
32+
public fingerprint?: string
33+
2934
@IsOptional()
3035
@IsMongoId()
3136
@ApiProperty({ type: String, required: false })

src/core/filestorage/_schemas/filestorage.schema.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ export class Filestorage extends AbstractSchema {
3232
})
3333
public path: string
3434

35+
@Prop({
36+
required: true,
37+
type: String,
38+
})
39+
public fingerprint: string
40+
3541
@Prop({
3642
required: false,
3743
type: String,

src/core/filestorage/filestorage.module.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import { TransformersFilestorageService } from '~/core/filestorage/_services/tra
1414
},
1515
]),
1616
],
17-
providers: [FilestorageService, TransformersFilestorageService],
1817
controllers: [FilestorageController],
19-
exports: [FilestorageService],
18+
providers: [FilestorageService, TransformersFilestorageService],
19+
exports: [FilestorageService, TransformersFilestorageService],
2020
})
2121
export class FilestorageModule { }

src/core/filestorage/filestorage.service.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// noinspection ExceptionCaughtLocallyJS
22

3-
import { BadRequestException, Inject, Injectable, Scope } from '@nestjs/common'
3+
import { BadRequestException, HttpException, HttpStatus, Inject, Injectable, Scope } from '@nestjs/common'
44
import { InjectModel } from '@nestjs/mongoose'
55
import { Filestorage } from './_schemas/filestorage.schema'
66
import { Document, FilterQuery, Model, ModifyResult, ProjectionType, Query, QueryOptions, SaveOptions, Types, UpdateQuery } from 'mongoose'
@@ -11,6 +11,7 @@ import { FilestorageCreateDto } from '~/core/filestorage/_dto/filestorage.dto'
1111
import { FsType } from '~/core/filestorage/_enum/fs-type.enum'
1212
import { omit } from 'radash'
1313
import { ModuleRef } from '@nestjs/core'
14+
import { createHash } from 'node:crypto'
1415

1516
export const EMBED_SEPARATOR = '#'
1617

@@ -61,7 +62,12 @@ export class FilestorageService extends AbstractServiceSchema {
6162
break
6263
}
6364
}
65+
66+
const fingerprint = createHash('sha256');
67+
fingerprint.update(file.buffer);
68+
payload.fingerprint = fingerprint.digest('hex').toString();
6469
payload.path = partPath.join('/')
70+
6571
return super.create(payload, options)
6672
} catch (e) {
6773
if (e.code === 'E_INVALID_CONFIG') {
@@ -160,5 +166,56 @@ export class FilestorageService extends AbstractServiceSchema {
160166

161167
return updated
162168
}
169+
170+
public async upsertFile<T extends AbstractSchema | Document>(
171+
filter?: FilterQuery<T>,
172+
data?: FilestorageCreateDto & { file?: Express.Multer.File },
173+
options?: QueryOptions<T> & { rawResult: true },
174+
) {
175+
let stored: Document<any, any, Filestorage> & Filestorage
176+
try {
177+
stored = await this.findOne<Filestorage>(filter, options)
178+
} catch (e) {
179+
if (e.status !== HttpStatus.NOT_FOUND) throw e
180+
}
181+
182+
if (stored) {
183+
const update = { ...omit(data, ['file']) }
184+
185+
const fingerprint = createHash('sha256');
186+
fingerprint.update(data.file.buffer);
187+
update.fingerprint = fingerprint.digest('hex').toString();
188+
189+
await this.checkFingerprint(
190+
{ _id: stored._id },
191+
update.fingerprint,
192+
)
193+
194+
const updated = await this.update(stored._id, update, options)
195+
this.storage.getDisk(stored.namespace).put(stored.path, data.file.buffer)
196+
197+
return updated
198+
} else {
199+
return await this.create(data, options)
200+
}
201+
}
202+
203+
public async checkFingerprint<T extends AbstractSchema | Document>(
204+
filters: FilterQuery<T>,
205+
fingerprint: string,
206+
): Promise<void> {
207+
const file = await this.model
208+
.findOne(
209+
{ ...filters, fingerprint },
210+
{
211+
_id: 1,
212+
},
213+
)
214+
.exec();
215+
if (file) {
216+
this.logger.debug(`Fingerprint matched for <${file._id}> (${fingerprint}).`);
217+
throw new HttpException('Fingerprint matched.', HttpStatus.NOT_MODIFIED);
218+
}
219+
}
163220
/* eslint-enable */
164221
}

src/management/identities/_dto/_parts/inetOrgPerson.dto.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ export class inetOrgPersonCreateDto {
8787
@ApiProperty({ required: false })
8888
@IsOptional()
8989
userPassword?: string;
90+
91+
@IsString()
92+
@ApiProperty({ required: false })
93+
@IsOptional()
94+
jpegPhoto?: string;
9095
}
9196

9297
export class inetOrgPersonDto extends inetOrgPersonCreateDto { }

src/management/identities/identities.controller.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ import {
66
Get,
77
HttpStatus,
88
Param,
9+
ParseFilePipe,
910
Patch,
1011
Post,
1112
Query,
12-
Res
13+
Res,
14+
UploadedFile,
15+
UseInterceptors
1316
} from '@nestjs/common';
14-
import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
17+
import { ApiOperation, ApiParam, ApiTags, PartialType } from '@nestjs/swagger';
1518
import { FilterOptions, FilterSchema, SearchFilterOptions, SearchFilterSchema } from '@the-software-compagny/nestjs_module_restools';
1619
import { Response } from 'express';
1720
import { Document, Types, isValidObjectId } from 'mongoose';
@@ -30,6 +33,14 @@ import { IdentityState } from './_enums/states.enum';
3033
import { Identities } from './_schemas/identities.schema';
3134
import { IdentitiesService } from './identities.service';
3235
import { IdentitiesValidationService } from './validations/identities.validation.service';
36+
import { FileInterceptor } from '@nestjs/platform-express';
37+
import { ApiFileUploadDecorator } from '~/_common/decorators/api-file-upload.decorator';
38+
import { FilestorageCreateDto, FilestorageDto, FileUploadDto } from '~/core/filestorage/_dto/filestorage.dto';
39+
import { FilestorageService } from '~/core/filestorage/filestorage.service';
40+
import { FsType } from '~/core/filestorage/_enum/fs-type.enum';
41+
import { join } from 'node:path';
42+
import { omit } from 'radash';
43+
import { TransformersFilestorageService } from '~/core/filestorage/_services/transformers-filestorage.service';
3344
// import { IdentitiesValidationFilter } from '~/_common/filters/identities-validation.filter';
3445

3546
// @UseFilters(new IdentitiesValidationFilter())
@@ -39,6 +50,8 @@ export class IdentitiesController extends AbstractController {
3950
constructor(
4051
protected readonly _service: IdentitiesService,
4152
protected readonly _validation: IdentitiesValidationService,
53+
protected readonly filestorage: FilestorageService,
54+
private readonly transformerService: TransformersFilestorageService,
4255
) {
4356
super();
4457
}
@@ -282,4 +295,55 @@ export class IdentitiesController extends AbstractController {
282295
});
283296
}
284297
}
298+
299+
@Post('upsert/photo')
300+
@UseInterceptors(FileInterceptor('file'))
301+
@ApiFileUploadDecorator(FileUploadDto, PartialType(FilestorageCreateDto), FilestorageDto)
302+
public async upsertInetOrgPersonJpegPhoto(
303+
@Res() res: Response,
304+
@Body() body: Partial<FilestorageCreateDto>,
305+
@SearchFilterSchema() searchFilterSchema: FilterSchema,
306+
@UploadedFile(new ParseFilePipe({ fileIsRequired: false })) file?: Express.Multer.File,
307+
): Promise<Response> {
308+
const identity = await this._service.findOne<Identities>(searchFilterSchema)
309+
const filter = {
310+
namespace: 'identities',
311+
path: join([
312+
identity.inetOrgPerson?.employeeType,
313+
identity.inetOrgPerson?.employeeNumber,
314+
'jpegPhoto.jpg',
315+
].join('/')),
316+
}
317+
318+
const data = await this.filestorage.upsertFile(filter, {
319+
...filter,
320+
type: FsType.FILE,
321+
file,
322+
...omit(body, ['namespace', 'path', 'type', 'file'] as any),
323+
})
324+
325+
return res.status(HttpStatus.OK).json({
326+
statusCode: HttpStatus.OK,
327+
data,
328+
})
329+
}
330+
331+
@Get('photo/raw')
332+
@ApiReadResponseDecorator(FilestorageDto)
333+
public async readPhotoRaw(
334+
@Res() res: Response,
335+
@SearchFilterSchema() searchFilterSchema: FilterSchema,
336+
@Query('mime') mime: string = '',
337+
): Promise<void> {
338+
const identity = await this._service.findOne<Identities>(searchFilterSchema)
339+
const [data, stream, parent] = await this.filestorage.findOneWithRawData({
340+
namespace: 'identities',
341+
path: join([
342+
identity.inetOrgPerson?.employeeType,
343+
identity.inetOrgPerson?.employeeNumber,
344+
'jpegPhoto.jpg',
345+
].join('/')),
346+
})
347+
await this.transformerService.transform(mime, res, data, stream, parent)
348+
}
285349
}

src/management/identities/identities.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { IdentitiesJsonformsService } from './jsonforms/identities.jsonforms.ser
99
import { IdentitiesJsonformsModule } from './jsonforms/identities.jsonforms.module';
1010
import { APP_FILTER } from '@nestjs/core';
1111
import { IdentitiesValidationFilter } from '~/_common/filters/identities-validation.filter';
12+
import { FilestorageModule } from '~/core/filestorage/filestorage.module';
1213

1314
@Module({
1415
imports: [
@@ -20,6 +21,7 @@ import { IdentitiesValidationFilter } from '~/_common/filters/identities-validat
2021
useFactory: () => IdentitiesSchema,
2122
},
2223
]),
24+
FilestorageModule,
2325
],
2426
providers: [
2527
IdentitiesService,
@@ -33,4 +35,4 @@ import { IdentitiesValidationFilter } from '~/_common/filters/identities-validat
3335
controllers: [IdentitiesController],
3436
exports: [IdentitiesService],
3537
})
36-
export class IdentitiesModule {}
38+
export class IdentitiesModule { }

src/management/identities/identities.service.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ import { IdentitiesUpsertDto } from './_dto/identities.dto';
2121
import { IdentityState } from './_enums/states.enum';
2222
import { Identities } from './_schemas/identities.schema';
2323
import { IdentitiesValidationService } from './validations/identities.validation.service';
24+
import { FactorydriveService } from '@the-software-compagny/nestjs_module_factorydrive';
2425

2526
@Injectable()
2627
export class IdentitiesService extends AbstractServiceSchema {
2728
constructor(
2829
@InjectModel(Identities.name) protected _model: Model<Identities>,
2930
protected readonly _validation: IdentitiesValidationService,
31+
protected readonly storage: FactorydriveService,
3032
) {
3133
super();
3234
}
@@ -35,6 +37,7 @@ export class IdentitiesService extends AbstractServiceSchema {
3537
data?: any,
3638
options?: SaveOptions,
3739
): Promise<Document<T, any, T>> {
40+
await this.checkInetOrgPersonJpegPhoto(data);
3841
const created: Document<T, any, T> = await super.create(data, options);
3942
return created;
4043
//TODO: add backends service logic here
@@ -66,6 +69,8 @@ export class IdentitiesService extends AbstractServiceSchema {
6669
throw new BadRequestException('inetOrgPerson.employeeNumber and inetOrgPerson.employeeType are required for create identity.');
6770
}
6871

72+
await this.checkInetOrgPersonJpegPhoto(data);
73+
6974
const logPrefix = `Validation [${data?.inetOrgPerson?.employeeType}:${data?.inetOrgPerson?.employeeNumber}]:`;
7075
try {
7176
this.logger.log(`${logPrefix} Starting additionalFields validation.`);
@@ -140,6 +145,8 @@ export class IdentitiesService extends AbstractServiceSchema {
140145
// if (update.state === IdentityState.TO_COMPLETE) {
141146
update = { ...update, state: IdentityState.TO_VALIDATE };
142147

148+
await this.checkInetOrgPersonJpegPhoto(update);
149+
143150
// }
144151
// if (update.state === IdentityState.SYNCED) {
145152
// update = { ...update, state: IdentityState.TO_VALIDATE };
@@ -286,4 +293,21 @@ export class IdentitiesService extends AbstractServiceSchema {
286293
throw error; // Rethrow the original error if it's not one of the handled types.
287294
}
288295
}
296+
297+
private async checkInetOrgPersonJpegPhoto(data: any) {
298+
if (data?.inetOrgPerson?.jpegPhoto) {
299+
let reqStorage;
300+
const [disk, path] = data.inetOrgPerson.jpegPhoto.split(':');
301+
302+
try {
303+
reqStorage = await this.storage.getDisk(disk).exists(path);
304+
} catch (error) {
305+
throw new BadRequestException(`Error while checking photo in storage: ${error.message}`);
306+
}
307+
308+
if (!reqStorage.exists) {
309+
throw new BadRequestException(`Photo not found in storage: ${data.inetOrgPerson.jpegPhoto}`);
310+
}
311+
}
312+
}
289313
}

src/management/identities/jsonforms/_config/inetorgperson.ui.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,12 @@ elements:
6969
- type: Control
7070
label: Titre
7171
scope: "#/properties/title"
72+
73+
- type: HorizontalLayout
74+
elements:
75+
- type: Control
76+
label: Photo
77+
scope: "#/properties/jpegPhoto"
78+
options:
79+
format: file
80+
storage: picture

0 commit comments

Comments
 (0)