Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cd180bf6a |
@@ -1,20 +1,21 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import 'reflect-metadata';
|
||||||
import { GestionnairesController } from './gestionnaires.controller';
|
import { GestionnairesController } from './gestionnaires.controller';
|
||||||
import { GestionnairesService } from './gestionnaires.service';
|
import { RoleType } from 'src/entities/users.entity';
|
||||||
|
|
||||||
describe('GestionnairesController', () => {
|
describe('GestionnairesController roles (#161)', () => {
|
||||||
let controller: GestionnairesController;
|
it('POST /gestionnaires autorise SUPER_ADMIN et ADMINISTRATEUR', () => {
|
||||||
|
const roles = Reflect.getMetadata('roles', GestionnairesController.prototype.create);
|
||||||
beforeEach(async () => {
|
expect(roles).toEqual(
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
expect.arrayContaining([RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR]),
|
||||||
controllers: [GestionnairesController],
|
);
|
||||||
providers: [GestionnairesService],
|
expect(roles).not.toContain(RoleType.GESTIONNAIRE);
|
||||||
}).compile();
|
|
||||||
|
|
||||||
controller = module.get<GestionnairesController>(GestionnairesController);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('PATCH /gestionnaires/:id autorise SUPER_ADMIN et ADMINISTRATEUR', () => {
|
||||||
expect(controller).toBeDefined();
|
const roles = Reflect.getMetadata('roles', GestionnairesController.prototype.update);
|
||||||
|
expect(roles).toEqual(
|
||||||
|
expect.arrayContaining([RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR]),
|
||||||
|
);
|
||||||
|
expect(roles).not.toContain(RoleType.GESTIONNAIRE);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ import { AuthGuard } from 'src/common/guards/auth.guard';
|
|||||||
export class GestionnairesController {
|
export class GestionnairesController {
|
||||||
constructor(private readonly gestionnairesService: GestionnairesService) { }
|
constructor(private readonly gestionnairesService: GestionnairesService) { }
|
||||||
|
|
||||||
@Roles(RoleType.SUPER_ADMIN)
|
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR)
|
||||||
@ApiResponse({ status: 201, description: 'Le gestionnaire a été créé avec succès.', type: Users })
|
@ApiResponse({ status: 201, description: 'Le gestionnaire a été créé avec succès.', type: Users })
|
||||||
@ApiResponse({ status: 409, description: 'Conflit. L\'email est déjà utilisé.' })
|
@ApiResponse({ status: 409, description: 'Conflit. L\'email est déjà utilisé.' })
|
||||||
@ApiOperation({ summary: 'Création d\'un gestionnaire' })
|
@ApiOperation({ summary: 'Création d\'un gestionnaire (admin / super admin)' })
|
||||||
@ApiBody({ type: CreateGestionnaireDto })
|
@ApiBody({ type: CreateGestionnaireDto })
|
||||||
@Post()
|
@Post()
|
||||||
create(@Body() dto: CreateGestionnaireDto): Promise<Users> {
|
create(@Body() dto: CreateGestionnaireDto): Promise<Users> {
|
||||||
@@ -43,7 +43,7 @@ export class GestionnairesController {
|
|||||||
return this.gestionnairesService.findAll();
|
return this.gestionnairesService.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RoleType.GESTIONNAIRE, RoleType.SUPER_ADMIN)
|
@Roles(RoleType.GESTIONNAIRE, RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR)
|
||||||
@ApiOperation({ summary: 'Récupérer un gestionnaire par ID' })
|
@ApiOperation({ summary: 'Récupérer un gestionnaire par ID' })
|
||||||
@ApiResponse({ status: 400, description: 'ID invalide' })
|
@ApiResponse({ status: 400, description: 'ID invalide' })
|
||||||
@ApiResponse({ status: 403, description: 'Accès refusé' })
|
@ApiResponse({ status: 403, description: 'Accès refusé' })
|
||||||
@@ -56,8 +56,8 @@ export class GestionnairesController {
|
|||||||
return this.gestionnairesService.findOne(id);
|
return this.gestionnairesService.findOne(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RoleType.SUPER_ADMIN)
|
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR)
|
||||||
@ApiOperation({ summary: 'Mettre à jour un gestionnaire' })
|
@ApiOperation({ summary: 'Mettre à jour un gestionnaire (admin / super admin)' })
|
||||||
@ApiResponse({ status: 200, description: 'Le gestionnaire a été mis à jour avec succès.', type: Users })
|
@ApiResponse({ status: 200, description: 'Le gestionnaire a été mis à jour avec succès.', type: Users })
|
||||||
@ApiResponse({ status: 404, description: 'Gestionnaire non trouvé' })
|
@ApiResponse({ status: 404, description: 'Gestionnaire non trouvé' })
|
||||||
@ApiResponse({ status: 403, description: 'Accès refusé' })
|
@ApiResponse({ status: 403, description: 'Accès refusé' })
|
||||||
|
|||||||
@@ -1,20 +1,13 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import 'reflect-metadata';
|
||||||
import { UserController } from './user.controller';
|
import { UserController } from './user.controller';
|
||||||
import { UserService } from './user.service';
|
import { RoleType } from 'src/entities/users.entity';
|
||||||
|
|
||||||
describe('UserController', () => {
|
describe('UserController roles (#161)', () => {
|
||||||
let controller: UserController;
|
it('POST /users/admin autorise SUPER_ADMIN et ADMINISTRATEUR', () => {
|
||||||
|
const roles = Reflect.getMetadata('roles', UserController.prototype.createAdmin);
|
||||||
beforeEach(async () => {
|
expect(roles).toEqual(
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
expect.arrayContaining([RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR]),
|
||||||
controllers: [UserController],
|
);
|
||||||
providers: [UserService],
|
expect(roles).not.toContain(RoleType.GESTIONNAIRE);
|
||||||
}).compile();
|
|
||||||
|
|
||||||
controller = module.get<UserController>(UserController);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(controller).toBeDefined();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ export class UserController {
|
|||||||
private readonly suppressionService: SuppressionService,
|
private readonly suppressionService: SuppressionService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
// Création d'un administrateur (réservée aux super admins)
|
// Création d'un administrateur (admin + super admin) — #161
|
||||||
@Post('admin')
|
@Post('admin')
|
||||||
@Roles(RoleType.SUPER_ADMIN)
|
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR)
|
||||||
@ApiOperation({ summary: 'Créer un nouvel administrateur (super admin seulement)' })
|
@ApiOperation({ summary: 'Créer un nouvel administrateur (admin / super admin)' })
|
||||||
createAdmin(
|
createAdmin(
|
||||||
@Body() dto: CreateAdminDto,
|
@Body() dto: CreateAdminDto,
|
||||||
@User() currentUser: Users
|
@User() currentUser: Users
|
||||||
|
|||||||
@@ -1,18 +1,87 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { BadRequestException, ForbiddenException } from '@nestjs/common';
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './user.service';
|
||||||
|
import { RoleType, StatutUtilisateurType } from 'src/entities/users.entity';
|
||||||
|
|
||||||
|
describe('UserService.createAdmin (#161)', () => {
|
||||||
|
const usersRepository = {
|
||||||
|
findOneBy: jest.fn(),
|
||||||
|
create: jest.fn(),
|
||||||
|
save: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
describe('UserService', () => {
|
|
||||||
let service: UserService;
|
let service: UserService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
const dto = {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
email: 'nouveau.admin@ptits-pas.fr',
|
||||||
providers: [UserService],
|
password: 'Password1!',
|
||||||
}).compile();
|
prenom: 'Nina',
|
||||||
|
nom: 'Admin',
|
||||||
|
telephone: '0601020304',
|
||||||
|
};
|
||||||
|
|
||||||
service = module.get<UserService>(UserService);
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
service = new UserService(
|
||||||
|
usersRepository as never,
|
||||||
|
{} as never,
|
||||||
|
{} as never,
|
||||||
|
{} as never,
|
||||||
|
{} as never,
|
||||||
|
{} as never,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('autorise un administrateur à créer un admin', async () => {
|
||||||
expect(service).toBeDefined();
|
usersRepository.findOneBy.mockResolvedValue(null);
|
||||||
|
usersRepository.create.mockImplementation((data) => data);
|
||||||
|
usersRepository.save.mockImplementation(async (entity) => ({
|
||||||
|
id: 'new-admin',
|
||||||
|
...entity,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await service.createAdmin(dto as never, {
|
||||||
|
id: 'admin-1',
|
||||||
|
role: RoleType.ADMINISTRATEUR,
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
expect(result.role).toBe(RoleType.ADMINISTRATEUR);
|
||||||
|
expect(result.statut).toBe(StatutUtilisateurType.ACTIF);
|
||||||
|
expect(usersRepository.save).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('autorise un super_admin à créer un admin', async () => {
|
||||||
|
usersRepository.findOneBy.mockResolvedValue(null);
|
||||||
|
usersRepository.create.mockImplementation((data) => data);
|
||||||
|
usersRepository.save.mockImplementation(async (entity) => ({
|
||||||
|
id: 'new-admin',
|
||||||
|
...entity,
|
||||||
|
}));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.createAdmin(dto as never, {
|
||||||
|
id: 'sa-1',
|
||||||
|
role: RoleType.SUPER_ADMIN,
|
||||||
|
} as never),
|
||||||
|
).resolves.toMatchObject({ role: RoleType.ADMINISTRATEUR });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refuse un gestionnaire (403 métier)', async () => {
|
||||||
|
await expect(
|
||||||
|
service.createAdmin(dto as never, {
|
||||||
|
id: 'gest-1',
|
||||||
|
role: RoleType.GESTIONNAIRE,
|
||||||
|
} as never),
|
||||||
|
).rejects.toBeInstanceOf(ForbiddenException);
|
||||||
|
expect(usersRepository.save).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refuse un email déjà utilisé', async () => {
|
||||||
|
usersRepository.findOneBy.mockResolvedValue({ id: 'exists' });
|
||||||
|
await expect(
|
||||||
|
service.createAdmin(dto as never, {
|
||||||
|
id: 'admin-1',
|
||||||
|
role: RoleType.ADMINISTRATEUR,
|
||||||
|
} as never),
|
||||||
|
).rejects.toBeInstanceOf(BadRequestException);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -117,8 +117,14 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createAdmin(dto: CreateAdminDto, currentUser: Users): Promise<Users> {
|
async createAdmin(dto: CreateAdminDto, currentUser: Users): Promise<Users> {
|
||||||
if (currentUser.role !== RoleType.SUPER_ADMIN) {
|
// #161 — admin et super_admin peuvent créer un administrateur
|
||||||
throw new ForbiddenException('Seuls les super administrateurs peuvent créer un administrateur');
|
if (
|
||||||
|
currentUser.role !== RoleType.SUPER_ADMIN &&
|
||||||
|
currentUser.role !== RoleType.ADMINISTRATEUR
|
||||||
|
) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
'Seuls les administrateurs et super administrateurs peuvent créer un administrateur',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const exist = await this.usersRepository.findOneBy({ email: dto.email });
|
const exist = await this.usersRepository.findOneBy({ email: dto.email });
|
||||||
|
|||||||
@@ -62,7 +62,10 @@ class _GestionnaireDashboardScreenState extends State<GestionnaireDashboardScree
|
|||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: UserManagementPanel(showAdministrateursTab: false),
|
child: UserManagementPanel(
|
||||||
|
showAdministrateursTab: false,
|
||||||
|
allowStaffAccountCreation: false,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const AppFooter(),
|
const AppFooter(),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
/// Création comptes staff (gestionnaire / admin) — ticket #161.
|
||||||
|
bool canCreateStaffAccounts(String? role) {
|
||||||
|
final r = (role ?? '').trim().toLowerCase();
|
||||||
|
return r == 'administrateur' || r == 'super_admin';
|
||||||
|
}
|
||||||
|
|
||||||
/// Droits d’affichage poubelle — tickets #154 / #160.
|
/// Droits d’affichage poubelle — tickets #154 / #160.
|
||||||
bool canDeleteMetier(String? role) {
|
bool canDeleteMetier(String? role) {
|
||||||
final r = (role ?? '').trim().toLowerCase();
|
final r = (role ?? '').trim().toLowerCase();
|
||||||
|
|||||||
@@ -15,9 +15,13 @@ class UserManagementPanel extends StatefulWidget {
|
|||||||
/// Afficher l'onglet Administrateurs (sinon sans Administrateurs).
|
/// Afficher l'onglet Administrateurs (sinon sans Administrateurs).
|
||||||
final bool showAdministrateursTab;
|
final bool showAdministrateursTab;
|
||||||
|
|
||||||
|
/// Création gestionnaire / admin (#161). False pour le dashboard gestionnaire.
|
||||||
|
final bool allowStaffAccountCreation;
|
||||||
|
|
||||||
const UserManagementPanel({
|
const UserManagementPanel({
|
||||||
super.key,
|
super.key,
|
||||||
this.showAdministrateursTab = true,
|
this.showAdministrateursTab = true,
|
||||||
|
this.allowStaffAccountCreation = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -86,6 +90,15 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
|
|||||||
|
|
||||||
bool get _isDossiersTab => _subIndex == 0;
|
bool get _isDossiersTab => _subIndex == 0;
|
||||||
|
|
||||||
|
bool get _isStaffAccountsTab =>
|
||||||
|
_subIndex == 4 || (widget.showAdministrateursTab && _subIndex == 5);
|
||||||
|
|
||||||
|
bool get _canShowAddButton {
|
||||||
|
if (_isDossiersTab) return false;
|
||||||
|
if (_isStaffAccountsTab && !widget.allowStaffAccountCreation) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
String _searchHintForTab() {
|
String _searchHintForTab() {
|
||||||
switch (_subIndex) {
|
switch (_subIndex) {
|
||||||
case 0:
|
case 0:
|
||||||
@@ -293,7 +306,8 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
|
|||||||
searchTooltip: _searchTooltipForTab(),
|
searchTooltip: _searchTooltipForTab(),
|
||||||
filterControl: _subBarFilterControl(),
|
filterControl: _subBarFilterControl(),
|
||||||
// Pas de « Créer » sur l’onglet Dossiers (#153).
|
// Pas de « Créer » sur l’onglet Dossiers (#153).
|
||||||
onAddPressed: _isDossiersTab ? null : _handleAddPressed,
|
// Pas de création staff pour le dashboard gestionnaire (#161).
|
||||||
|
onAddPressed: _canShowAddButton ? _handleAddPressed : null,
|
||||||
addLabel: 'Ajouter',
|
addLabel: 'Ajouter',
|
||||||
subTabCount: labels.length,
|
subTabCount: labels.length,
|
||||||
tabLabels: labels,
|
tabLabels: labels,
|
||||||
@@ -305,6 +319,9 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
|
|||||||
|
|
||||||
Future<void> _handleAddPressed() async {
|
Future<void> _handleAddPressed() async {
|
||||||
// 1 Parents, 2 Enfants, 3 AM, 4 Gestionnaires, 5 Admin
|
// 1 Parents, 2 Enfants, 3 AM, 4 Gestionnaires, 5 Admin
|
||||||
|
if (_isStaffAccountsTab && !widget.allowStaffAccountCreation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (_subIndex == 1) {
|
if (_subIndex == 1) {
|
||||||
await showDialog<void>(
|
await showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
|
|||||||
Reference in New Issue
Block a user