From daae2398c8486615b74f76f5ad4d37be078c925a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?L=C3=A9o=20Banno-Cloutier?=
 <leo.banno-cloutier@savoirfairelinux.com>
Date: Tue, 15 Aug 2023 11:21:53 -0400
Subject: [PATCH] refactor: minor changes

- jams-react-client: fix request spam on devices tab
- don't send hashed password in response when creating a user

Change-Id: I3c576003d57ef84ff523ebbf14ac611793a8b750
---
 .dockerignore                                 | 11 +++
 Dockerfile                                    | 14 ++--
 generate-versions.py                          | 51 +++++++++---
 .../ca/workers/csr/builders/UserBuilder.java  | 68 ++++++++--------
 .../requests/DeviceRevocationRequest.java     | 39 ----------
 .../adapters/ContactAdapter.java              |  1 -
 .../net/jami/jams/common/utils/X509Utils.java |  2 +
 .../src/components/Devices/Devices.tsx        | 12 ++-
 .../src/components/Grid/GridItem.tsx          | 21 +++--
 jams-react-client/src/globalUrls.tsx          |  2 +-
 .../src/views/Blueprint/PolicyDataContext.tsx |  2 +-
 .../views/Blueprint/policyData.constants.tsx  | 17 +++-
 .../src/views/Blueprint/updatePolicyData.tsx  |  4 +-
 .../src/views/Contacts/Contacts.tsx           |  1 -
 .../core/workflows/RegisterDeviceFlow.java    |  5 +-
 .../api/admin/contacts/ContactServlet.java    | 48 ++----------
 .../api/admin/update/SubscriptionServlet.java | 29 +++----
 .../auth/directory/DirectoryEntryServlet.java | 71 ++++++++---------
 .../api/image/FileHandlerServlet.java         |  8 +-
 .../server/servlets/filters/FilterUtils.java  |  2 -
 .../connector/service/UserProfileService.java | 78 ++++++++++---------
 21 files changed, 226 insertions(+), 260 deletions(-)
 delete mode 100644 jams-common/src/main/java/net/jami/jams/common/objects/requests/DeviceRevocationRequest.java

diff --git a/.dockerignore b/.dockerignore
index c8561256..99519023 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,5 +1,7 @@
 .git
 .gitignore
+.gitmodules
+.gitreview
 
 Dockerfile
 .dockerignore
@@ -7,4 +9,13 @@ Dockerfile
 **/node_modules
 **/target
 
+.gradle
+.idea
+.vscode
+
+README.md
+derby.log
+
+extras
 jams
+jams-server/src/main/resources/webapp
diff --git a/Dockerfile b/Dockerfile
index ffd06171..654a2500 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -22,9 +22,9 @@ COPY jams-server/pom.xml jams-server/pom.xml
 # RUN mvn dependency:resolve --fail-never
 # RUN mvn dependency:go-offline --fail-never -am
 RUN mvn install -pl ad-connector,authentication-module,datastore,jami-dht,jami-nameserver,jams-ca,jams-common,jams-launcher,ldap-connector,jams-server -am -DskipTests
-COPY . .
 
 FROM build as dev
+COPY . .
 WORKDIR /app
 RUN mkdir -p /app/jams-server/src/main/resources/webapp \
     && echo '<h1>Dev build, this is a placeholder index.html. Please connect to <a href="http://localhost:3000">localhost:3000</a> instead</h1>' \
@@ -38,18 +38,16 @@ CMD java -jar jams-server.jar 8080 \
 
 FROM build as prod
 WORKDIR /app/jams-react-client
+COPY jams-react-client .
 RUN npm run build
