diff --git a/src/admin/admin.controller.spec.ts b/src/admin/admin.controller.spec.ts index 675158f30b5261dc42d2ace11d146b28c37b5981..c8cf57eef5a13eec29e5f2097d4d1ab6f15f7dba 100644 --- a/src/admin/admin.controller.spec.ts +++ b/src/admin/admin.controller.spec.ts @@ -361,4 +361,10 @@ describe('AdminController', () => { it('should find unverified users', async () => { expect((await controller.findUnVerifiedUsers()).length).toBe(0); }); + + describe('should test getDeletedStructures()', () => { + it('should find getDeletedStructures', async () => { + expect((await controller.getDeletedStructures()).length).toBeGreaterThan(0); + }); + }); }); diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index 415bad28c53aa1b8f35abd9301e1a5ea692041f0..6f7b0874895e7fc2be070a8b6e68c8e7b7ff341e 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -309,4 +309,16 @@ export class AdminController { await this.usersService.updateUserEmployer(userDocument._id, employerDocument); return this.usersService.findById(setUserEmployer.userId); } + + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles('admin') + @ApiBearerAuth('JWT') + @ApiOperation({ description: 'Get deleted structures' }) + @Get('getDeletedStructures') + public async getDeletedStructures() { + this.logger.debug('getDeletedStructures'); + const deletedStructures = await this.structuresService.findAll(true); + this.logger.debug(`Found ${deletedStructures.length} deleted structures`); + return deletedStructures; + } } diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 50cd74207c831ce02792d0a239cfb016050efcd6..f29357b9dd9871be4abbb9a334df15ccfe02c216 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -6,6 +6,7 @@ import { UsersModule } from '../users/users.module'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { JwtStrategy } from './strategy/jwt.strategy'; +import { AnonymousStrategy } from './strategy/anonymous.strategy'; config(); @Module({ @@ -17,7 +18,7 @@ config(); signOptions: { expiresIn: '86400s' }, // 24h validity }), ], - providers: [AuthService, JwtStrategy], + providers: [AuthService, JwtStrategy, AnonymousStrategy], controllers: [AuthController], }) export class AuthModule {} diff --git a/src/auth/strategy/anonymous.strategy.ts b/src/auth/strategy/anonymous.strategy.ts new file mode 100644 index 0000000000000000000000000000000000000000..64d15bf0f8f8c8913bdc21fea299e55c94c893f3 --- /dev/null +++ b/src/auth/strategy/anonymous.strategy.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy } from 'passport'; + +@Injectable() +export class AnonymousStrategy extends PassportStrategy(Strategy, 'anonymous') { + constructor() { + super(); + } + + authenticate() { + return this.success({}); + } +} diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 625df75b54351cdb931f234bcd6ed1c385147f24..9235c3513c24c12ec5b801ff7ccec9c47f6220ba 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -23,7 +23,7 @@ export function rewriteGhostImgUrl(configService: ConfigurationService, itemData } export function hasAdminRole(user: User): boolean { - return user.role === UserRole.admin; + return user?.role === UserRole.admin; } const analyzer: Record<string, AnalysisAnalyzer> = { diff --git a/src/structures/services/structures.service.spec.ts b/src/structures/services/structures.service.spec.ts index 6433f74bc03ce526280563759c73ba7d54f44357..26674c4adb78bd498c8b5b4c9a31c5ed16b4c140 100644 --- a/src/structures/services/structures.service.spec.ts +++ b/src/structures/services/structures.service.spec.ts @@ -427,11 +427,15 @@ describe('StructuresService', () => { expect(service.findAll()).toBeTruthy(); }); + it('should find all deleted structures', () => { + expect(service.findAll(true)).toBeTruthy(); + }); + it('should find all unclaimed structures', () => { expect(service.findAllUnclaimed()).toBeTruthy(); }); - it('should find all formated structures', () => { + it('should find all formatted structures', () => { expect(service.findAllFormated(null)).toBeTruthy(); }); }); diff --git a/src/structures/services/structures.service.ts b/src/structures/services/structures.service.ts index 9309af635dcc0934fe99773df3944753f33045ed..5018ad442d9aca900825fdfe73a1e2c252d123be 100644 --- a/src/structures/services/structures.service.ts +++ b/src/structures/services/structures.service.ts @@ -220,10 +220,13 @@ export class StructuresService { }); } - public async findAll(): Promise<StructureDocument[]> { + /** + * @param deleted boolean to get deleted structures + */ + public async findAll(deleted = false): Promise<StructureDocument[]> { this.logger.debug('findAll'); return this.structureModel - .find({ deletedAt: { $exists: false }, accountVerified: true }) + .find({ deletedAt: { $exists: deleted }, accountVerified: true }) .populate('structureType') .exec(); } diff --git a/src/structures/structures.controller.spec.ts b/src/structures/structures.controller.spec.ts index 00394287ca9a1e8eddcbf456fb249bcbce37de35..75a0d6d683a5ae566a94e109d3f3bef09c964e35 100644 --- a/src/structures/structures.controller.spec.ts +++ b/src/structures/structures.controller.spec.ts @@ -10,6 +10,7 @@ import { UsersServiceMock } from '../../test/mock/services/user.mock.service'; import { CategoriesService } from '../categories/services/categories.service'; import { PersonalOffersService } from '../personal-offers/personal-offers.service'; import { TempUserService } from '../temp-user/temp-user.service'; +import { UserRole } from '../users/enum/user-role.enum'; import { UsersService } from '../users/services/users.service'; import { PersonalOffersServiceMock } from './../../test/mock/services/personalOffers.mock.service'; import { PersonalOffer, PersonalOfferDocument } from './../personal-offers/schemas/personal-offer.schema'; @@ -161,11 +162,28 @@ describe('AuthController', () => { expect(res).toBeTruthy(); }); - it('should find struct', async () => { - let res = controller.find('6093ba0e2ab5775cfc01ed3e'); - expect(res).toBeTruthy(); - res = controller.find(''); - expect(res).toBeTruthy(); + describe('find(structureId)', () => { + it('should find structure', async () => { + let res = controller.find({ user: {} }, '6093ba0e2ab5775cfc01ed3e'); + expect(res).toBeTruthy(); + res = controller.find({ user: {} }, ''); + expect(res).toBeTruthy(); + }); + + it('should find deleted structure as an admin', async () => { + const response = controller.find({ user: { role: UserRole.admin } }, '6093ba0e2ab5775cfc01ed33'); + expect(response).toBeTruthy(); + expect((await response).deletedAt).toBeTruthy(); + }); + + it('should not find deleted structure as anonymous', async () => { + try { + await controller.find({ user: {} }, '6093ba0e2ab5775cfc01ed33'); + expect(true).toBe(false); + } catch (error) { + expect(error.status).toEqual(HttpStatus.NOT_FOUND); + } + }); }); it('should find struct with owners', async () => { diff --git a/src/structures/structures.controller.ts b/src/structures/structures.controller.ts index cf505282b7f921afed532f66940ce3a5ba08e453..ced3eedffbbc288abcc0ebda7626340d7d47bae3 100644 --- a/src/structures/structures.controller.ts +++ b/src/structures/structures.controller.ts @@ -15,17 +15,18 @@ import { Request, UseGuards, } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import * as _ from 'lodash'; import { Types } from 'mongoose'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { CategoriesService } from '../categories/services/categories.service'; +import { hasAdminRole } from '../shared/utils'; import { CreateTempUserDto } from '../temp-user/dto/create-temp-user.dto'; import { ITempUser } from '../temp-user/temp-user.interface'; import { TempUser } from '../temp-user/temp-user.schema'; import { TempUserService } from '../temp-user/temp-user.service'; import { Roles } from '../users/decorators/roles.decorator'; -import { UserRole } from '../users/enum/user-role.enum'; import { IsStructureOwnerGuard } from '../users/guards/isStructureOwner.guard'; import { RolesGuard } from '../users/guards/roles.guard'; import { pendingStructuresLink } from '../users/interfaces/pendingStructure'; @@ -158,15 +159,15 @@ export class StructuresController { } @Get(':id') - public async find(@Param('id') id: string) { + @UseGuards(AuthGuard(['jwt', 'anonymous'])) // first success wins (allow anonymous call, req.user is an empty object if user is not authenticated) + public async find(@Request() req, @Param('id') id: string) { this.logger.debug(`find with ${id}`); - const result = await this.structureService.findOne(id); - if (!result || result.deletedAt) { + const structure = await this.structureService.findOne(id); + if (!structure || (structure.deletedAt && !hasAdminRole(req.user))) { this.logger.warn(`structure with ${id} does not exist`); throw new HttpException('Structure does not exist', HttpStatus.NOT_FOUND); - } else { - return result; } + return structure; } @Get(':id/withOwners') @@ -206,7 +207,7 @@ export class StructuresController { const otherOwners: IUser[] = (await this.userService.getStructureOwners(id)).filter((owner) => { return !owner._id.equals(req.user._id); }); - if (otherOwners.length === 0 || req.user.role === UserRole.admin) { + if (otherOwners.length === 0 || hasAdminRole(req.user)) { return this.structureService.deleteOne(structure); } else { return this.structureService.setToBeDeleted(req.user, structure); diff --git a/test/mock/services/structures.mock.service.ts b/test/mock/services/structures.mock.service.ts index a602463c6ad7bd9928d5cda3b16384dc3842d07c..3ef08059f2c2128c4916893af1acbdc7a454e7d7 100644 --- a/test/mock/services/structures.mock.service.ts +++ b/test/mock/services/structures.mock.service.ts @@ -262,6 +262,87 @@ export class StructuresServiceMock { }; } + if (id.toString() === '6093ba0e2ab5775cfc01ed33') { + return { + _id: '6903ba0e2ab5775cfc01ed4d', + structureType: null, + categories: { + accessModality: ['accesLibre'], + labelsQualifications: [ + 'monEspaceSante', + 'numRelay', + 'pix', + 'passNumerique', + 'fabriqueDeTerritoire', + 'aidantsConnect', + 'espacePublicNumeriqueepn', + 'maisonFranceService', + 'conseillerNumFranceServices', + ], + onlineProcedures: [ + 'idDoc', + 'training', + 'health', + 'work', + 'caf', + 'cpam', + 'foreigners', + 'needs', + 'franceConnect', + 'taxes', + 'housing', + 'retirement', + 'scolarity', + 'transport', + 'autres', + ], + selfServiceMaterial: ['wifiEnAccesLibre', 'computer', 'printer', 'scanner'], + solidarityMaterial: [], + baseSkills: ['computer', 'internet'], + advancedSkills: [], + age: ['young', 'family'], + languageAndIlliteracy: ['illettrisme', 'english'], + handicaps: ['physicalDisability'], + publicOthers: ['uniquementFemmes'], + }, + freeWorkShop: 'Non', + + structureName: "L'Atelier Numérique", + description: + "L'Atelier Numérique est l'Espace Public Numérique des Centres Sociaux de Rillieux-la-Pape, ayant pour mission la médiation numérique pour toutes et tous.", + lockdownActivity: + 'accesLibres, permanences numériques téléphoniques, cours et ateliers à distance, formations professionnelles.', + contactPhone: '', + contactMail: '', + website: '', + facebook: null, + twitter: null, + instagram: null, + pmrAccess: true, + exceptionalClosures: '', + publicsAccompaniment: [], + autresAccompagnements: '', + nbComputers: 16, + nbPrinters: 1, + __v: 0, + address: { + numero: '30 bis', + street: 'Avenue Leclerc', + commune: 'Rillieux-la-Pape', + postcode: '69140', + inseeCode: '69286', + }, + coord: [4.9036773, 45.8142196], + accountVerified: true, + linkedin: null, + nbScanners: 1, + otherDescription: null, + personalOffers: [], + createdAt: new Date('2020-11-16T09:30:00.000Z'), + updatedAt: new Date('2020-11-16T09:30:00.000Z'), + deletedAt: new Date('2020-11-16T09:30:00.000Z'), + }; + } return null; }