Skip to content

Commit 4ed0922

Browse files
Fix user add and update endpoint logic. The following problems were
fixed. - Server did not accept the data in the format of the api spec. fixed by changing to json. - Updating using required username and name always to be set, even if when not changing one of them. - When updating a user (with the mandatory username), the server would return a 404 saying this username is already in use.
1 parent 47efe69 commit 4ed0922

File tree

5 files changed

+136
-86
lines changed

5 files changed

+136
-86
lines changed

webapp/src/Controller/API/UserController.php

Lines changed: 57 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
use Symfony\Component\HttpFoundation\Response;
2424
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
2525
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
26+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
2627
use Symfony\Component\Security\Http\Attribute\IsGranted;
28+
use Symfony\Component\Validator\Exception\ValidationFailedException;
29+
use Symfony\Component\Validator\Validator\ValidatorInterface;
2730

2831
/**
2932
* @extends AbstractRestController<User, User>
@@ -41,7 +44,8 @@ public function __construct(
4144
DOMJudgeService $dj,
4245
ConfigurationService $config,
4346
EventLogService $eventLogService,
44-
protected readonly ImportExportService $importExportService
47+
protected readonly ImportExportService $importExportService,
48+
protected readonly ValidatorInterface $validator,
4549
) {
4650
parent::__construct($entityManager, $dj, $config, $eventLogService);
4751
}
@@ -86,9 +90,9 @@ public function addGroupsAction(Request $request): string
8690
$message = null;
8791
$result = -1;
8892
if ((($tsvFile && ($result = $this->importExportService->importTsv('groups', $tsvFile, $message))) ||
89-
($jsonFile && ($result = $this->importExportService->importJson('groups', $jsonFile, $message)))) &&
93+
($jsonFile && ($result = $this->importExportService->importJson('groups', $jsonFile, $message)))) &&
9094
$result >= 0) {
91-
return "$result new group(s) successfully added.";
95+
return "$result new group(s) successfully added.";
9296
} else {
9397
throw new BadRequestHttpException("Error while adding groups: $message");
9498
}
@@ -171,7 +175,7 @@ public function addTeamsAction(Request $request): string
171175
}
172176
$message = null;
173177
if ((($tsvFile && ($result = $this->importExportService->importTsv('teams', $tsvFile, $message))) ||
174-
($jsonFile && ($result = $this->importExportService->importJson('teams', $jsonFile, $message)))) &&
178+
($jsonFile && ($result = $this->importExportService->importJson('teams', $jsonFile, $message)))) &&
175179
$result >= 0) {
176180
// TODO: better return all teams here?
177181
return "$result new team(s) successfully added.";
@@ -235,7 +239,7 @@ public function addAccountsAction(Request $request): string
235239

236240
$message = null;
237241
if ((($tsvFile && ($result = $this->importExportService->importTsv('accounts', $tsvFile, $message))) ||
238-
($jsonFile && ($result = $this->importExportService->importJson('accounts', $jsonFile, $message)))) &&
242+
($jsonFile && ($result = $this->importExportService->importJson('accounts', $jsonFile, $message)))) &&
239243
$result >= 0) {
240244
// TODO: better return all accounts here?
241245
return "$result new account(s) successfully added.";
@@ -291,13 +295,12 @@ public function singleAction(Request $request, string $id): Response
291295
* Add a new user.
292296
*/
293297
#[IsGranted('ROLE_API_WRITER')]
294-
#[Rest\Post]
298+
#[Rest\Post()]
295299
#[OA\RequestBody(
296300
required: true,
297301
content: [
298-
new OA\MediaType(
299-
mediaType: 'multipart/form-data',
300-
schema: new OA\Schema(ref: new Model(type: AddUser::class))
302+
new OA\JsonContent(
303+
ref: new Model(type: AddUser::class)
301304
),
302305
]
303306
)]
@@ -307,86 +310,86 @@ public function singleAction(Request $request, string $id): Response
307310
content: new Model(type: User::class)
308311
)]
309312
public function addAction(
310-
#[MapRequestPayload(validationFailedStatusCode: Response::HTTP_BAD_REQUEST)]
313+
#[MapRequestPayload(validationFailedStatusCode: Response::HTTP_BAD_REQUEST, validationGroups: ['add'])]
311314
AddUser $addUser,
312315
Request $request
313316
): Response {
314317
return $this->addOrUpdateUser($addUser, $request);
315318
}
316319