-RUN mkdir -p ../jams-server/src/main/resources/webapp \
-    && mv build/* ../jams-server/src/main/resources/webapp
 WORKDIR /app
+COPY . .
+RUN mkdir -p jams-server/src/main/resources/webapp \
+    && mv jams-react-client/build/* jams-server/src/main/resources/webapp
 RUN mvn package
 
 ENV JAMS_VERSION=3.5
-RUN python3 generate-versions.py net.jami.jams.ca.JamsCA $JAMS_VERSION libs/cryptoengine.jar
-RUN python3 generate-versions.py net.jami.jams.authmodule.UserAuthenticationModule $JAMS_VERSION libs/authentication-module.jar
-RUN python3 generate-versions.py net.jami.jams.server.Server $JAMS_VERSION jams-server.jar
-RUN python3 generate-versions.py net.jami.jams.ad.connector.ADConnector $JAMS_VERSION libs/ad-connector.jar
-RUN python3 generate-versions.py net.jami.jams.ldap.connector.LDAPConnector $JAMS_VERSION libs/ldap-connector.jar
+RUN python3 generate-versions.py $JAMS_VERSION
 
 RUN ./build-doc.sh
 WORKDIR /app/jams
diff --git a/generate-versions.py b/generate-versions.py
index a460bdc1..b28d4e16 100644
--- a/generate-versions.py
+++ b/generate-versions.py
@@ -4,6 +4,9 @@ import sys
 from pathlib import Path
 
 
+here = Path(__file__).parent
+
+
 def read_versions(versions_file: Path) -> dict:
     if not versions_file.exists():
         return {}
@@ -12,31 +15,57 @@ def read_versions(versions_file: Path) -> dict:
         return json.load(f)
 
 
+def get_md5_hash(filename: str) -> str:
+    md5_hash = hashlib.md5()
 
-def main() -> None:
-    here = Path(__file__).parent
-    versions_file = here / "versions.json"
+    with open(here / "jams" / filename, "rb") as jar:
+        md5_hash.update(jar.read())
 
-    class_name = sys.argv[1]
-    version = sys.argv[2]
-    filename = sys.argv[3]
+    return md5_hash.hexdigest()
 
-    versions = read_versions(versions_file)
 
-    md5_hash = hashlib.md5()
+def generate_versions(class_name: str, version: str, filename: str) -> None:
+    versions_file = here / "versions.json"
 
-    with open(here / "jams" / filename, "rb") as jar:
-        md5_hash.update(jar.read())
+    versions = read_versions(versions_file)
 
     versions[class_name] = {
         "version": version,
         "filename": filename,
-        "md5": md5_hash.hexdigest(),
+        "md5": get_md5_hash(filename),
     }
 
     with open(versions_file, "w") as f:
         json.dump(versions, f, indent=4)
 
 
+def main() -> None:
+    if len(sys.argv) == 2:
+        version = sys.argv[1]
+
+        class_to_filename = {
+            "net.jami.jams.ca.JamsCA": "libs/cryptoengine.jar",
+            "net.jami.jams.authmodule.UserAuthenticationModule": "libs/authentication-module.jar",
+            "net.jami.jams.server.Server": "jams-server.jar",
+            "net.jami.jams.ad.connector.ADConnector": "libs/ad-connector.jar",
+            "net.jami.jams.ldap.connector.LDAPConnector": "libs/ldap-connector.jar",
+        }
+
+        for class_name, filename in class_to_filename.items():
+            generate_versions(class_name, version, filename)
+
+        return
+
+    if len(sys.argv) != 4:
+        print(f"Usage: {sys.argv[0]} (<version> | <class_name> <version> <filename>)")
+        sys.exit(1)
+
+    class_name = sys.argv[1]
+    version = sys.argv[2]
+    filename = sys.argv[3]
+
+    generate_versions(class_name, version, filename)
+
+
 if __name__ == "__main__":
     main()
diff --git a/jams-ca/src/main/java/net/jami/jams/ca/workers/csr/builders/UserBuilder.java b/jams-ca/src/main/java/net/jami/jams/ca/workers/csr/builders/UserBuilder.java
index 86f7437f..3d3827fd 100644
--- a/jams-ca/src/main/java/net/jami/jams/ca/workers/csr/builders/UserBuilder.java
+++ b/jams-ca/src/main/java/net/jami/jams/ca/workers/csr/builders/UserBuilder.java
@@ -44,6 +44,8 @@ import java.security.KeyPair;
 import java.security.KeyPairGenerator;
 import java.security.MessageDigest;
 import java.security.SecureRandom;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
 import java.util.Date;
 
 @Slf4j
@@ -51,7 +53,6 @@ public class UserBuilder {
 
     public static User generateUser(User user) {
         try {
-            long now = System.currentTimeMillis();
             KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
             keyPairGenerator.initialize(4096);
             KeyPair keyPair = keyPairGenerator.generateKeyPair();
@@ -60,23 +61,14 @@ public class UserBuilder {
                     MessageDigest.getInstance(MessageDigestAlgorithms.SHA_1)
                             .digest(keyPair.getPublic().getEncoded());
             user.getX509Fields().setUid(Hex.encodeHexString(digest));
-
-            X509v3CertificateBuilder builder =
-                    new X509v3CertificateBuilder(
-                            new JcaX509CertificateHolder(JamsCA.CA.getCertificate()).getSubject(),
-                            new BigInteger(128, new SecureRandom()),
-                            new Date(now - SHIFT),
-                            new Date(now + JamsCA.userLifetime),
-                            new X500Name(user.getX509Fields().getDN()),
-                            SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
-
             user.setPrivateKey(keyPair.getPrivate());
-            user.setCertificate(
-                    CertificateSigner.signCertificate(
-                            JamsCA.CA.getPrivateKey(), builder, ExtensionLibrary.userExtensions));
-            log.info(
-                    "New user certificate:  Not valid after: "
-                            + user.getCertificate().getNotAfter());
+
+            String dn = user.getX509Fields().getDN();
+            SubjectPublicKeyInfo publicKeyInfo =
+                    SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded());
+            X509Certificate certificate =
+                    generateSignedCertificate(dn, JamsCA.userLifetime, publicKeyInfo);
+            user.setCertificate(certificate);
             return user;
         } catch (Exception e) {
             log.error("Could not generate a user certificate: " + e);
@@ -89,28 +81,18 @@ public class UserBuilder {
     }
 
     public static User refreshUser(User user, long userLifeTime) {
-        long now = System.currentTimeMillis();
         X509Fields x509 = new X509Fields();
         x509.setCommonName(user.getUsername());
         x509.setUid(user.getJamiId());
         user.setX509Fields(x509);
 
         try {
-            X509v3CertificateBuilder builder =
-                    new X509v3CertificateBuilder(
-                            new JcaX509CertificateHolder(JamsCA.CA.getCertificate()).getSubject(),
-                            new BigInteger(128, new SecureRandom()),
-                            new Date(now - SHIFT),
-                            new Date(now + userLifeTime),
-                            new X500Name(user.getX509Fields().getDN()),
-                            new JcaX509CertificateHolder(user.getCertificate())
-                                    .getSubjectPublicKeyInfo());
-            user.setCertificate(
-                    CertificateSigner.signCertificate(
-                            JamsCA.CA.getPrivateKey(), builder, ExtensionLibrary.userExtensions));
-            log.info(
-                    "Refreshed user certificate:  Not valid after: "
-                            + user.getCertificate().getNotAfter());
+            String dn = user.getX509Fields().getDN();
+            SubjectPublicKeyInfo publicKeyInfo =
+                    new JcaX509CertificateHolder(user.getCertificate()).getSubjectPublicKeyInfo();
+            X509Certificate certificate =
+                    generateSignedCertificate(dn, userLifeTime, publicKeyInfo);
+            user.setCertificate(certificate);
 
             return user;
         } catch (Exception e) {
@@ -118,4 +100,24 @@ public class UserBuilder {
             return null;
         }
     }
+
+    private static X509Certificate generateSignedCertificate(
+            String dn, long userLifeTime, SubjectPublicKeyInfo publicKeyInfo)
+            throws CertificateEncodingException {
+        long now = System.currentTimeMillis();
+        X509v3CertificateBuilder builder =
+                new X509v3CertificateBuilder(
+                        new JcaX509CertificateHolder(JamsCA.CA.getCertificate()).getSubject(),
+                        new BigInteger(128, new SecureRandom()),
+                        new Date(now - SHIFT),
+                        new Date(now + userLifeTime),
+                        new X500Name(dn),
+                        publicKeyInfo);
+
+        X509Certificate certificate =
+                CertificateSigner.signCertificate(
+                        JamsCA.CA.getPrivateKey(), builder, ExtensionLibrary.userExtensions);
+        log.info("User certificate:  Not valid after: {}", certificate.getNotAfter());
+        return certificate;
+    }
 }
diff --git a/jams-common/src/main/java/net/jami/jams/common/objects/requests/DeviceRevocationRequest.java b/jams-common/src/main/java/net/jami/jams/common/objects/requests/DeviceRevocationRequest.java
deleted file mode 100644
index 21a1b903..00000000
--- a/jams-common/src/main/java/net/jami/jams/common/objects/requests/DeviceRevocationRequest.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2020 by Savoir-faire Linux
- * Authors: William Enright <william.enright@savoirfairelinux.com>
- *          Ndeye Anna Ndiaye <anna.ndiaye@savoirfairelinux.com>
- *          Johnny Flores <johnny.flores@savoirfairelinux.com>
- *          Mohammed Raza <mohammed.raza@savoirfairelinux.com>
- *          Felix Sidokhine <felix.sidokhine@savoirfairelinux.com>
- *
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-package net.jami.jams.common.objects.requests;
-
-import lombok.Getter;
-import lombok.Setter;
-
-@Getter
-@Setter
-public class DeviceRevocationRequest {
-
-    private String owner;
-    private String deviceId;
-
-    public DeviceRevocationRequest(String username, String deviceId) {
-        this.owner = owner;
-        this.deviceId = deviceId;
-    }
-}
diff --git a/jams-common/src/main/java/net/jami/jams/common/serialization/adapters/ContactAdapter.java b/jams-common/src/main/java/net/jami/jams/common/serialization/adapters/ContactAdapter.java
index 80fb7a17..a8ba9a52 100644
--- a/jams-common/src/main/java/net/jami/jams/common/serialization/adapters/ContactAdapter.java
+++ b/jams-common/src/main/java/net/jami/jams/common/serialization/adapters/ContactAdapter.java
@@ -25,7 +25,6 @@ public class ContactAdapter implements JsonSerializer<Contact>, JsonDeserializer
         contact.setUri(input.get("uri").getAsString());
         contact.setConversationId(input.get("conversationId").getAsString());
 
-
         long timeAdded = 0L;
         if (input.has("added")) {
             timeAdded = input.get("added").getAsLong();
diff --git a/jams-common/src/main/java/net/jami/jams/common/utils/X509Utils.java b/jams-common/src/main/java/net/jami/jams/common/utils/X509Utils.java
index def3a3ec..1276d4db 100644
--- a/jams-common/src/main/java/net/jami/jams/common/utils/X509Utils.java
+++ b/jams-common/src/main/java/net/jami/jams/common/utils/X509Utils.java
@@ -66,6 +66,8 @@ public class X509Utils {
     private static final String PPK_TAIL = "\n-----END PUBLIC KEY-----";
 
     public static PrivateKey getKeyFromPEMString(String keyString) {
+        if (keyString.isEmpty()) return null;
+
         try {
             PEMParser parser = new PEMParser(new StringReader(keyString));
             Object parsedObject = parser.readObject();
diff --git a/jams-react-client/src/components/Devices/Devices.tsx b/jams-react-client/src/components/Devices/Devices.tsx
index 535aad00..84403f62 100755
--- a/jams-react-client/src/components/Devices/Devices.tsx
+++ b/jams-react-client/src/components/Devices/Devices.tsx
@@ -40,7 +40,7 @@ import i18next from "i18next";
 
 const useStyles = makeStyles(styles);
 
-export default function Devices(props) {
+export default function Devices({ username }) {
   const classes = useStyles();
   const history = useHistory();
 
@@ -49,9 +49,7 @@ export default function Devices(props) {
   const [displayName, setDisplayName] = useState("");
   const [openEdit, setOpenEdit] = useState(false);
   const [openRevoke, setOpenRevoke] = useState(false);
-  const userData = {
-    username: props.username,
-  };
+  const userData = { username };
 
   useEffect(() => {
     auth.checkDirectoryType(() => {
@@ -94,7 +92,7 @@ export default function Devices(props) {
           });
       }
     });
-  }, [history, selectedDevice, userData]);
+  }, []);
 
   function getDeviceStatus(device) {
     if (!device.revoked) {
@@ -134,7 +132,7 @@ export default function Devices(props) {
   const handleUpdate = () => {
     if (auth.hasAdminScope()) {
       const data = {
-        username: props.username,
+        username,
         deviceId: selectedDevice.deviceId,
         deviceName: displayName,
       };
@@ -188,7 +186,7 @@ export default function Devices(props) {
   const handleDeviceRevoke = () => {
     if (auth.hasAdminScope()) {
       const data = {
-        username: props.username,
+        username,
         deviceId: selectedDevice.deviceId,
       };
       axios(
diff --git a/jams-react-client/src/components/Grid/GridItem.tsx b/jams-react-client/src/components/Grid/GridItem.tsx
index 2161d6b8..922404b8 100644
--- a/jams-react-client/src/components/Grid/GridItem.tsx
+++ b/jams-react-client/src/components/Grid/GridItem.tsx
@@ -1,9 +1,7 @@
-import React from "react";
-// nodejs library to set properties for components
-import PropTypes from "prop-types";
-// @mui/material components
+import React, { FC } from "react";
 import { makeStyles } from "@mui/styles";
-import Grid from "@mui/material/Grid";
+import { Grid, GridTypeMap } from "@mui/material";
+import { OverridableComponent } from "@mui/material/OverridableComponent";
 
 const styles = {
   grid: {
@@ -13,16 +11,17 @@ const styles = {
 
 const useStyles = makeStyles(styles);
 
-export default function GridItem(props) {
+interface GridItemProps extends OverridableComponent<GridTypeMap> {
+  children: React.ReactNode;
+}
+
+const GridItem: FC<GridItemProps> = ({ children, ...rest }) => {
   const classes = useStyles();
-  const { children, ...rest } = props;
   return (
     <Grid item {...rest} className={classes.grid}>
       {children}
     </Grid>
   );
-}
-
-GridItem.propTypes = {
-  children: PropTypes.node,
 };
+
+export default GridItem;
diff --git a/jams-react-client/src/globalUrls.tsx b/jams-react-client/src/globalUrls.tsx
index 12efdb95..12013152 100644
--- a/jams-react-client/src/globalUrls.tsx
+++ b/jams-react-client/src/globalUrls.tsx
@@ -2,7 +2,7 @@ const uri = "";
 const current_uri = window.location.href;
 const backend_address = new URL(
   process.env.NODE_ENV === "development"
-    ? "http://localhost:8080"
+    ? window.location.origin.replace(/\d+$/, "") + "8080"
     : window.location.href
 );
 const url_path = backend_address.protocol + "//" + backend_address.hostname;
diff --git a/jams-react-client/src/views/Blueprint/PolicyDataContext.tsx b/jams-react-client/src/views/Blueprint/PolicyDataContext.tsx
index 3a642e6f..653a6560 100644
--- a/jams-react-client/src/views/Blueprint/PolicyDataContext.tsx
+++ b/jams-react-client/src/views/Blueprint/PolicyDataContext.tsx
@@ -39,7 +39,7 @@ export const PolicyDataContextProvider: FC<Props> = ({
   children,
 }) => {
   const [policyData, setPolicyData] = useState(DEFAULT_POLICY_DATA);
-  const [snackbar, setSnackbar] = useState({
+  const [snackbar, setSnackbar] = useState<SnackbarProps>({
     open: false,
     severity: "success",
     message: "",
diff --git a/jams-react-client/src/views/Blueprint/policyData.constants.tsx b/jams-react-client/src/views/Blueprint/policyData.constants.tsx
index 9e204d59..09f3e5da 100644
--- a/jams-react-client/src/views/Blueprint/policyData.constants.tsx
+++ b/jams-react-client/src/views/Blueprint/policyData.constants.tsx
@@ -19,9 +19,7 @@ export const DEFAULT_UI_CUSTOMIZATION = {
   logoSize: 100,
 };
 
-export type UiCustomization = typeof DEFAULT_UI_CUSTOMIZATION;
-
-export const DEFAULT_POLICY_DATA = {
+const DEFAULT_POLICY_DATA_PERMISSIONS = {
   videoEnabled: true,
   publicInCalls: false,
   autoAnswer: false,
@@ -30,7 +28,9 @@ export const DEFAULT_POLICY_DATA = {
 
   rendezVous: false,
   blueprintModerators: [],
+};
 
+const DEFAULT_POLICY_DATA_CONFIGURATION = {
   upnpEnabled: true,
 
   selectedTurnOption: "defaultTurn",
@@ -41,8 +41,17 @@ export const DEFAULT_POLICY_DATA = {
   selectedDHTProxyOption: "defaultDHTProxy",
   proxyServer: "dhtproxy.jami.net",
   dhtProxyListUrl: "",
+};
 
+export const DEFAULT_POLICY_DATA = {
+  ...DEFAULT_POLICY_DATA_PERMISSIONS,
+  ...DEFAULT_POLICY_DATA_CONFIGURATION,
   uiCustomization: DEFAULT_UI_CUSTOMIZATION,
 };
 
-export type PolicyData = typeof DEFAULT_POLICY_DATA;
+export type UiCustomization = typeof DEFAULT_UI_CUSTOMIZATION;
+export type PolicyDataPermissions = typeof DEFAULT_POLICY_DATA_PERMISSIONS;
+export type PolicyDataNetwork = typeof DEFAULT_POLICY_DATA_CONFIGURATION;
+export interface PolicyData extends PolicyDataPermissions, PolicyDataNetwork {
+  uiCustomization: UiCustomization;
+}
diff --git a/jams-react-client/src/views/Blueprint/updatePolicyData.tsx b/jams-react-client/src/views/Blueprint/updatePolicyData.tsx
index 984df536..8066a59d 100644
--- a/jams-react-client/src/views/Blueprint/updatePolicyData.tsx
+++ b/jams-react-client/src/views/Blueprint/updatePolicyData.tsx
@@ -186,8 +186,8 @@ export const _updatePolicyData = (
   policyData: PolicyData,
   setPolicyData: Dispatch<SetStateAction<PolicyData>>,
   field: string,
-  value: string,
-  setSnackbar: (snackbar: any) => void
+  value: any,
+  setSnackbar: (snackbar: SnackbarProps) => void
 ) => {
   setPolicyData((state) => ({ ...state, [field]: value }));
 
diff --git a/jams-react-client/src/views/Contacts/Contacts.tsx b/jams-react-client/src/views/Contacts/Contacts.tsx
index 5736bdc6..cd138279 100644
--- a/jams-react-client/src/views/Contacts/Contacts.tsx
+++ b/jams-react-client/src/views/Contacts/Contacts.tsx
@@ -413,7 +413,6 @@ export default function Users(props) {
               xs={12}
               sm={12}
               md={2}
-              wrap="nowrap"
               key={contact.uri}
               style={{ display: contact.display }}
             >
diff --git a/jams-server/src/main/java/net/jami/jams/server/core/workflows/RegisterDeviceFlow.java b/jams-server/src/main/java/net/jami/jams/server/core/workflows/RegisterDeviceFlow.java
index ed742f18..7bcac7f4 100644
--- a/jams-server/src/main/java/net/jami/jams/server/core/workflows/RegisterDeviceFlow.java
+++ b/jams-server/src/main/java/net/jami/jams/server/core/workflows/RegisterDeviceFlow.java
@@ -102,9 +102,8 @@ public class RegisterDeviceFlow {
                     });
             return response;
         } catch (Exception e) {
-            log.error(
-                    "An exception has occurred while trying to enroll a device with error {}",
-                    e.getMessage());
+            log.error("An exception has occurred while trying to enroll a device");
+            e.printStackTrace();
             return null;
         }
     }
diff --git a/jams-server/src/main/java/net/jami/jams/server/servlets/api/admin/contacts/ContactServlet.java b/jams-server/src/main/java/net/jami/jams/server/servlets/api/admin/contacts/ContactServlet.java
index 64117277..c1a30341 100644
--- a/jams-server/src/main/java/net/jami/jams/server/servlets/api/admin/contacts/ContactServlet.java
+++ b/jams-server/src/main/java/net/jami/jams/server/servlets/api/admin/contacts/ContactServlet.java
@@ -38,15 +38,9 @@ import net.jami.jams.common.objects.contacts.Contact;
 import net.jami.jams.common.objects.user.AccessLevel;
 import net.jami.jams.common.serialization.adapters.GsonFactory;
 import net.jami.jams.common.serialization.tomcat.TomcatCustomErrorHandler;
-import net.jami.jams.common.utils.ContactMerger;
-
-import org.json.JSONObject;
 
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
-import java.util.Scanner;
 
 @WebServlet("/api/admin/contacts")
 public class ContactServlet extends HttpServlet {
@@ -68,8 +62,8 @@ public class ContactServlet extends HttpServlet {
     @JsonContent
     protected void doGet(HttpServletRequest req, HttpServletResponse resp)
             throws ServletException, IOException {
-        List<Contact> contactList =
-                dataStore.getContactDao().getByOwner(req.getParameter("username"));
+        String username = req.getParameter("username");
+        List<Contact> contactList = dataStore.getContactDao().getByOwner(username);
         resp.getOutputStream().write(gson.toJson(contactList).getBytes());
     }
 
@@ -90,28 +84,9 @@ public class ContactServlet extends HttpServlet {
     @ScopedServletMethod(securityGroups = {AccessLevel.ADMIN})
     protected void doPut(HttpServletRequest req, HttpServletResponse resp)
             throws ServletException, IOException {
-        Scanner s = new Scanner(req.getInputStream()).useDelimiter("\\A");
-        String res = s.hasNext() ? s.next() : "";
-        final JSONObject obj = new JSONObject(res);
-
-        Contact contact = new Contact();
-        // TODO: Replace with mergetool.
-        contact.setDisplayName(obj.get("displayName").toString());
-        contact.setTimestamp(System.currentTimeMillis() / 1000);
-        contact.setStatus('A');
-        contact.setOwner(req.getParameter("username"));
-        contact.setUri(obj.get("uri").toString());
-
-        List<Contact> localList =
-                dataStore.getContactDao().getByOwner(req.getParameter("username"));
-        List<Contact> remoteList = new ArrayList<>();
-        remoteList.add(contact);
-        List<Contact> result = ContactMerger.mergeContacts(localList, remoteList);
-
-        if (dataStore.getContactDao().storeContactList(result)) resp.setStatus(200);
-        else
-            TomcatCustomErrorHandler.sendCustomError(
-                    resp, 500, "could not store a contact due to server-side error");
+        String username = req.getParameter("username");
+        net.jami.jams.server.servlets.api.auth.contacts.ContactServlet.addContact(
+                req, resp, username);
     }
 
     /**
@@ -155,15 +130,8 @@ public class ContactServlet extends HttpServlet {
     @ScopedServletMethod(securityGroups = {AccessLevel.ADMIN})
     protected void doPost(HttpServletRequest req, HttpServletResponse resp)
             throws ServletException, IOException {
-        List<Contact> localList =
-                dataStore.getContactDao().getByOwner(req.getParameter("username"));
-        List<Contact> remoteList = Arrays.asList(gson.fromJson(req.getReader(), Contact[].class));
-
-        remoteList.forEach(contact -> contact.setOwner(req.getParameter("username")));
-        List<Contact> result = ContactMerger.mergeContacts(localList, remoteList);
-
-        if (!dataStore.getContactDao().storeContactList(result))
-            TomcatCustomErrorHandler.sendCustomError(resp, 500, "Could not store contacts!");
-        else resp.getOutputStream().write(gson.toJson(result).getBytes());
+        String username = req.getParameter("username");
+        net.jami.jams.server.servlets.api.auth.contacts.ContactServlet.addContacts(
+                req, resp, username);
     }
 }
diff --git a/jams-server/src/main/java/net/jami/jams/server/servlets/api/admin/update/SubscriptionServlet.java b/jams-server/src/main/java/net/jami/jams/server/servlets/api/admin/update/SubscriptionServlet.java
index 89ea65e1..52602d18 100644
--- a/jams-server/src/main/java/net/jami/jams/server/servlets/api/admin/update/SubscriptionServlet.java
+++ b/jams-server/src/main/java/net/jami/jams/server/servlets/api/admin/update/SubscriptionServlet.java
@@ -23,6 +23,7 @@
 package net.jami.jams.server.servlets.api.admin.update;
 
 import com.google.gson.Gson;
+import com.google.gson.JsonObject;
 
 import jakarta.servlet.ServletException;
 import jakarta.servlet.annotation.WebServlet;
@@ -38,8 +39,6 @@ import net.jami.jams.common.serialization.adapters.GsonFactory;
 import net.jami.jams.server.Server;
 import net.jami.jams.server.licensing.LicenseService;
 
-import org.json.JSONObject;
-
 import java.io.FileWriter;
 import java.io.IOException;
 
@@ -68,22 +67,18 @@ public class SubscriptionServlet extends HttpServlet {
     @ScopedServletMethod(securityGroups = {AccessLevel.ADMIN})
     @JsonContent
     protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
-        String license = new String(req.getInputStream().readAllBytes());
-        final JSONObject obj = new JSONObject(license);
-        license = obj.getString("base64License");
+        JsonObject jsonObject = gson.fromJson(req.getReader(), JsonObject.class);
+        String license = jsonObject.get("base64License").getAsString();
+
+        // create .dat file to be used later
+        FileWriter fw = new FileWriter("license.dat");
+        fw.write(license);
+        fw.close();
 
-        if (license != null || !license.isBlank()) {
-            // create .dat file to be used later
-            FileWriter fw = new FileWriter("license.dat");
-            fw.write(license);
-            fw.close();
-            LicenseService licenseService = new LicenseService();
-            licenseService.loadLicense();
-            if (Server.activated.get()) {
-                resp.setStatus(200);
-                return;
-            }
+        LicenseService licenseService = new LicenseService();
+        licenseService.loadLicense();
+        if (Server.activated.get()) {
+            resp.setStatus(200);
         }
-        resp.setStatus(500);
     }
 }
diff --git a/jams-server/src/main/java/net/jami/jams/server/servlets/api/auth/directory/DirectoryEntryServlet.java b/jams-server/src/main/java/net/jami/jams/server/servlets/api/auth/directory/DirectoryEntryServlet.java
index 04291124..dfb2cab4 100644
--- a/jams-server/src/main/java/net/jami/jams/server/servlets/api/auth/directory/DirectoryEntryServlet.java
+++ b/jams-server/src/main/java/net/jami/jams/server/servlets/api/auth/directory/DirectoryEntryServlet.java
@@ -37,7 +37,6 @@ import lombok.extern.slf4j.Slf4j;
 
 import net.jami.jams.common.authentication.AuthenticationSourceType;
 import net.jami.jams.common.authmodule.AuthModuleKey;
-import net.jami.jams.common.objects.user.User;
 import net.jami.jams.common.objects.user.UserProfile;
 import net.jami.jams.common.serialization.adapters.GsonFactory;
 
@@ -203,55 +202,49 @@ public class DirectoryEntryServlet extends HttpServlet {
     protected void doGet(HttpServletRequest req, HttpServletResponse resp)
             throws ServletException, IOException {
 
+        String directory = req.getParameter("directory");
+        String directoryType = req.getParameter("directoryType");
+
+        String format = req.getParameter("format");
+        boolean isInVCardFormat = format != null && format.equals("vcard");
+
         String jamiId = req.getParameter("jamiId");
-        if (jamiId != null) {
-            User user = dataStore.getUserDao().getByJamiId(jamiId).get(0);
-            List<UserProfile> userProfiles = new ArrayList<>();
-            userAuthenticationModule
-                    .getAuthSources()
-                    .forEach(
-                            (k, v) -> {
-                                userProfiles.addAll(
-                                        v.searchUserProfiles(
-                                                user.getUsername(),
-                                                "LOGON_NAME",
-                                                Optional.empty()));
-                            });
-            if (req.getParameter("format") != null && req.getParameter("format").equals("vcard")) {
-                resp.getOutputStream().write(userProfiles.get(0).getAsVCard().getBytes());
-            } else resp.getOutputStream().write(gson.toJson(userProfiles.get(0)).getBytes());
-            return;
-        }
-        if (req.getParameter("directory") != null && req.getParameter("directoryType") != null) {
-            List<UserProfile> profiles =
+
+        String username =
+                jamiId != null
+                        ? dataStore.getUserDao().getByJamiId(jamiId).get(0).getUsername()
+                        : req.getParameter("username");
+
+        if (directory != null && directoryType != null) {
+            AuthModuleKey authModuleKey =
+                    new AuthModuleKey(
+                            directory, AuthenticationSourceType.fromString(directoryType));
+
+            List<UserProfile> userProfiles =
                     userAuthenticationModule
                             .getAuthSources()
-                            .get(
-                                    new AuthModuleKey(
-                                            req.getParameter("directory"),
-                                            AuthenticationSourceType.fromString(
-                                                    req.getParameter("directoryType"))))
-                            .searchUserProfiles(
-                                    req.getParameter("username"), "LOGON_NAME", Optional.empty());
-            if (req.getParameter("format") != null && req.getParameter("format").equals("vcard")) {
-                resp.getOutputStream().write(profiles.get(0).getAsVCard().getBytes());
-            } else resp.getOutputStream().write(gson.toJson(profiles.get(0)).getBytes());
+                            .get(authModuleKey)
+                            .searchUserProfiles(username, "LOGON_NAME", Optional.empty());
+
+            UserProfile userProfile = userProfiles.get(0);
+            String result = isInVCardFormat ? userProfile.getAsVCard() : gson.toJson(userProfile);
+            resp.getOutputStream().write(result.getBytes());
             return;
         }
+
         List<UserProfile> userProfiles = new ArrayList<>();
         userAuthenticationModule
                 .getAuthSources()
+                .values()
                 .forEach(
-                        (k, v) -> {
+                        v -> {
                             userProfiles.addAll(
-                                    v.searchUserProfiles(
-                                            req.getParameter("username"),
-                                            "LOGON_NAME",
-                                            Optional.empty()));
+                                    v.searchUserProfiles(username, "LOGON_NAME", Optional.empty()));
                         });
-        if (req.getParameter("format") != null && req.getParameter("format").equals("vcard")) {
-            resp.getOutputStream().write(userProfiles.get(0).getAsVCard().getBytes());
-        } else resp.getOutputStream().write(gson.toJson(userProfiles.get(0)).getBytes());
+
+        UserProfile userProfile = userProfiles.get(0);
+        String result = isInVCardFormat ? userProfile.getAsVCard() : gson.toJson(userProfile);
+        resp.getOutputStream().write(result.getBytes());
     }
 
     @Override
diff --git a/jams-server/src/main/java/net/jami/jams/server/servlets/api/image/FileHandlerServlet.java b/jams-server/src/main/java/net/jami/jams/server/servlets/api/image/FileHandlerServlet.java
index b5358130..a4e1b265 100644
--- a/jams-server/src/main/java/net/jami/jams/server/servlets/api/image/FileHandlerServlet.java
+++ b/jams-server/src/main/java/net/jami/jams/server/servlets/api/image/FileHandlerServlet.java
@@ -1,5 +1,7 @@
 package net.jami.jams.server.servlets.api.image;
 
+import com.google.gson.Gson;
+
 import jakarta.servlet.ServletException;
 import jakarta.servlet.annotation.MultipartConfig;
 import jakarta.servlet.annotation.WebServlet;
@@ -9,6 +11,7 @@ import jakarta.servlet.http.HttpServletResponse;
 import jakarta.servlet.http.Part;
 
 import lombok.extern.slf4j.Slf4j;
+
 import net.jami.jams.common.serialization.adapters.GsonFactory;
 
 import java.io.File;
@@ -20,8 +23,6 @@ import java.nio.file.Paths;
 import java.util.HashMap;
 import java.util.Map;
 
-import com.google.gson.Gson;
-
 @MultipartConfig
 @Slf4j
 @WebServlet("/api/image/filehandler/*")
@@ -87,7 +88,8 @@ public class FileHandlerServlet extends HttpServlet {
             }
 
             Map<String, String> map = new HashMap<>();
-            String url = "/api/image/filehandler/" + blueprintName + "/" + imageType + "/" + fileName;
+            String url =
+                    "/api/image/filehandler/" + blueprintName + "/" + imageType + "/" + fileName;
             map.put("url", url);
 
             Gson gson = GsonFactory.createGson();
diff --git a/jams-server/src/main/java/net/jami/jams/server/servlets/filters/FilterUtils.java b/jams-server/src/main/java/net/jami/jams/server/servlets/filters/FilterUtils.java
index 5ba99ad9..fa299778 100644
--- a/jams-server/src/main/java/net/jami/jams/server/servlets/filters/FilterUtils.java
+++ b/jams-server/src/main/java/net/jami/jams/server/servlets/filters/FilterUtils.java
@@ -95,9 +95,7 @@ public class FilterUtils {
             }
 
             String username = token.getJWTClaimsSet().getSubject();
-            log.info("Getting user from database");
             User user = dataStore.getUserDao().getByUsername(username).orElseThrow();
-            log.info("User retrieved from database: {}", user);
 
             if (!user.getAccessLevelName().equals("ADMIN")
                     && certificateAuthority.getLatestCRL().get() != null) {
diff --git a/ldap-connector/src/main/java/net/jami/jams/ldap/connector/service/UserProfileService.java b/ldap-connector/src/main/java/net/jami/jams/ldap/connector/service/UserProfileService.java
index 56c177fa..af8fe25f 100644
--- a/ldap-connector/src/main/java/net/jami/jams/ldap/connector/service/UserProfileService.java
+++ b/ldap-connector/src/main/java/net/jami/jams/ldap/connector/service/UserProfileService.java
@@ -38,7 +38,11 @@ import org.ldaptive.SearchRequest;
 import org.ldaptive.SearchResponse;
 
 import java.nio.charset.StandardCharsets;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Optional;
 import java.util.stream.Collectors;
 
 @Slf4j
@@ -58,45 +62,23 @@ public class UserProfileService {
                             queryString.getBytes(StandardCharsets.UTF_8),
                             StandardCharsets.ISO_8859_1);
             connection = connectionFactory.getConnection();
-            try {
-                connection.open();
-                SearchOperation search = new SearchOperation(connectionFactory);
-                SearchResponse res = search.execute(buildRequest(queryString, field, exactMatch));
-
-                DataStore.NUM_PAGES =
-                        (Integer) res.getEntries().size() / DataStore.RESULTS_PER_PAGE;
-                if (res.getEntries().size() % DataStore.RESULTS_PER_PAGE != 0)
-                    DataStore.NUM_PAGES++;
-
-                if (page.isPresent() && !res.getEntries().isEmpty()) {
-                    if (res.getEntries().size() < DataStore.RESULTS_PER_PAGE)
-                        res = res.subResult(0, res.getEntries().size());
-                    else if (page.get() * DataStore.RESULTS_PER_PAGE > res.getEntries().size())
-                        res =
-                                res.subResult(
-                                        (page.get() - 1) * DataStore.RESULTS_PER_PAGE,
-                                        res.getEntries().size());
-                    else
-                        res =
-                                res.subResult(
-                                        (page.get() - 1) * DataStore.RESULTS_PER_PAGE,
-                                        (page.get() * DataStore.RESULTS_PER_PAGE));
-                }
+            connection.open();
+            SearchOperation search = new SearchOperation(connectionFactory);
+            SearchResponse res = search.execute(buildRequest(queryString, field, exactMatch));
 
-                if (res.getEntries().size() == 0) return new ArrayList<>();
-                List<UserProfile> profilesFromResponse =
-                        res.getEntries().stream()
-                                .map(UserProfileService::profileFromResponse)
-                                .collect(Collectors.toList());
-                for (UserProfile p : profilesFromResponse) {
-                    dataStore.getUserProfileDao().insertIfNotExists(p);
-                }
+            Collection<LdapEntry> entries = getEntriesPage(res, page);
+
+            if (entries.isEmpty()) return new ArrayList<>();
 
-                return profilesFromResponse;
-            } catch (Exception e) {
-                log.error("Could not search LDAP directory with error " + e);
-                return null;
+            List<UserProfile> profilesFromResponse =
+                    entries.stream()
+                            .map(UserProfileService::profileFromResponse)
+                            .collect(Collectors.toList());
+            for (UserProfile p : profilesFromResponse) {
+                dataStore.getUserProfileDao().insertIfNotExists(p);
             }
+
+            return profilesFromResponse;
         } catch (Exception e) {
             log.info("Failed to search LDAP directory with error " + e);
             return null;
@@ -105,6 +87,28 @@ public class UserProfileService {
         }
     }
 
+    private Collection<LdapEntry> getEntriesPage(SearchResponse res, Optional<Integer> page) {
+        int size = res.getEntries().size();
+
+        DataStore.NUM_PAGES = (Integer) size / DataStore.RESULTS_PER_PAGE;
+        if (size % DataStore.RESULTS_PER_PAGE != 0) DataStore.NUM_PAGES++;
+
+        if (page.isEmpty() || size == 0) {
+            return res.getEntries();
+        }
+
+        if (size < DataStore.RESULTS_PER_PAGE) res = res.subResult(0, size);
+        else if (page.get() * DataStore.RESULTS_PER_PAGE > size)
+            res = res.subResult((page.get() - 1) * DataStore.RESULTS_PER_PAGE, size);
+        else
+            res =
+                    res.subResult(
+                            (page.get() - 1) * DataStore.RESULTS_PER_PAGE,
+                            (page.get() * DataStore.RESULTS_PER_PAGE));
+
+        return res.getEntries();
+    }
+
     public static SearchRequest buildRequest(String queryString, String field, boolean exactMatch) {
 
         if (!exactMatch) {
-- 
GitLab