diff --git a/docs/api/v1/users/create.md b/docs/api/v1/users/create.md
new file mode 100644
index 00000000..a5a43197
--- /dev/null
+++ b/docs/api/v1/users/create.md
@@ -0,0 +1,172 @@
+# Create User
+
+Create a new user.
+
+**URL** : `/api/v1/users/create`
+
+**Method** : `POST`
+
+**Auth required** : YES
+
+**Request Body constraints**
+
+```json
+{
+ "name": "[string: username, alphanumeric, spaces, underscores and hyphens, max 64 chars]",
+ "display_name": "[string: max 255 chars]",
+ "email": "[string: valid email, max 255 chars]",
+ "password": "[string: max 255 chars]",
+ "is_admin": "[string or boolean: 'true', 'false', true, false, default false]"
+}
+```
+
+**Body example**
+
+```json
+{
+ "name": "user",
+ "display_name": "New User",
+ "email": "user@user.user",
+ "password": "password",
+ "is_admin": "false"
+}
+```
+
+## Success Response
+
+**Code** : `200 OK`
+
+**Content examples**
+
+```json
+{
+ "status": "success",
+ "data": {
+ "user_id": 5,
+ "user_name": "user"
+ },
+ "timestamp": "2026-09-23T15:01:33+01:00"
+}
+```
+
+## Error Response
+
+**Condition** : If 'X-Davis-API-Token' is not present or mismatched in headers.
+
+**Code** : `401 UNAUTHORIZED`
+
+**Content** :
+
+```json
+{
+ "message": "No API token provided",
+ "timestamp": "2026-09-23T15:01:33+01:00"
+}
+```
+
+or
+
+```json
+{
+ "message": "Invalid API token",
+ "timestamp": "2026-09-23T15:01:33+01:00"
+}
+```
+
+**Condition** : If request body contains invalid JSON.
+
+**Code** : `400 BAD REQUEST`
+
+**Content** :
+
+```json
+{
+ "status": "error",
+ "message": "Invalid JSON",
+ "timestamp": "2026-09-23T15:01:33+01:00"
+}
+```
+
+**Condition** : If 'name' parameter is invalid (not matching the regex or exceeds length).
+
+**Code** : `400 BAD REQUEST`
+
+**Content** :
+
+```json
+{
+ "status": "error",
+ "message": "Invalid Userame",
+ "timestamp": "2026-09-23T15:01:33+01:00"
+}
+```
+
+**Condition** : If 'display_name' parameter is invalid (null or empty string).
+
+**Code** : `400 BAD REQUEST`
+
+**Content** :
+
+```json
+{
+ "status": "error",
+ "message": "Invalid Display Name",
+ "timestamp": "2026-09-23T15:01:33+01:00"
+}
+```
+
+**Condition** : If 'email' parameter is invalid (null, empty string or invalid email).
+
+**Code** : `400 BAD REQUEST`
+
+**Content** :
+
+```json
+{
+ "status": "error",
+ "message": "Invalid Email",
+ "timestamp": "2026-09-23T15:01:33+01:00"
+}
+```
+
+**Condition** : If 'password' parameter is invalid (null or empty string).
+
+**Code** : `400 BAD REQUEST`
+
+**Content** :
+
+```json
+{
+ "status": "error",
+ "message": "Invalid Password",
+ "timestamp": "2026-09-23T15:01:33+01:00"
+}
+```
+
+**Condition** : If 'is_admin' parameter is invalid (not in [true, false, 'true', 'false']).
+
+**Code** : `400 BAD REQUEST`
+
+**Content** :
+
+```json
+{
+ "status": "error",
+ "message": "Invalid Is Admin",
+ "timestamp": "2026-09-23T15:01:33+01:00"
+}
+```
+
+**Condition** : If user with specified username already exists.
+
+**Code** : `400 BAD REQUEST`
+
+**Content** :
+
+```json
+{
+ "status": "error",
+ "message": "Usrname Already Exists",
+ "timestamp": "2026-09-23T15:01:33+01:00"
+}
+```
diff --git a/docs/api/v1/users/delete.md b/docs/api/v1/users/delete.md
new file mode 100644
index 00000000..28500151
--- /dev/null
+++ b/docs/api/v1/users/delete.md
@@ -0,0 +1,86 @@
+# Delete User
+
+Deletes a specific user.
+
+**URL** : `/api/v1/users/:user_id`
+
+**Method** : `DELETE`
+
+**Auth required** : YES
+
+**Params constraints**
+
+```
+:user_id -> "[user id as an int]",
+```
+
+**URL example**
+
+```
+/api/v1/users/1
+```
+
+## Success Response
+
+**Code** : `200 OK`
+
+**Content examples**
+
+```json
+{
+ "status": "success",
+ "timestamp": "2026-09-23T15:01:33+01:00"
+}
+```
+
+## Error Response
+
+**Condition** : If 'X-Davis-API-Token' is not present or mismatched in headers.
+
+**Code** : `401 UNAUTHORIZED`
+
+**Content** :
+
+```json
+{
+ "message": "No API token provided",
+ "timestamp": "2026-09-23T15:01:33+01:00"
+}
+```
+
+or
+
+```json
+{
+ "message": "Invalid API token",
+ "timestamp": "2026-09-23T15:01:33+01:00"
+}
+```
+
+**Condition** : If user is not found.
+
+**Code** : `404 NOT FOUND`
+
+**Content** :
+
+```json
+{
+ "status": "error",
+ "message": "User Not Found",
+ "timestamp": "2026-09-23T15:01:33+01:00"
+}
+```
+
+**Condition** : If any error arises while deleting, like if a principal was deleted without removing the corresponding user.
+
+**Code** : `500 INTERNAL SERVER ERROR`
+
+**Content** :
+
+```json
+{
+ "status": "error",
+ "message": "Failed to Delete User",
+ "timestamp": "2026-09-23T15:01:33+01:00"
+}
+```
diff --git a/src/Controller/Admin/UserController.php b/src/Controller/Admin/UserController.php
index 4943bfca..7d94d191 100644
--- a/src/Controller/Admin/UserController.php
+++ b/src/Controller/Admin/UserController.php
@@ -2,12 +2,7 @@
namespace App\Controller\Admin;
-use App\Entity\AddressBook;
-use App\Entity\Calendar;
-use App\Entity\CalendarInstance;
-use App\Entity\CalendarSubscription;
use App\Entity\Principal;
-use App\Entity\SchedulingObject;
use App\Entity\User;
use App\Form\UserType;
use App\Services\Utils;
@@ -37,23 +32,28 @@ public function users(ManagerRegistry $doctrine): Response
#[Route('/edit/{userId}', name: 'edit')]
public function userCreate(ManagerRegistry $doctrine, Utils $utils, Request $request, ?int $userId, TranslatorInterface $trans): Response
{
- if ($userId) {
+ $new_user = is_null($userId) ? false : true;
+ if ($new_user) {
$user = $doctrine->getRepository(User::class)->findOneById($userId);
if (!$user) {
throw $this->createNotFoundException('User not found');
}
$oldHash = $user->getPassword();
$principal = $doctrine->getRepository(Principal::class)->findOneByUri($user->getPrincipalUri());
+ if (!$principal) {
+ throw $this->createNotFoundException('Principal not found');
+ }
} else {
$user = new User();
- $principal = new Principal();
}
- $form = $this->createForm(UserType::class, $user, ['new' => !$userId]);
+ $form = $this->createForm(UserType::class, $user, ['new' => !$new_user]);
- $form->get('displayName')->setData($principal->getDisplayName());
- $form->get('email')->setData($principal->getEmail());
- $form->get('isAdmin')->setData($principal->getIsAdmin());
+ if ($new_user) {
+ $form->get('displayName')->setData($principal->getDisplayName());
+ $form->get('email')->setData($principal->getEmail());
+ $form->get('isAdmin')->setData($principal->getIsAdmin());
+ }
$form->handleRequest($request);
@@ -63,7 +63,7 @@ public function userCreate(ManagerRegistry $doctrine, Utils $utils, Request $req
$isAdmin = $form->get('isAdmin')->getData();
// Create password for user
- if ($userId && is_null($user->getPassword())) {
+ if ($new_user && is_null($user->getPassword())) {
// The user is not new and does not want to change its password
$user->setPassword($oldHash);
} else {
@@ -71,45 +71,18 @@ public function userCreate(ManagerRegistry $doctrine, Utils $utils, Request $req
$user->setPassword($hash);
}
- $entityManager = $doctrine->getManager();
-
- // If it's a new user, create default calendar and address book, and principal
- if (null === $user->getId()) {
- $principal->setUri($user->getPrincipalUri());
-
- $calendarInstance = new CalendarInstance();
- $calendar = new Calendar();
- $calendarInstance->setPrincipalUri($user->getPrincipalUri())
- ->setUri('default') // No risk of collision since unicity is guaranteed by the new user principal
- ->setDisplayName($trans->trans('default.calendar.title'))
- ->setDescription($trans->trans('default.calendar.description', ['user' => $displayName]))
- ->setCalendar($calendar);
-
- // Enable delegation by default
- $principalProxyRead = new Principal();
- $principalProxyRead->setUri($principal->getUri().Principal::READ_PROXY_SUFFIX)
- ->setIsMain(false);
- $entityManager->persist($principalProxyRead);
-
- $principalProxyWrite = new Principal();
- $principalProxyWrite->setUri($principal->getUri().Principal::WRITE_PROXY_SUFFIX)
- ->setIsMain(false);
- $entityManager->persist($principalProxyWrite);
-
- $addressbook = new AddressBook();
- $addressbook->setPrincipalUri($user->getPrincipalUri())
- ->setUri('default') // No risk of collision since unicity is guaranteed by the new user principal
- ->setDisplayName($trans->trans('default.addressbook.title'))
- ->setDescription($trans->trans('default.addressbook.description', ['user' => $displayName]));
- $entityManager->persist($calendarInstance);
- $entityManager->persist($addressbook);
- $entityManager->persist($principal);
+ // If it's a new user, create default objects, otherwise set the new values
+ if (!$new_user) {
+ $username = $form->get('username')->getData();
+ $user->setUsername($username);
+ $utils->createDefaultObjectsForUser($user, $displayName, $email, $isAdmin);
+ } else {
+ $principal->setDisplayName($displayName)
+ ->setEmail($email)
+ ->setIsAdmin($isAdmin);
}
- $principal->setDisplayName($displayName)
- ->setEmail($email)
- ->setIsAdmin($isAdmin);
-
+ $entityManager = $doctrine->getManager();
$entityManager->persist($user);
$entityManager->flush();
@@ -126,75 +99,21 @@ public function userCreate(ManagerRegistry $doctrine, Utils $utils, Request $req
}
#[Route('/delete/{userId}', name: 'delete', methods: ['POST'])]
- public function userDelete(ManagerRegistry $doctrine, Request $request, #[MapEntity(id: 'userId')] User $user, int $userId, TranslatorInterface $trans): Response
+ public function userDelete(ManagerRegistry $doctrine, Request $request, #[MapEntity(id: 'userId')] User $user, int $userId, TranslatorInterface $trans, Utils $utils): Response
{
if (!$this->isCsrfTokenValid('admin_action', $request->getPayload()->getString('_token'))) {
throw $this->createAccessDeniedException('Invalid CSRF token.');
}
$entityManager = $doctrine->getManager();
- $entityManager->remove($user);
-
- $principal = $doctrine->getRepository(Principal::class)->findOneByUri($user->getPrincipalUri());
- $principalProxyRead = $doctrine->getRepository(Principal::class)->findOneByUri($principal->getUri().Principal::READ_PROXY_SUFFIX);
- $principalProxyWrite = $doctrine->getRepository(Principal::class)->findOneByUri($principal->getUri().Principal::WRITE_PROXY_SUFFIX);
-
- $entityManager->remove($principal);
-
- if ($principalProxyRead) {
- $entityManager->remove($principalProxyRead);
- }
-
- if ($principalProxyWrite) {
- $entityManager->remove($principalProxyWrite);
- }
-
- $principalUri = $user->getPrincipalUri();
-
- // Remove calendars and addressbooks
- $calendars = $doctrine->getRepository(CalendarInstance::class)->findByPrincipalUriWithCalendars($principalUri);
- foreach ($calendars ?? [] as $instance) {
- // We're only removing the calendar objects / changes / and calendar if the deleted user is an owner,
- // which means that the underlying calendar instance should not have another principal as owner.
- $hasDifferentOwner = $doctrine->getRepository(CalendarInstance::class)->hasDifferentOwner($instance->getCalendar()->getId(), $principalUri);
- if (!$hasDifferentOwner) {
- foreach ($instance->getCalendar()->getObjects() ?? [] as $object) {
- $entityManager->remove($object);
- }
- foreach ($instance->getCalendar()->getChanges() ?? [] as $change) {
- $entityManager->remove($change);
- }
- // We need to remove the shared versions of this calendar, too
- foreach ($instance->getCalendar()->getInstances() ?? [] as $instances) {
- $entityManager->remove($instances);
- }
- $entityManager->remove($instance->getCalendar());
- }
- $entityManager->remove($instance);
- }
- $calendarsSubscriptions = $doctrine->getRepository(CalendarSubscription::class)->findByPrincipalUri($principalUri);
- foreach ($calendarsSubscriptions ?? [] as $subscription) {
- $entityManager->remove($subscription);
- }
- $schedulingObjects = $doctrine->getRepository(SchedulingObject::class)->findByPrincipalUri($principalUri);
- foreach ($schedulingObjects ?? [] as $object) {
- $entityManager->remove($object);
- }
-
- $addressbooks = $doctrine->getRepository(AddressBook::class)->findByPrincipalUri($principalUri);
- foreach ($addressbooks ?? [] as $addressbook) {
- foreach ($addressbook->getCards() ?? [] as $card) {
- $entityManager->remove($card);
- }
- foreach ($addressbook->getChanges() ?? [] as $change) {
- $entityManager->remove($change);
- }
- $entityManager->remove($addressbook);
+ try {
+ $utils->deleteUser($user);
+ $entityManager->flush();
+ $this->addFlash('success', $trans->trans('user.deleted'));
+ } catch (\Exception $e) {
+ $this->addFlash('error', $trans->trans('user.deleted.error'));
}
- $entityManager->flush();
- $this->addFlash('success', $trans->trans('user.deleted'));
-
return $this->redirectToRoute('user_index');
}
diff --git a/src/Controller/Api/ApiController.php b/src/Controller/Api/ApiController.php
index 597b08a1..5e84d117 100644
--- a/src/Controller/Api/ApiController.php
+++ b/src/Controller/Api/ApiController.php
@@ -14,6 +14,9 @@
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
+use Symfony\Component\Validator\Constraints as Assert;
+use Symfony\Component\Validator\Validator\ValidatorInterface;
+use Symfony\Contracts\Translation\TranslatorInterface;
#[Route('/api/v1', name: 'api_v1_')]
class ApiController extends AbstractController
@@ -146,6 +149,93 @@ public function getUserDetails(Request $request, ManagerRegistry $doctrine, int
return $this->json($response, 200);
}
+ /**
+ * Creates a new user.
+ *
+ * @param Request $request The HTTP POST request
+ *
+ * @return JsonResponse A JSON response containing the user details if successfull
+ */
+ #[Route('/users/create', name: 'user_create', methods: ['POST'])]
+ public function createUser(Request $request, ManagerRegistry $doctrine, TranslatorInterface $trans, ValidatorInterface $validator, Utils $utils): JsonResponse
+ {
+ // Parse JSON body
+ $data = json_decode($request->getContent(), true);
+ if (JSON_ERROR_NONE !== json_last_error()) {
+ return $this->json(['status' => 'error', 'message' => 'Invalid JSON', 'timestamp' => $this->getTimestamp()], 400);
+ }
+
+ $username = $data['name'] ?? null;
+ if (is_null($username) || !$this->validateUsername($username)) {
+ return $this->json(['status' => 'error', 'message' => 'Invalid Username', 'timestamp' => $this->getTimestamp()], 400);
+ }
+ $display_name = $data['display_name'] ?? null;
+ if (empty($display_name)) {
+ return $this->json(['status' => 'error', 'message' => 'Invalid Display Name', 'timestamp' => $this->getTimestamp()], 400);
+ }
+ $email = $data['email'] ?? null;
+ if (empty($email) || count($validator->validate($email, new Assert\Email())) > 0) {
+ return $this->json(['status' => 'error', 'message' => 'Invalid Email', 'timestamp' => $this->getTimestamp()], 400);
+ }
+ $password = $data['password'] ?? null;
+ if (is_null($password)) {
+ return $this->json(['status' => 'error', 'message' => 'Invalid Password', 'timestamp' => $this->getTimestamp()], 400);
+ }
+ $isAdmin = $data['is_admin'] ?? false;
+ if (!in_array($isAdmin, [true, false, 'true', 'false'], true)) {
+ return $this->json(['status' => 'error', 'message' => 'Invalid Is Admin', 'timestamp' => $this->getTimestamp()], 400);
+ }
+
+ $existingUsername = $doctrine->getRepository(User::class)->findOneByUsername($username);
+ if ($existingUsername) {
+ return $this->json(['status' => 'error', 'message' => 'Username Already Exists', 'timestamp' => $this->getTimestamp()], 400);
+ }
+
+ $hashed_password = password_hash($password, PASSWORD_DEFAULT);
+ $user = $utils->createUserWithDefaultObjects($username, $display_name, $email, $hashed_password, (true === $isAdmin || 'true' === $isAdmin) ? true : false);
+
+ $entityManager = $doctrine->getManager();
+ $entityManager->flush();
+
+ $response = [
+ 'status' => 'success',
+ 'data' => [
+ 'user_id' => $user->getId(),
+ 'user_name' => $user->getUsername(),
+ ],
+ 'timestamp' => $this->getTimestamp(),
+ ];
+
+ return $this->json($response, 201);
+ }
+
+ /**
+ * Deletes a specific user.
+ *
+ * @param int $userId The ID of the user to delete
+ *
+ * @return JsonResponse A JSON response indicating the success or failure of the operation
+ */
+ #[Route('/users/{userId}', name: 'user_delete', methods: ['DELETE'], requirements: ['userId' => '\d+'])]
+ public function deleteUser(int $userId, ManagerRegistry $doctrine, Utils $utils): JsonResponse
+ {
+ $user = $this->resolveUser($doctrine, $userId);
+ if (!$user) {
+ return $this->json(['status' => 'error', 'message' => 'User Not Found', 'timestamp' => $this->getTimestamp()], 404);
+ }
+
+ try {
+ $utils->deleteUser($user);
+ } catch (\Exception $e) {
+ return $this->json(['status' => 'error', 'message' => 'Error while Deleting User', 'timestamp' => $this->getTimestamp()], 500);
+ }
+
+ $entityManager = $doctrine->getManager();
+ $entityManager->flush();
+
+ return $this->json(['status' => 'success', 'timestamp' => $this->getTimestamp()], 200);
+ }
+
/**
* Retrieves a list of calendars for a specific user, including user calendars, shared calendars, and subscriptions.
*
diff --git a/src/Services/Utils.php b/src/Services/Utils.php
index 38aca4bd..90a52a1c 100644
--- a/src/Services/Utils.php
+++ b/src/Services/Utils.php
@@ -5,7 +5,9 @@
use App\Entity\AddressBook;
use App\Entity\Calendar;
use App\Entity\CalendarInstance;
+use App\Entity\CalendarSubscription;
use App\Entity\Principal;
+use App\Entity\SchedulingObject;
use App\Entity\User;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -58,6 +60,18 @@ public static function isValidUsername(?string $username): bool
}
public function createPasswordlessUserWithDefaultObjects(string $username, string $displayName, string $email)
+ {
+ // Set the password to a random string (but hashed beforehand)
+ $password = substr(bin2hex(random_bytes(256)), 0, 48);
+ $hash = password_hash($password, PASSWORD_DEFAULT);
+
+ return $this->createUserWithDefaultObjects($username, $displayName, $email, $hash, false);
+ }
+
+ /**
+ * Return the new user, persisted in the database.
+ */
+ public function createUserWithDefaultObjects(string $username, string $displayName, string $email, string $hashed_password, bool $isAdmin)
{
if (!self::isValidUsername($username)) {
throw new \InvalidArgumentException(sprintf('Refusing to create the user "%s": a username may only contain letters, digits and the characters _ . @ + \' -', $username));
@@ -65,18 +79,29 @@ public function createPasswordlessUserWithDefaultObjects(string $username, strin
$user = new User();
$user->setUsername($username);
+ $user->setPassword($hashed_password);
- // Set the password to a random string (but hashed beforehand)
- $randomBytes = substr(bin2hex(random_bytes(256)), 0, 48);
- $hash = password_hash($randomBytes, PASSWORD_DEFAULT);
- $user->setPassword($hash);
+ $this->createDefaultObjectsForUser($user, $displayName, $email, $isAdmin);
+
+ $em = $this->doctrine->getManager();
+ $em->persist($user);
- // Create principal, default calendar and addressbook
+ return $user;
+ }
+
+ /**
+ * Create the default objects for a new user: principal, default calendar
+ * and addressbook, all persisted in the database.
+ *
+ * Return the new principal
+ */
+ public function createDefaultObjectsForUser(User $user, string $displayName, string $email, bool $isAdmin)
+ {
$principal = new Principal();
$principal->setUri($user->getPrincipalUri())
->setDisplayName($displayName)
->setEmail($email)
- ->setIsAdmin(false);
+ ->setIsAdmin($isAdmin);
$calendarInstance = new CalendarInstance();
$calendar = new Calendar();
@@ -108,6 +133,74 @@ public function createPasswordlessUserWithDefaultObjects(string $username, strin
$em->persist($calendarInstance);
$em->persist($addressbook);
$em->persist($principal);
- $em->persist($user);
+
+ return $principal;
+ }
+
+ public function deleteUser(User $user)
+ {
+ $entityManager = $this->doctrine->getManager();
+ $entityManager->remove($user);
+
+ $principal = $this->doctrine->getRepository(Principal::class)->findOneByUri($user->getPrincipalUri());
+ if (!$principal) {
+ throw new \Exception('Principal is null');
+ }
+
+ $principalProxyRead = $this->doctrine->getRepository(Principal::class)->findOneByUri($principal->getUri().Principal::READ_PROXY_SUFFIX);
+ $principalProxyWrite = $this->doctrine->getRepository(Principal::class)->findOneByUri($principal->getUri().Principal::WRITE_PROXY_SUFFIX);
+
+ $entityManager->remove($principal);
+
+ if ($principalProxyRead) {
+ $entityManager->remove($principalProxyRead);
+ }
+
+ if ($principalProxyWrite) {
+ $entityManager->remove($principalProxyWrite);
+ }
+
+ $principalUri = $user->getPrincipalUri();
+
+ // Remove calendars and addressbooks
+ $calendars = $this->doctrine->getRepository(CalendarInstance::class)->findByPrincipalUriWithCalendars($principalUri);
+ foreach ($calendars ?? [] as $instance) {
+ // We're only removing the calendar objects / changes / and calendar if the deleted user is an owner,
+ // which means that the underlying calendar instance should not have another principal as owner.
+ $hasDifferentOwner = $this->doctrine->getRepository(CalendarInstance::class)->hasDifferentOwner($instance->getCalendar()->getId(), $principalUri);
+ if (!$hasDifferentOwner) {
+ foreach ($instance->getCalendar()->getObjects() ?? [] as $object) {
+ $entityManager->remove($object);
+ }
+ foreach ($instance->getCalendar()->getChanges() ?? [] as $change) {
+ $entityManager->remove($change);
+ }
+ // We need to remove the shared versions of this calendar, too
+ foreach ($instance->getCalendar()->getInstances() ?? [] as $instances) {
+ $entityManager->remove($instances);
+ }
+ $entityManager->remove($instance->getCalendar());
+ }
+ $entityManager->remove($instance);
+ }
+ $calendarsSubscriptions = $this->doctrine->getRepository(CalendarSubscription::class)->findByPrincipalUri($principalUri);
+ foreach ($calendarsSubscriptions ?? [] as $subscription) {
+ $entityManager->remove($subscription);
+ }
+ $schedulingObjects = $this->doctrine->getRepository(SchedulingObject::class)->findByPrincipalUri($principalUri);
+ foreach ($schedulingObjects ?? [] as $object) {
+ $entityManager->remove($object);
+ }
+
+ $addressbooks = $this->doctrine->getRepository(AddressBook::class)->findByPrincipalUri($principalUri);
+ foreach ($addressbooks ?? [] as $addressbook) {
+ foreach ($addressbook->getCards() ?? [] as $card) {
+ $entityManager->remove($card);
+ }
+ foreach ($addressbook->getChanges() ?? [] as $change) {
+ $entityManager->remove($change);
+ }
+ $entityManager->remove($addressbook);
+ }
}
}
diff --git a/tests/Functional/Controllers/ApiControllerTest.php b/tests/Functional/Controllers/ApiControllerTest.php
index dc559646..03161834 100644
--- a/tests/Functional/Controllers/ApiControllerTest.php
+++ b/tests/Functional/Controllers/ApiControllerTest.php
@@ -2,6 +2,7 @@
namespace App\Tests\Functional;
+use App\Entity\User;
use App\Entity\Calendar;
use App\Entity\CalendarInstance;
use App\Entity\CalendarObject;
@@ -210,6 +211,290 @@ public function testUserDetails(): void
$this->assertStringEqualsStringIgnoringLineEndings($username, $data['data']['username']);
}
+ /*
+ * Test the user creation endpoint
+ */
+ public function testUserCreate(): void
+ {
+ $client = static::createClient();
+ $em = static::getContainer()->get('doctrine.orm.entity_manager');
+
+ // Create user API request with JSON body
+ $payload = [
+ 'name' => 'user',
+ 'display_name' => 'user display name',
+ 'email' => 'user@email.com',
+ 'password' => 'password',
+ 'is_admin' => false,
+ ];
+
+ $client->request('POST', '/api/v1/users/create', [], [], [
+ 'HTTP_ACCEPT' => 'application/json',
+ 'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],
+ 'CONTENT_TYPE' => 'application/json',
+ ], json_encode($payload));
+ $this->assertResponseIsSuccessful();
+ $this->assertResponseHeaderSame('Content-Type', 'application/json');
+
+ $data = json_decode($client->getResponse()->getContent(), true);
+
+ // Check if the user was created
+ $user = $em->getRepository(User::class)->findOneByUsername('user');
+ $this->assertNotNull($user, 'The user was not created');
+
+ // Check if user details are correct
+ $this->assertArrayHasKey('user_id', $data['data']);
+ $this->assertEquals($user->getId(), $data['data']['user_id']);
+ $this->assertArrayHasKey('user_name', $data['data']);
+ $this->assertEquals($user->getUsername(), $data['data']['user_name']);
+ $this->assertTrue(password_verify('password', $user->getPassword()));
+
+ // Check if the principal was created
+ $principal = $em->getRepository(Principal::class)->findOneByUri($user->getPrincipalUri());
+ $this->assertNotNull($principal, 'The principal was not created');
+
+ // Check if principal details are correct
+ $this->assertEquals('user display name', $principal->getDisplayName());
+ $this->assertEquals('user@email.com', $principal->getEmail());
+ $this->assertFalse($principal->getIsAdmin());
+ }
+
+ /*
+ * Test the user creation endpoint if is_admin is passed as string
+ */
+ public function testUserCreateIfIsAdminIsString(): void
+ {
+ $client = static::createClient();
+ $em = static::getContainer()->get('doctrine.orm.entity_manager');
+
+ // Create user API request with JSON body
+ $payload = [
+ 'name' => 'user',
+ 'display_name' => 'user display name',
+ 'email' => 'user@email.com',
+ 'password' => 'password',
+ 'is_admin' => "true",
+ ];
+
+ $client->request('POST', '/api/v1/users/create', [], [], [
+ 'HTTP_ACCEPT' => 'application/json',
+ 'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],
+ 'CONTENT_TYPE' => 'application/json',
+ ], json_encode($payload));
+ $this->assertResponseIsSuccessful();
+ $this->assertResponseHeaderSame('Content-Type', 'application/json');
+
+ $data = json_decode($client->getResponse()->getContent(), true);
+
+ // Check if the user was created
+ $user = $em->getRepository(User::class)->findOneByUsername('user');
+ $this->assertNotNull($user, 'The user was not created');
+
+ // Check if user details are correct
+ $this->assertArrayHasKey('user_id', $data['data']);
+ $this->assertEquals($user->getId(), $data['data']['user_id']);
+ $this->assertArrayHasKey('user_name', $data['data']);
+ $this->assertEquals($user->getUsername(), $data['data']['user_name']);
+ $this->assertTrue(password_verify('password', $user->getPassword()));
+
+ // Check if the principal was created
+ $principal = $em->getRepository(Principal::class)->findOneByUri($user->getPrincipalUri());
+ $this->assertNotNull($principal, 'The principal was not created');
+
+ // Check if principal details are correct
+ $this->assertEquals('user display name', $principal->getDisplayName());
+ $this->assertEquals('user@email.com', $principal->getEmail());
+ $this->assertTrue($principal->getIsAdmin());
+ }
+
+ /*
+ * Test that the user creation endpoint fails when passing invalid email
+ */
+ public function testUserCreateFailInvalidEmail(): void
+ {
+ $client = static::createClient();
+ $em = static::getContainer()->get('doctrine.orm.entity_manager');
+
+ // Create user API request with JSON body
+ $payload = [
+ 'name' => 'user',
+ 'display_name' => 'user display name',
+ 'email' => 'email',
+ 'password' => 'password',
+ 'is_admin' => false,
+ ];
+
+ $client->request('POST', '/api/v1/users/create', [], [], [
+ 'HTTP_ACCEPT' => 'application/json',
+ 'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],
+ 'CONTENT_TYPE' => 'application/json',
+ ], json_encode($payload));
+
+ $this->assertResponseStatusCodeSame(400);
+ $this->assertResponseHeaderSame('Content-Type', 'application/json');
+
+ $data = json_decode($client->getResponse()->getContent(), true);
+ $this->assertEquals('error', $data['status']);
+ $this->assertStringContainsString('Invalid Email', $data['message']);
+ }
+
+ /*
+ * Test that the user creation endpoint fails when passing invalid is_admin
+ */
+ public function testUserCreateFailInvalidIsAdmin(): void
+ {
+ $client = static::createClient();
+ $em = static::getContainer()->get('doctrine.orm.entity_manager');
+
+ // Create user API request with JSON body
+ $payload = [
+ 'name' => 'user',
+ 'display_name' => 'user display name',
+ 'email' => 'user@email.com',
+ 'password' => 'password',
+ 'is_admin' => "notfalse",
+ ];
+
+ $client->request('POST', '/api/v1/users/create', [], [], [
+ 'HTTP_ACCEPT' => 'application/json',
+ 'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],
+ 'CONTENT_TYPE' => 'application/json',
+ ], json_encode($payload));
+
+ $this->assertResponseStatusCodeSame(400);
+ $this->assertResponseHeaderSame('Content-Type', 'application/json');
+
+ $data = json_decode($client->getResponse()->getContent(), true);
+ $this->assertEquals('error', $data['status']);
+ $this->assertStringContainsString('Invalid Is Admin', $data['message']);
+ }
+
+ /*
+ * Test that the user creation endpoint fails when the user already exists
+ */
+ public function testUserCreateFailWhenUserAlreadyExists(): void
+ {
+ $client = static::createClient();
+ $em = static::getContainer()->get('doctrine.orm.entity_manager');
+
+ // Get username from existing user lists
+ $username = $this->getUserUsername($client, 0);
+
+ // Create user API request with JSON body
+ $payload = [
+ 'name' => $username,
+ 'display_name' => 'user display name',
+ 'email' => 'user@email.com',
+ 'password' => 'password',
+ 'is_admin' => false,
+ ];
+
+ $client->request('POST', '/api/v1/users/create', [], [], [
+ 'HTTP_ACCEPT' => 'application/json',
+ 'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],
+ 'CONTENT_TYPE' => 'application/json',
+ ], json_encode($payload));
+
+ $this->assertResponseStatusCodeSame(400);
+ $this->assertResponseHeaderSame('Content-Type', 'application/json');
+
+ $data = json_decode($client->getResponse()->getContent(), true);
+ $this->assertEquals('error', $data['status']);
+ $this->assertStringContainsString('Username Already Exists', $data['message']);
+ }
+
+ /*
+ * Test the user deletion endpoint
+ */
+ public function testUserDelete(): void
+ {
+ $client = static::createClient();
+ $em = static::getContainer()->get('doctrine.orm.entity_manager');
+
+ // Get userId, username and principal from existing user
+ $userId = $this->getUserId($client, 0);
+ $username = $this->getUserUsername($client, 0);
+ $user = $em->getRepository(User::class)->findOneByUsername($username);
+ $principal = $em->getRepository(Principal::class)->findOneByUri($user->getPrincipalUri());
+
+ $client->request('DELETE', '/api/v1/users/'.$userId, [], [], [
+ 'HTTP_ACCEPT' => 'application/json',
+ 'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],
+ 'CONTENT_TYPE' => 'application/json',
+ ]);
+ $this->assertResponseIsSuccessful();
+ $this->assertResponseHeaderSame('Content-Type', 'application/json');
+
+ $data = json_decode($client->getResponse()->getContent(), true);
+
+ // Check if the user was deleted
+ $user = $em->getRepository(User::class)->findOneByUsername($username);
+ $this->assertNull($user, 'The user was not deleted');
+
+ // Check if the principal was deleted
+ $principal = $em->getRepository(Principal::class)->findOneByUri($principal->getUri());
+ $this->assertNull($principal, 'The principal was not deleted');
+ }
+
+ /*
+ * Test that the user deletion endpoint fails when the user does not exist
+ */
+ public function testUserDeleteFailUserDoesNotExists(): void
+ {
+ $client = static::createClient();
+ $em = static::getContainer()->get('doctrine.orm.entity_manager');
+
+ // Get userId, username and principal from existing user
+ $userId = $this->getUserId($client, 0);
+ $username = $this->getUserUsername($client, 0);
+ $user = $em->getRepository(User::class)->findOneByUsername($username);
+ $em->remove($user);
+ $em->flush();
+
+ $client->request('DELETE', '/api/v1/users/'.$userId, [], [], [
+ 'HTTP_ACCEPT' => 'application/json',
+ 'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],
+ 'CONTENT_TYPE' => 'application/json',
+ ]);
+
+ $this->assertResponseStatusCodeSame(404);
+ $this->assertResponseHeaderSame('Content-Type', 'application/json');
+
+ $data = json_decode($client->getResponse()->getContent(), true);
+ $this->assertEquals('error', $data['status']);
+ $this->assertStringContainsString('User Not Found', $data['message']);
+ }
+
+ /*
+ * Test that the user deletion endpoint fails when the user does not have a principal
+ */
+ public function testUserDeleteFailIfNoPrincipal(): void
+ {
+ $client = static::createClient();
+ $em = static::getContainer()->get('doctrine.orm.entity_manager');
+
+ // Get userId, username and principal from existing user
+ $userId = $this->getUserId($client, 0);
+ $username = $this->getUserUsername($client, 0);
+ $user = $em->getRepository(User::class)->findOneByUsername($username);
+ $principal = $em->getRepository(Principal::class)->findOneByUri($user->getPrincipalUri());
+ $em->remove($principal);
+ $em->flush();
+
+ $client->request('DELETE', '/api/v1/users/'.$userId, [], [], [
+ 'HTTP_ACCEPT' => 'application/json',
+ 'HTTP_X_DAVIS_API_TOKEN' => $_ENV['API_KEY'],
+ 'CONTENT_TYPE' => 'application/json',
+ ]);
+
+ $this->assertResponseStatusCodeSame(500);
+ $this->assertResponseHeaderSame('Content-Type', 'application/json');
+
+ $data = json_decode($client->getResponse()->getContent(), true);
+ $this->assertEquals('error', $data['status']);
+ $this->assertStringContainsString('Error while Deleting User', $data['message']);
+ }
+
/*
* Test the user calendars list endpoint
*/
diff --git a/translations/messages+intl-icu.de.xlf b/translations/messages+intl-icu.de.xlf
index 3f9a28c7..99e4b126 100644
--- a/translations/messages+intl-icu.de.xlf
+++ b/translations/messages+intl-icu.de.xlf
@@ -261,6 +261,10 @@
user.deleted
Benutzer erfolgreich gelöscht
+
+ user.deleted.error
+ Fehler beim Löschen dieses Benutzers
+
calendar.saved
Kalender erfolgreich gespeichert
@@ -585,7 +589,7 @@
calendar.share_access.3
lesen / schreiben
-
+
calendar.public
öffentlich
diff --git a/translations/messages+intl-icu.en.xlf b/translations/messages+intl-icu.en.xlf
index 4c6e1cd4..8c693210 100644
--- a/translations/messages+intl-icu.en.xlf
+++ b/translations/messages+intl-icu.en.xlf
@@ -261,6 +261,10 @@
user.deleted
User deleted successfully
+
+ user.deleted.error
+ Error while deleting this user
+
calendar.saved
Calendar saved successfully
@@ -585,7 +589,7 @@
calendar.share_access.3
read / write
-
+
calendar.public
public
diff --git a/translations/messages+intl-icu.fr.xliff b/translations/messages+intl-icu.fr.xliff
index fad1aede..753a4e6d 100644
--- a/translations/messages+intl-icu.fr.xliff
+++ b/translations/messages+intl-icu.fr.xliff
@@ -261,6 +261,10 @@
user.deleted
Utilisateur supprimé avec succès
+
+ user.deleted.error
+ Erreur lors de la suppression de cet utilisateur
+
calendar.saved
Calendrier enregistré avec succès
@@ -585,7 +589,7 @@
calendar.share_access.3
lecture / écriture
-
+
calendar.share_access.10
public