317320
/**
318-
* Update an existing User or create one with the given ID
321+
* Update an existing User
319322
*/
320323
#[IsGranted('ROLE_API_WRITER')]
321-
#[Rest\Put('/{id}')]
324+
#[Rest\Patch('/{id}')]
322325
#[OA\RequestBody(
323326
required: true,
324327
content: [
325-
new OA\MediaType(
326-
mediaType: 'multipart/form-data',
327-
schema: new OA\Schema(ref: new Model(type: UpdateUser::class))
328+
new OA\JsonContent(
329+
ref: new Model(type: UpdateUser::class)
328330
),
329331
]
330332
)]
331333
#[OA\Response(
332334
response: 201,
333-
description: 'Returns the added user',
335+
description: 'Returns the updated user',
334336
content: new Model(type: User::class)
335337
)]
336338
public function updateAction(
337339
#[MapRequestPayload(validationFailedStatusCode: Response::HTTP_BAD_REQUEST)]
338-
UpdateUser $updateUser,
340+
UpdateUser $mutateUser,
341+
string $id,
339342
Request $request
340343
): Response {
341-
return $this->addOrUpdateUser($updateUser, $request);
344+
/** @var User|null $user */
345+
$user = $this->em->getRepository(User::class)->findOneBy(['externalid' => $id]);
346+
if ($user === null) {
347+
throw new NotFoundHttpException(sprintf("User with id %s not found", $id));
348+
}
349+
return $this->addOrUpdateUser($mutateUser, $request, $user);
342350
}
343351

