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
8 changes: 7 additions & 1 deletion src/modules/proconnect/insee.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { ConfigService } from '@nestjs/config';
export interface OrganizationInfo {
nom: string;
isPublic: boolean;
isCommune: boolean;
codeCommune: string | null;
}

@Injectable()
Expand Down Expand Up @@ -57,8 +59,12 @@ export class InseeService {
const categorieJuridique =
uniteLegale?.categorieJuridiqueUniteLegale || '';
const isPublic = categorieJuridique.startsWith('7');
// Catégorie juridique 7210 = Commune et commune nouvelle (mairie)
const isCommune = categorieJuridique === '7210';
const codeCommune =
etablissement?.adresseEtablissement?.codeCommuneEtablissement || null;

return { nom, isPublic };
return { nom, isPublic, isCommune, codeCommune };
} catch (error) {
this.logger.error(
`Error fetching organization info for SIRET ${siret}: ${error.message}`,
Expand Down
24 changes: 23 additions & 1 deletion src/modules/proconnect/proconnect.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Controller, Get, Query, Res } from '@nestjs/common';
import {
Controller,
Get,
HttpException,
HttpStatus,
Query,
Res,
} from '@nestjs/common';
import { Response } from 'express';
import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
import { ConfigService } from '@nestjs/config';
Expand Down Expand Up @@ -60,9 +67,24 @@ export class ProConnectController {
res.redirect(`${frontendUrl}/#/proconnect-callback?${params.toString()}`);
} catch (error) {
console.error('ProConnect login callback error:', error);

const frontendUrl = this.configService.get<string>(
'MES_SIGNALEMENTS_URL',
);

if (typeof error === 'object' && error instanceof HttpException) {
const status = error.getStatus();
const response = error.getResponse();

if (status === HttpStatus.FORBIDDEN) {
const { error, errorLink } = response as any;
res.redirect(
`${frontendUrl}/#/proconnect-callback?error=${encodeURIComponent(error)}&errorLink=${encodeURIComponent(errorLink)}`,
Comment thread
MaGOs92 marked this conversation as resolved.
Comment thread
MaGOs92 marked this conversation as resolved.
);
return;
}
}

res.redirect(
`${frontendUrl}/#/proconnect-callback?error=${encodeURIComponent('Authentication failed')}`,
);
Expand Down
18 changes: 15 additions & 3 deletions src/modules/proconnect/proconnect.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export class ProConnectService {

if (!userInfo.siret) {
throw new HttpException(
'No SIRET found in user info',
'Aucun SIRET trouvé dans les informations utilisateur',
HttpStatus.BAD_REQUEST,
);
}
Expand All @@ -156,14 +156,26 @@ export class ProConnectService {

if (!organizationInfo) {
throw new HttpException(
`No organization found for SIRET ${userInfo.siret}`,
`Aucune organisation trouvée pour le SIRET ${userInfo.siret}`,
HttpStatus.BAD_REQUEST,
);
}

if (!organizationInfo.isPublic) {
throw new HttpException(
`Organization with SIRET ${userInfo.siret} is not a public organism`,
{
error: 'ORGANIZATION_NOT_PUBLIC',
},
HttpStatus.FORBIDDEN,
);
}

if (organizationInfo.isCommune) {
throw new HttpException(
{
error: `COMMUNE_NOT_ALLOWED`,
errorLink: `${this.configService.get<string>('MES_ADRESSES_URL')}/new?commune=${organizationInfo.codeCommune}`,
},
HttpStatus.FORBIDDEN,
);
Comment thread
MaGOs92 marked this conversation as resolved.
}
Expand Down
82 changes: 69 additions & 13 deletions src/tests/proconnect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { InseeService } from '../modules/proconnect/insee.service';
import { ProConnectModule } from '../modules/proconnect/proconnect.module';

const MES_SIGNALEMENTS_URL = 'http://localhost:7777';
const MES_ADRESSES_URL = 'http://localhost:8888';

jest.setTimeout(60000);

Expand Down Expand Up @@ -60,6 +61,7 @@ describe('ProConnect module', () => {
load: [
() => ({
MES_SIGNALEMENTS_URL,
MES_ADRESSES_URL,
API_SIGNALEMENT_URL: 'http://localhost:5005',
PROCONNECT_CLIENT_SECRET: 'test-secret',
}),
Expand Down Expand Up @@ -252,9 +254,12 @@ describe('ProConnect module', () => {
jest
.spyOn(proConnectService, 'getClient')
.mockResolvedValue(mockClient as any);
jest
.spyOn(inseeService, 'getOrganizationInfo')
.mockResolvedValue({ nom: 'Mairie Existante', isPublic: true });
jest.spyOn(inseeService, 'getOrganizationInfo').mockResolvedValue({
nom: 'Mairie Existante',
isPublic: true,
isCommune: false,
codeCommune: null,
});

const result = await proConnectService.handleCallback(
'code',
Expand Down Expand Up @@ -285,9 +290,12 @@ describe('ProConnect module', () => {
jest
.spyOn(proConnectService, 'getClient')
.mockResolvedValue(mockClient as any);
jest
.spyOn(inseeService, 'getOrganizationInfo')
.mockResolvedValue({ nom: 'Commune Nouvelle', isPublic: true });
jest.spyOn(inseeService, 'getOrganizationInfo').mockResolvedValue({
nom: 'Commune Nouvelle',
isPublic: true,
isCommune: false,
codeCommune: null,
});

const result = await proConnectService.handleCallback(
'code',
Expand Down Expand Up @@ -334,7 +342,7 @@ describe('ProConnect module', () => {

await expect(
proConnectService.handleCallback('code', signedState),
).rejects.toThrow('No SIRET found in user info');
).rejects.toThrow('Aucun SIRET trouvé dans les informations utilisateur');
});

it('should throw when organization is not a public organism', async () => {
Expand All @@ -355,21 +363,69 @@ describe('ProConnect module', () => {
jest
.spyOn(proConnectService, 'getClient')
.mockResolvedValue(mockClient as any);
jest
.spyOn(inseeService, 'getOrganizationInfo')
.mockResolvedValue({ nom: 'Entreprise Privée SAS', isPublic: false });
jest.spyOn(inseeService, 'getOrganizationInfo').mockResolvedValue({
nom: 'Entreprise Privée SAS',
isPublic: false,
isCommune: false,
codeCommune: null,
});

await expect(
proConnectService.handleCallback('code', signedState),
).rejects.toThrow(
'Organization with SIRET 55566677788899 is not a public organism',
);
).rejects.toMatchObject({
status: HttpStatus.FORBIDDEN,
response: { error: 'ORGANIZATION_NOT_PUBLIC' },
});

// Verify no source was created
const source = await sourceRepository.findOne({
where: { siret: '55566677788899' },
});
expect(source).toBeNull();
});

describe('mairie (commune)', () => {
const mairieUserInfo: ProConnectUserInfo = {
sub: 'user-mairie',
email: 'agent@mairie-test.fr',
given_name: 'Marie',
usual_name: 'Mairie',
siret: '21330063500017',
organizational_unit: 'Mairie',
};

const buildMockClient = () => ({
issuer: { metadata: { issuer: 'https://test-issuer' } },
callback: jest.fn().mockResolvedValue({ access_token: 'at' }),
userinfo: jest.fn().mockResolvedValue(mairieUserInfo),
});

it('should throw COMMUNE_NOT_ALLOWED with a Mes Adresses link for the commune', async () => {
jest
.spyOn(proConnectService, 'getClient')
.mockResolvedValue(buildMockClient() as any);
jest.spyOn(inseeService, 'getOrganizationInfo').mockResolvedValue({
nom: 'Mairie de Test',
isPublic: true,
isCommune: true,
codeCommune: '33063',
});

await expect(
proConnectService.handleCallback('code', signedState),
).rejects.toMatchObject({
status: HttpStatus.FORBIDDEN,
response: {
error: 'COMMUNE_NOT_ALLOWED',
errorLink: `${MES_ADRESSES_URL}/new?commune=33063`,
},
});

const source = await sourceRepository.findOne({
where: { siret: '21330063500017' },
});
expect(source).toBeNull();
});
});
});
});
Loading