344-
protected function addOrUpdateUser(AddUser $addUser, Request $request): Response
352+
protected function addOrUpdateUser(AddUser|UpdateUser $mutateUser, Request $request, ?User $user = null): Response
345353
{
346-
if ($addUser instanceof UpdateUser && !$addUser->id) {
347-
throw new BadRequestHttpException('`id` field is required');
354+
// if the user to update is not set, create a new user
355+
if (!$user) {
356+
$user = new User();
348357
}
349-
350-
if ($this->em->getRepository(User::class)->findOneBy(['username' => $addUser->username])) {
351-
throw new BadRequestHttpException(sprintf("User %s already exists", $addUser->username));
358+
if ($mutateUser->username !== null) {
359+
$user->setUsername($mutateUser->username);
352360
}
353-
354-
$user = new User();
355-
if ($addUser instanceof UpdateUser) {
356-
$existingUser = $this->em->getRepository(User::class)->findOneBy(['externalid' => $addUser->id]);
357-
if ($existingUser) {
358-
$user = $existingUser;
359-
}
361+
if ($mutateUser->name !== null) {
362+
$user->setName($mutateUser->name);
360363
}
361-
$user
362-
->setUsername($addUser->username)
363-
->setName($addUser->name)
364-
->setEmail($addUser->email)
365-
->setIpAddress($addUser->ip)
366-
->setPlainPassword($addUser->password)
367-
->setEnabled($addUser->enabled ?? true);
368-
369-
if ($addUser instanceof UpdateUser) {
370-
$user->setExternalid($addUser->id);
364+
if ($mutateUser->email !== null) {
365+
$user->setEmail($mutateUser->email);
371366
}
372-
373-
if ($addUser->teamId) {
367+
if ($mutateUser->ip !== null) {
368+
$user->setIpAddress($mutateUser->ip);
369+
}
370+
if ($mutateUser->password !== null) {
371+
$user->setPlainPassword($mutateUser->password);
372+
}
373+
if ($mutateUser->enabled !== null) {
374+
$user->setEnabled($mutateUser->enabled);
375+
}
376+
if ($mutateUser->teamId) {
374377
/** @var Team|null $team */
375378
$team = $this->em->createQueryBuilder()
376379
->from(Team::class, 't')
377380
->select('t')
378381
->andWhere('t.externalid = :team')
379-
->setParameter('team', $addUser->teamId)
382+
->setParameter('team', $mutateUser->teamId)
380383
->getQuery()
381384
->getOneOrNullResult();
382385

383386
if ($team === null) {
384-
throw new BadRequestHttpException(sprintf("Team %s not found", $addUser->teamId));
387+
throw new BadRequestHttpException(sprintf("Team %s not found", $mutateUser->teamId));
385388
}
386389
$user->setTeam($team);
387390
}
388391

389-
$roles = $addUser->roles;
392+
$roles = $mutateUser->roles;
390393
// For the file import we change a CDS user to the roles needed for ICPC CDS.
391394
if ($user->getUsername() === 'cds') {
392395
$roles = ['cds'];
@@ -408,18 +411,23 @@ protected function addOrUpdateUser(AddUser $addUser, Request $request): Response
408411
$user->addUserRole($role);
409412
}
410413

414+
$validation = $this->validator->validate($user);
415+
if (count($validation) > 0) {
416+
throw new ValidationFailedException($user, $validation);
417+
}
418+
411419
$this->em->persist($user);
412420
$this->em->flush();
413-
$this->dj->auditlog('user', $user->getUserid(), 'added');
421+
$this->dj->auditlog('user', $user->getUserid(), 'updated');
414422

415423
return $this->renderCreateData($request, $user, 'user', $user->getUserid());
416424
}
417425

418426
protected function getQueryBuilder(Request $request): QueryBuilder
419427
{
420428
$queryBuilder = $this->em->createQueryBuilder()
421-
->from(User::class, 'u')
422-
->select('u');
429+
->from(User::class, 'u')
430+
->select('u');
423431

424432
if ($request->query->has('team')) {
425433
$queryBuilder

webapp/src/DataTransferObject/AddUser.php

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,24 @@
44

55
use JMS\Serializer\Annotation as Serializer;
66
use OpenApi\Attributes as OA;
7+
use Symfony\Component\Validator\Constraints as Assert;
78

8-
#[OA\Schema(required: ['username', 'name', 'roles'])]
99
class AddUser
1010
{
11+
#[Assert\NotBlank]
12+
public string $username;
13+
#[Assert\NotBlank]
14+
public string $name;
15+
public ?string $id = null;
16+
public ?string $email = null;
17+
public ?string $ip = null;
18+
#[OA\Property(format: 'password')]
19+
public ?string $password = null;
20+
public ?bool $enabled = null;
21+
public ?string $teamId = null;
1122
/**
12-
* @param array<string> $roles
23+
* @var array<string>
1324
*/
14-
public function __construct(
15-
public readonly string $username,
16-
public readonly string $name,
17-
#[OA\Property(format: 'email', nullable: true)]
18-
public readonly ?string $email,
19-
#[OA\Property(nullable: true)]
20-
public readonly ?string $ip,
21-
#[OA\Property(format: 'password', nullable: true)]
22-
public readonly ?string $password,
23-
#[OA\Property(nullable: true)]
24-
public readonly ?bool $enabled,
25-
#[OA\Property(nullable: true)]
26-
public readonly ?string $teamId,
27-
#[Serializer\Type('array<string>')]
28-
public readonly array $roles,
29-
) {}
25+
#[Serializer\Type('array<string>')]
26+
public array $roles = [];
3027
}

webapp/src/DataTransferObject/UpdateUser.php

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,23 @@
22

33
namespace App\DataTransferObject;
44

5+
use JMS\Serializer\Annotation as Serializer;
56
use OpenApi\Attributes as OA;
67

7-
#[OA\Schema(required: ['id', 'username', 'name', 'roles'])]
8-
class UpdateUser extends AddUser
8+
class UpdateUser
99
{
10-
public function __construct(
11-
public readonly string $id,
12-
string $username,
13-
string $name,
14-
?string $email,
15-
?string $ip,
16-
?string $password,
17-
?bool $enabled,
18-
?string $teamId,
19-
array $roles
20-
) {
21-
parent::__construct($username, $name, $email, $ip, $password, $enabled, $teamId, $roles);
22-
}
10+
public ?string $id = null;
11+
public ?string $username = null;
12+
public ?string $name = null;
13+
public ?string $email = null;
14+
public ?string $ip = null;
15+
#[OA\Property(format: 'password')]
16+
public ?string $password = null;
17+
public ?bool $enabled = null;
18+
public ?string $teamId = null;
19+
/**
20+
* @var array<string>
21+
*/
22+
#[Serializer\Type('array<string>')]
23+
public array $roles = [];
2324
}

webapp/src/Entity/User.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<?php declare(strict_types=1);
2+
23
namespace App\Entity;
34

45
use App\Controller\API\AbstractRestController as ARC;
@@ -132,7 +133,7 @@ class User extends BaseApiEntity implements
132133
#[ORM\JoinTable(name: 'userrole')]
133134
#[ORM\JoinColumn(name: 'userid', referencedColumnName: 'userid', onDelete: 'CASCADE')]
134135
#[ORM\InverseJoinColumn(name: 'roleid', referencedColumnName: 'roleid', onDelete: 'CASCADE')]
135-
#[Assert\Count(min: 1)]
136+
#[Assert\Count(min: 1, minMessage: 'User should have at least {{ limit }} role')]
136137
#[Serializer\Exclude]
137138
private Collection $user_roles;
138139

@@ -439,9 +440,14 @@ public function getType(): ?string
439440
// Types allowed by the CCS Specs Contest API in order of most permissions to least.
440441
// Either key=>value where key is the DOMjudge role and value is the API account type or
441442
// only value, where both the DOMjudge role and API type are the same.
442-
$allowedTypes = ['admin', 'api_writer' => 'admin', 'api_reader' => 'admin',
443-
'jury' => 'judge', 'api_source_reader' => 'judge',
444-
'team'];
443+
$allowedTypes = [
444+
'admin',
445+
'api_writer' => 'admin',
446+
'api_reader' => 'admin',
447+
'jury' => 'judge',
448+
'api_source_reader' => 'judge',
449+
'team'
450+
];
445451
foreach ($allowedTypes as $role => $allowedType) {
446452
if (is_numeric($role)) {
447453
$role = $allowedType;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace App\EventListener;
4+
5+
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
6+
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
7+
use Symfony\Component\Validator\Exception\ValidationFailedException;
8+
use Symfony\Component\HttpFoundation\JsonResponse;
9+
10+
#[AsEventListener(event: 'kernel.exception', priority: 0)]
11+
class ValidationExceptionListener
12+
{
13+
public function __invoke(ExceptionEvent $event): void
14+
{
15+
$e = $event->getThrowable();
16+
17+
if (!$e instanceof ValidationFailedException) {
18+
return;
19+
}
20+
21+
$errors = [];
22+
foreach ($e->getViolations() as $violation) {
23+
$errors[] = [
24+
'property' => $violation->getPropertyPath(),
25+
'message' => $violation->getMessage(),
26+
'code' => $violation->getCode(),
27+
];
28+
}
29+
30+
$response = new JsonResponse([
31+
'title' => 'Bad Request',
32+
'status' => 400,
33+
'errors' => $errors,
34+
], 400);
35+
36+
$event->setResponse($response);
37+
}
38+
}

0 commit comments

Comments
 (0)