diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9eefbf5b0bc3ff87b9f9ca68d3b7305631dc6d1b..f4316fe9826fc4f63b2f66ed9d60c2be3ce675de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
 # Changelog
 
+## [0.7.6]
+
+- Add Forgejo metadata for use as custom app.
+
+## [0.7.5]
+
+- Add Jitsi and Mattermost metadata for use as custom apps.
+
 ## [0.7.4]
 
 - Make the sign-in UI less wide.
diff --git a/README.md b/README.md
index 153595f5a6d1da09257264e0a29ee27c8e1759ee..34a99ff6d48667b90a7b42a5fc53f2df0eaf520e 100644
--- a/README.md
+++ b/README.md
@@ -154,11 +154,20 @@ might behave differently in the local development environment compared to a
 regular Stackspin instance, i.e., one that's not a local/cluster hybrid. In
 this case, you'll want to run your new version in a regular Stackspin cluster.
 
-To do that, make sure to increase the chart version number in `Chart.yaml`, and
-push your work to a MR. The CI pipeline should then publish your new chart
-version in the Gitlab helm chart repo for the dashboard project, but in the
-`unstable` channel -- the `stable` channel is reserved for chart versions that
-have been merged to the `main` branch.
+To do that:
+* Push your work to an MR.
+* Set the image tags in `values.yaml` to the one created for your branch; if
+  unsure, check the available tags in the Gitlab container registry for the
+  dashboard project.
+* Make sure to increase the chart version number in `Chart.yaml`, preferably
+  with a suffix to denote that it's not a stable version. For example, if the
+  last stable release is 1.2.3, make the version 1.2.4-myawesomefeature in your
+  branch.
+
+The CI pipeline should then publish your new chart version in the Gitlab helm
+chart repo for the dashboard project, but in the `unstable` channel -- the
+`stable` channel is reserved for chart versions that have been merged to the
+`main` branch.
 
 Once your package is published, use it by
 
diff --git a/backend/app.py b/backend/app.py
index 3115dcbcca51344a0d746201ab5acf6bb8188c29..a24224c899b04414b43b317a31e6be160497bad7 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -62,6 +62,7 @@ app = Flask(__name__)
 
 app.config["SECRET_KEY"] = SECRET_KEY
 app.config["SQLALCHEMY_DATABASE_URI"] = SQLALCHEMY_DATABASE_URI
+app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {'pool_pre_ping': True}
 app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = SQLALCHEMY_TRACK_MODIFICATIONS
 
 app.logger.setLevel(logging.INFO)
diff --git a/backend/areas/auth/auth.py b/backend/areas/auth/auth.py
index 5ea14d93a7721ac74bd976a2c3698df8f1b1b434..6cd27d343e77514b37f9147d93c31d42d9d775d0 100644
--- a/backend/areas/auth/auth.py
+++ b/backend/areas/auth/auth.py
@@ -1,4 +1,4 @@
-from flask import jsonify, request
+from flask import current_app, jsonify, request
 from flask_jwt_extended import create_access_token
 from flask_cors import cross_origin
 from datetime import timedelta
@@ -29,24 +29,22 @@ def hydra_callback():
 
     token = HydraOauth.get_token(state, code)
     user_info = HydraOauth.get_user_info()
-    # Match Kratos identity with Hydra
-    identities = KratosApi.get("/identities")
-    identity = None
-    for i in identities.json():
-        if i["traits"]["email"] == user_info["email"]:
-            identity = i
+    kratos_id = user_info["sub"]
 
-    # Short lifetime for token. If the session is still active, it will be
-    # automatically renewed via Hydra.
-    access_token = create_access_token(
-        identity=token, expires_delta=timedelta(hours=1), additional_claims={"user_id": identity["id"]}
-    )
+    # TODO: add a check to see if this a valid ID/active account
+
+    try:
+        access_token = create_access_token(
+            identity=token, expires_delta=timedelta(hours=1), additional_claims={"user_id": kratos_id}
+        )
+    except Exception as e:
+        raise BadRequest("Error with creating auth token between backend and frontend")
 
     apps = App.query.all()
     app_roles = []
     for app in apps:
         tmp_app_role = AppRole.query.filter_by(
-            user_id=identity["id"], app_id=app.id
+            user_id=kratos_id, app_id=app.id
         ).first()
         app_roles.append(
             {
@@ -59,7 +57,7 @@ def hydra_callback():
         {
             "accessToken": access_token,
             "userInfo": {
-                "id": identity["id"],
+                "id": kratos_id,
                 "email": user_info["email"],
                 "name": user_info["name"],
                 "preferredUsername": user_info["preferred_username"],
diff --git a/backend/areas/users/user_service.py b/backend/areas/users/user_service.py
index 75166d12474e31d8781e63e2eec703a43cb0b8da..ebb965a1b494d2d8d559c21d477e0307bb442f56 100644
--- a/backend/areas/users/user_service.py
+++ b/backend/areas/users/user_service.py
@@ -22,10 +22,18 @@ kratos_identity_api = identity_api.IdentityApi(kratos_client)
 class UserService:
     @staticmethod
     def get_users():
-        res = KratosApi.get("/admin/identities").json()
+        page = 1
         userList = []
-        for r in res:
-            userList.append(UserService.__insertAppRoleToUser(r["id"], r))
+        while page > 0:
+            res = KratosApi.get("/admin/identities?per_page=1000&page={}".format(page)).json()
+            for r in res:
+                # removed the app role assignment function, passing simple user data
+                # userList.append(UserService.__insertAppRoleToUser(r["id"], r))
+                userList.append(r)
+            if len(res) == 0:
+                page = -1
+            else:
+                page = page + 1
 
         return userList
 
@@ -135,6 +143,38 @@ class UserService:
 
         return UserService.get_user(id)
 
+    @staticmethod
+    def put_multiple_users(user_editing_id, data):
+        for user_data in data["users"]:
+            kratos_data = {
+                # "schema_id": "default",
+                "traits": {"email": user_data["email"]},
+            }
+            KratosApi.put("/admin/identities/{}".format(user_data["id"]), kratos_data)
+
+            is_admin = RoleService.is_user_admin(user_editing_id)
+
+            if is_admin and user_data["app_roles"]:
+                app_roles = user_data["app_roles"]
+                for ar in app_roles:
+                    app = App.query.filter_by(slug=ar["name"]).first()
+                    app_role = AppRole.query.filter_by(
+                        user_id=user_data["id"], app_id=app.id).first()
+
+                    if app_role:
+                        app_role.role_id = ar["role_id"] if "role_id" in ar else None
+                        db.session.commit()
+                    else:
+                        appRole = AppRole(
+                            user_id=user_Data["id"],
+                            role_id=ar["role_id"] if "role_id" in ar else None,
+                            app_id=app.id,
+                        )
+                        db.session.add(appRole)
+                        db.session.commit()
+
+            return UserService.get_user(user_data["id"])
+
     @staticmethod
     def delete_user(id):
         app_role = AppRole.query.filter_by(user_id=id).all()
diff --git a/backend/areas/users/users.py b/backend/areas/users/users.py
index 25d906e593b405cb9a6a19f21a44f123194c181a..a00e7d14671a0911c9064a990367f910529e9be6 100644
--- a/backend/areas/users/users.py
+++ b/backend/areas/users/users.py
@@ -7,7 +7,7 @@ from areas import api_v1
 from helpers import KratosApi
 from helpers.auth_guard import admin_required
 
-from .validation import schema, schema_multiple
+from .validation import schema, schema_multiple, schema_multi_edit
 from .user_service import UserService
 
 
@@ -83,6 +83,18 @@ def post_multiple_users():
     return jsonify(res)
 
 
+# multi-user editing of app roles
+@api_v1.route("/users-multi-edit", methods=["PUT"])
+@jwt_required()
+@cross_origin()
+@expects_json(schema_multi_edit)
+@admin_required()
+def put_multiple_users():
+    data = request.get_json()
+    user_id = __get_user_id_from_jwt()
+    res = UserService.put_multiple_users(user_id, data)
+    return jsonify(res)
+
 @api_v1.route("/me", methods=["GET"])
 @jwt_required()
 @cross_origin()
diff --git a/backend/areas/users/validation.py b/backend/areas/users/validation.py
index 4131c838a4e59cd6f9d3fc33b4e706cdbdf42d89..972a024373dfa19df3b426209ffdcecffb7b22ee 100644
--- a/backend/areas/users/validation.py
+++ b/backend/areas/users/validation.py
@@ -17,7 +17,7 @@ schema = {
                     "name": {
                         "type": "string",
                         "description": "Name of the app",
-                        "minLenght": 1,
+                        "minLength": 1,
                     },
                     "role_id": {
                         "type": ["integer", "null"],
@@ -40,3 +40,45 @@ schema_multiple = {
         }
     }
 }
+
+# Multiple app role edit of existing users
+schema_multi_edit = {
+    "users": {
+        "type": "array",
+        "items" : {
+            "type": "object",
+            "properties": {
+                "email": {
+                    "type": "string",
+                    "description": "Email of the user",
+                    "minLength": 1,
+                },
+                "id": {
+                    "type": "string",
+                    "description": "UUID of the user",
+                    "minLength": 1,
+                },
+                "app_roles": {
+                    "type": "array",
+                    "items": {
+                        "type": "object",
+                        "properties": {
+                            "name": {
+                                "type": "string",
+                                "description": "Name of the app",
+                                "minLength": 1,
+                            },
+                            "role_id": {
+                                "type": ["integer", "null"],
+                                "description": "Role of the user",
+                                "minimum": 1,
+                            },
+                        },
+                        # "required": ["name", "role_id"],
+                    },
+                },
+            },
+            # "required": ["email", "app_roles"],
+        }
+    }
+}
\ No newline at end of file
diff --git a/backend/helpers/kratos_user.py b/backend/helpers/kratos_user.py
index 3615809e0adcd29abcdd167a69bb4ca6cf7aafb0..e1540368bdb0a44d0754331a7acbe1a0df31265d 100644
--- a/backend/helpers/kratos_user.py
+++ b/backend/helpers/kratos_user.py
@@ -133,12 +133,18 @@ class KratosUser():
         kratos_id = None
 
         # Get out user ID by iterating over all available IDs
-        data = api.list_identities()
-        for kratos_obj in data:
-            # Unique identifier we use
-            if kratos_obj.traits['email'] == email:
-                kratos_id = str(kratos_obj.id)
-                return KratosUser(api, kratos_id)
+        page = 1
+        while page > 0:
+            data = api.list_identities(per_page=1000, page=page)
+            for kratos_obj in data:
+                # Unique identifier we use
+                if kratos_obj.traits['email'] == email:
+                    kratos_id = str(kratos_obj.id)
+                    return KratosUser(api, kratos_id)
+            if len(data) == 0:
+                page = -1
+            else:
+                page = page + 1
 
         return None
 
@@ -151,11 +157,17 @@ class KratosUser():
 
         kratos_id = None
         return_list = []
-        # Get out user ID by iterating over all available IDs
-        data = api.list_identities()
-        for kratos_obj in data:
-            kratos_id = str(kratos_obj.id)
-            return_list.append(KratosUser(api, kratos_id))
+        # Get out user ID by iterating over all available ID
+        page = 1
+        while page > 0:
+            data = api.list_identities(per_page=1000, page=page)
+            for kratos_obj in data:
+                kratos_id = str(kratos_obj.id)
+                return_list.append(KratosUser(api, kratos_id))
+            if len(data) == 0:
+                page = -1
+            else:
+                page = page + 1
 
         return return_list
 
@@ -340,9 +352,6 @@ class KratosUser():
                 return False
         raise BackendError("Unable to set password by submitting form")
 
-    # Pylint complains about app not used. That is correct, but we will use that
-    # in the future. Ignore this error
-    # pylint: disable=unused-argument
     def get_claims(self, app, roles, mapping=None) -> Dict[str, Dict[str, str]]:
         """Create openID Connect token
         Use the userdata stored in the user object to create an OpenID Connect token.
@@ -352,7 +361,7 @@ class KratosUser():
         Example: getClaims('nextcloud', mapping=[("name", "username"),("roles", "groups")])
 
         Attributes:
-            appname - Name or ID of app to connect to
+            appname - client_id of app to connect to
             roles   - List of roles to add to the `stackspin_roles` claim
             mapping - Mapping of the fields
 
@@ -380,6 +389,34 @@ class KratosUser():
             "stackspin_roles": roles,
         }
 
+        if app == "wekan":
+            # This is a non-standard extension to OIDC. It's used in this form
+            # by Wekan. We don't really have user groups in Stackspin, just an
+            # admin flag. However as far as I can see, the only way to make
+            # some users admin in Wekan via OIDC is to have a group for them.
+            #
+            # We include a default "stackspin_users" group, because Wekan doesn't
+            # process group information if the `groups` list is empty, so we
+            # would not be able to remove a user from the admin group
+            # otherwise.
+            #
+            # Actually Wekan doesn't remove users from groups based on this
+            # list apparently, but it still implements a correct `isAdmin`
+            # check based on this group data, which is the part we care about.
+            groups = [{
+                "displayName": "Stackspin users",
+                "isAdmin": False,
+                "forceCreate": True,
+                "isActive": True,
+            }]
+            if "admin" in roles:
+                groups.append({
+                    "displayName": "Stackspin admins",
+                    "isAdmin": True,
+                    "forceCreate": True,
+                    "isActive": True,
+                })
+            token["groups"] = groups
 
         # Relabel field names
         if mapping:
diff --git a/backend/web/login/login.py b/backend/web/login/login.py
index e03b03ed3f07a347a40897f4b49aa0e485cb0647..572253717af5926a14e1023b9790e4c5f71e7743 100644
--- a/backend/web/login/login.py
+++ b/backend/web/login/login.py
@@ -4,9 +4,10 @@ Hydra for OIDC sessions and MariaDB for application and role specifications.
 The application provides also several command line options to interact with
 the user entries in the database(s)"""
 
+import ast
+import json
 import urllib.parse
 import urllib.request
-import ast
 
 import ory_hydra_client
 # hydra v2
@@ -129,17 +130,23 @@ def login():
     """
 
     # Check if we are logged in:
-    identity = get_auth()
+    (identity, auth_response) = get_auth()
+    # We ignore the potential `auth_response` in this case: that's for telling
+    # the user they have to upgrade their session to include a second factor,
+    # but we're already on the login page so there's no use for that here --
+    # they'd be redirected by Kratos back to this same login page anyway,
+    # creating a redirect loop. Chances are that if `auth_response` is not
+    # None, we're actually serving or processing the TOTP form here.
 
     # List to contain messages pushed to the frontend
     messages = list()
 
-
     refresh = False
     flow = request.args.get("flow")
     if flow:
         cookies = request.headers['cookie']
         flow = kratos_public_frontend_api.get_login_flow(flow, cookie=cookies)
+        # current_app.logger.info("flow found in login: {}".format(flow))
         refresh = flow['refresh']
         if refresh:
             message = {
@@ -241,7 +248,12 @@ def auth():
         abort(400, description="Challenge required when requesting authorization")
 
     # Check if we are logged in:
-    identity = get_auth()
+    (identity, auth_response) = get_auth()
+
+    if auth_response is not None:
+        # According to `get_auth`, we have to send the user a response already,
+        # probably a redirect to let the user provide their second factor.
+        return auth_response
 
     # If the user is not logged in yet, we redirect to the login page
     # but before we do that, we set the "flow_state" cookie to auth.
@@ -370,7 +382,7 @@ def consent():
             current_app.logger.info(f"Providing consent to {client_id} for {kratos_id}")
             current_app.logger.info(f"{kratos_id} was granted admin access to {client_id}")
             # Get claims for this user, provided the current app
-            claims = user.get_claims(None, ['admin'])
+            claims = user.get_claims(client_id, ['admin'])
             current_app.logger.info(f"claims: {claims}")
             return redirect(
                 hydra_admin_api.accept_consent_request(
@@ -431,7 +443,8 @@ def consent():
     current_app.logger.info(f"Using '{roles}' when applying consent for {kratos_id}")
 
     # Get claims for this user, provided the current app
-    claims = user.get_claims(None, roles)
+    claims = user.get_claims(client_id, roles)
+    current_app.logger.info(f"claims: {claims}")
 
     # pylint: disable=fixme
     # TODO: Need to implement checking claims here, once the backend for that is
@@ -464,7 +477,7 @@ def status():
     Show if there is an user is logged in. If not shows: not-auth
     """
 
-    auth_status = get_auth()
+    (auth_status, auth_response) = get_auth()
 
     if auth_status:
         return auth_status.id
@@ -475,27 +488,43 @@ def get_auth():
     """Checks if user is logged in
     Queries the cookies. If an authentication cookie is found, it
     checks with Kratos if the cookie is still valid. If so,
-    the profile is returned. Otherwise False is returned.
-    :return: Profile or False if not logged in
+    the profile is returned. Otherwise False is returned, possibly with a
+    response to send to the user, for redirecting them to the kratos-suggested
+    url, for providing 2FA in particular.
+    :return: (Profile, None) or (False, None) or (False, Response)
     """
 
     cookie = get_kratos_cookie()
     if not cookie:
-        return False
+        return False, None
 
     # Given a cookie, check if it is valid and get the profile
     try:
         api_response = kratos_public_frontend_api.to_session(cookie=cookie)
 
         # Get all traits from ID
-        return api_response.identity
+        return api_response.identity, None
 
     except ory_kratos_client.ApiException as ex:
+        # If it fails because the client needs to provide 2FA, we return a
+        # redirect response for use by the caller of this function.
+        if ex.body is not None:
+            body = json.loads(ex.body)
+            current_app.logger.info("Error in to_session: {}".format(body))
+            error_id = body.get('error', {}).get('id')
+            if error_id == 'session_aal2_required':
+                current_app.logger.info("2FA requested by Kratos. Redirecting the user.")
+                redirect_url = body.get('redirect_browser_to')
+                if redirect_url is None:
+                    response = None
+                else:
+                    response = redirect(redirect_url)
+                return False, response
         current_app.logger.error(
             f"Exception when calling to_session(): {ex}\n"
         )
 
-    return False
+    return False, None
 
 
 def get_kratos_cookie():
diff --git a/backend/web/static/base.js b/backend/web/static/base.js
index 61d08200d5409383f40d02e7aa7d32530eb10142..bf79733c8c7eaf990a84556bf5753d67430be9e9 100644
--- a/backend/web/static/base.js
+++ b/backend/web/static/base.js
@@ -16,8 +16,8 @@
 
 */
 
-// In default configuration the dashboed is on '/'. This can be overwritten
-// before calling the scripts (and configured by the flask app
+// In default configuration the dashboard is on '/'. This can be overwritten
+// before calling the scripts (and configured by the flask app).
 var dashboard_url = "";
 
 // Render a message by appending the data to the messages box. The message id is
@@ -42,6 +42,7 @@ function renderMessage(id, message, type) {
 // case.
 function check_flow_auth() {
 	var state = Cookies.get("flow_state");
+	window.console.log("check_flow_auth: flow_state=" + state);
 	var url = Cookies.get("auth_url");
 
 	// Redirect to the specified URL
@@ -51,6 +52,15 @@ function check_flow_auth() {
 		return;
 	}
 
+	if (state == "recovery") {
+		Cookies.set("flow_state", "");
+		// Set a custom cookie so the settings page knows we're in
+		// recovery context and can open the right tab.
+		Cookies.set("stackspin_context", "recovery");
+		window.location.href = api_url + '/self-service/settings/browser';
+		return;
+	}
+
 	// Some older stackspin releases, do not provide the dashboard_url,
 	// flask writes 'None' as string in that case, we want to cover those
 	// cases and revert to the default
@@ -76,7 +86,6 @@ function check_flow_expired() {
 function flow_login() {
 	var flow = $.urlParam("flow");
 	var uri = api_url + "self-service/login/flows?id=" + flow;
-
 	// Query the Kratos backend to know what fields to render for the
 	// current flow
 	$.ajax({
@@ -112,7 +121,6 @@ function flow_login() {
 function flow_settings_validate() {
 	var flow = $.urlParam("flow");
 	var uri = api_url + "self-service/settings/flows?id=" + flow;
-
 	$.ajax({
 		type: "GET",
 		url: uri,
@@ -153,6 +161,7 @@ function flow_settings() {
 		url: uri,
 		success: function (data) {
 			var state = Cookies.get("flow_state");
+			var context = Cookies.get("stackspin_context");
 
 			// If we have confirmation the settings are saved, show the
 			// notification
@@ -203,6 +212,12 @@ function flow_settings() {
 					},
 				});
 			});
+
+			// If we are in recovery context, switch to the password tab.
+			if (context == "recovery") {
+				$('#pills-password-tab').tab('show');
+				Cookies.set('stackspin_context', '');
+			}
 		},
 		complete: function (obj) {
 			// If we get a 410, the flow is expired, need to refresh the flow
@@ -211,6 +226,25 @@ function flow_settings() {
 				window.location.href = "settings";
 			}
 		},
+		error: function (xhr, textStatus, errorThrown) {
+			// Check if we got a 403 error from Kratos.
+			if (textStatus == "error" && xhr.status == 403) {
+				var response = $.parseJSON(xhr.responseText);
+				window.console.log(response);
+				if (response.error.id == "session_aal2_required") {
+					// Redirect so user can enter 2FA.
+					window.location.href = response.redirect_browser_to;
+					return;
+                                }
+			}
+			// There was another error, one we don't specifically prepared for.
+			$("#contentProfileSaveFailed").show();
+
+			// For now, this code assumes that only the password can fail
+			// validation. Other forms might need to be added in the future.
+			html = render_form(data, "password", "validation");
+			$("#contentPassword").html(html);
+		},
 	});
 }
 
diff --git a/deployment/helmchart/CHANGELOG.md b/deployment/helmchart/CHANGELOG.md
index c6a7b57464f6bd18369061afdcbb2a5fa9c19dd4..b8a775b3034a7072247467b690755364e579900d 100644
--- a/deployment/helmchart/CHANGELOG.md
+++ b/deployment/helmchart/CHANGELOG.md
@@ -1,5 +1,13 @@
 # Changelog
 
+## [1.7.6]
+
+* Update dashboard to version 0.7.6.
+
+## [1.7.5]
+
+* Update dashboard to version 0.7.5.
+
 ## [1.7.4]
 
 * Update dashboard to version 0.7.4.
diff --git a/deployment/helmchart/Chart.yaml b/deployment/helmchart/Chart.yaml
index 952b004e46ffe3b42250d0742ca2c4ace3decdc8..b402fc65c6023850bb399aed0406680f6be7bd07 100644
--- a/deployment/helmchart/Chart.yaml
+++ b/deployment/helmchart/Chart.yaml
@@ -1,7 +1,7 @@
 annotations:
   category: Dashboard
 apiVersion: v2
-appVersion: 0.7.4
+appVersion: 0.7.6
 dependencies:
   - name: common
     # https://artifacthub.io/packages/helm/bitnami/common
@@ -23,4 +23,4 @@ name: stackspin-dashboard
 sources:
   - https://open.greenhost.net/stackspin/dashboard/
   - https://open.greenhost.net/stackspin/dashboard-backend/
-version: 1.7.5-nosecrets
+version: 1.7.6
diff --git a/deployment/helmchart/values.yaml b/deployment/helmchart/values.yaml
index 6b42b622bf50284996b5b7c216dacb9b2b74fe55..754b8e0a4c2681a74f2cfd4c97b56246e83c0fb4 100644
--- a/deployment/helmchart/values.yaml
+++ b/deployment/helmchart/values.yaml
@@ -68,7 +68,7 @@ dashboard:
   image:
     registry: open.greenhost.net:4567
     repository: stackspin/dashboard/dashboard
-    tag: 143-make-dashboard-secret-generation-generic
+    tag: 0.7.6
     digest: ""
     ## Optionally specify an array of imagePullSecrets.
     ## Secrets must be manually created in the namespace.
@@ -236,7 +236,7 @@ backend:
   image:
     registry: open.greenhost.net:4567
     repository: stackspin/dashboard/dashboard-backend
-    tag: 143-make-dashboard-secret-generation-generic
+    tag: 0.7.6
     digest: ""
     ## Optionally specify an array of imagePullSecrets.
     ## Secrets must be manually created in the namespace.
@@ -723,7 +723,7 @@ tests:
   image:
     registry: open.greenhost.net:4567
     repository: stackspin/dashboard/cypress-test
-    tag: 143-make-dashboard-secret-generation-generic
+    tag: 0.7.6
     pullPolicy: IfNotPresent
   credentials:
     user: ""
diff --git a/docker-compose.yml b/docker-compose.yml
index be2676feaec1edbdef067e114f7765219092e052..d8861940817bcf271a7dc059792c7ba2d57b88fa 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -11,7 +11,7 @@ services:
       - "3000:3000"
     command: "yarn start --watch --verbose"
   stackspin_proxy:
-    image: nginx:1.25.1
+    image: nginx:1.25.2
     ports:
       - "8081:8081"
     volumes:
@@ -57,7 +57,7 @@ services:
       - kube_port_mysql
     entrypoint: ["bash", "-c", "flask run --host $$(hostname -i)"]
   kube_port_kratos_admin:
-    image: bitnami/kubectl:1.27.3
+    image: bitnami/kubectl:1.27.5
     user: "${KUBECTL_UID}:${KUBECTL_GID}"
     expose:
       - 8000
@@ -70,7 +70,7 @@ services:
         "kubectl -n stackspin port-forward --address $$(hostname -i) service/kratos-admin 8000:80",
       ]
   kube_port_hydra_admin:
-    image: bitnami/kubectl:1.27.3
+    image: bitnami/kubectl:1.27.5
     user: "${KUBECTL_UID}:${KUBECTL_GID}"
     expose:
       - 4445
@@ -83,7 +83,7 @@ services:
         "kubectl -n stackspin port-forward --address $$(hostname -i) service/hydra-admin 4445:4445",
       ]
   kube_port_kratos_public:
-    image: bitnami/kubectl:1.27.3
+    image: bitnami/kubectl:1.27.5
     user: "${KUBECTL_UID}:${KUBECTL_GID}"
     ports:
       - "8080:8080"
@@ -98,7 +98,7 @@ services:
         "kubectl -n stackspin port-forward --address 0.0.0.0 service/kratos-public 8080:80",
       ]
   kube_port_mysql:
-    image: bitnami/kubectl:1.27.3
+    image: bitnami/kubectl:1.27.5
     user: "${KUBECTL_UID}:${KUBECTL_GID}"
     expose:
       - 3306
diff --git a/frontend/package.json b/frontend/package.json
index daaa1f9a4e1ff6abe55fc7a6acd7a14494a599cf..a56e177575604dc3873831e890e7b01dc5dacf1a 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,6 +10,8 @@
     "@hookform/resolvers": "^2.6.1",
     "@tailwindcss/forms": "^0.3.3",
     "@tailwindcss/typography": "^0.4.1",
+    "@tanstack/match-sorter-utils": "^8.8.4",
+    "@tanstack/react-table": "^8.9.3",
     "@testing-library/jest-dom": "^5.11.4",
     "@testing-library/react": "^11.1.0",
     "@testing-library/user-event": "^12.1.10",
diff --git a/frontend/public/custom/assets/forgejo.svg b/frontend/public/custom/assets/forgejo.svg
new file mode 100644
index 0000000000000000000000000000000000000000..bcacdc020034d0d1e6db88e9b2377d4c33cbe184
--- /dev/null
+++ b/frontend/public/custom/assets/forgejo.svg
@@ -0,0 +1,27 @@
+<svg viewBox="0 0 212 212" xmlns="http://www.w3.org/2000/svg">
+  <style type="text/css">
+    circle {
+      fill: none;
+      stroke: #000;
+      stroke-width: 15;
+    }
+    path {
+      fill: none;
+      stroke: #000;
+      stroke-width: 25;
+    }
+    .orange {
+      stroke:#ff6600;
+    }
+    .red {
+      stroke:#d40000;
+    }
+  </style>
+  <g transform="translate(6,6)">
+    <path d="M58 168 v-98 a50 50 0 0 1 50-50 h20" class="orange" />
+    <path d="M58 168 v-30 a50 50 0 0 1 50-50 h20" class="red" />
+    <circle cx="142" cy="20" r="18" class="orange" />
+    <circle cx="142" cy="88" r="18" class="red" />
+    <circle cx="58" cy="180" r="18" class="red" />
+  </g>
+</svg>
diff --git a/frontend/public/custom/assets/jitsi.svg b/frontend/public/custom/assets/jitsi.svg
new file mode 100644
index 0000000000000000000000000000000000000000..5a3526ac89ef7578bd411360348ebaddfc5772c5
--- /dev/null
+++ b/frontend/public/custom/assets/jitsi.svg
@@ -0,0 +1,650 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="744.09448819"
+   height="1052.3622047"
+   id="svg5488"
+   version="1.1"
+   inkscape:version="0.47 r22583"
+   sodipodi:docname="New document 5">
+  <defs
+     id="defs5490">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 526.18109 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="744.09448 : 526.18109 : 1"
+       inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
+       id="perspective5496" />
+    <inkscape:perspective
+       id="perspective5347"
+       inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+       inkscape:vp_z="1 : 0.5 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_x="0 : 0.5 : 1"
+       sodipodi:type="inkscape:persp3d" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient8896"
+       id="linearGradient4242"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(-297.23084,320.86007)"
+       x1="439.90353"
+       y1="455.73935"
+       x2="469.71744"
+       y2="557.74847" />
+    <linearGradient
+       id="linearGradient8896">
+      <stop
+         id="stop8898"
+         offset="0"
+         style="stop-color:#0f3060;stop-opacity:1;" />
+      <stop
+         style="stop-color:#0575ce;stop-opacity:1;"
+         offset="0.27115166"
+         id="stop8902" />
+      <stop
+         id="stop12553"
+         offset="0.7327472"
+         style="stop-color:#0575ce;stop-opacity:1;" />
+      <stop
+         style="stop-color:#0f3060;stop-opacity:1;"
+         offset="1"
+         id="stop8900" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient5041"
+       id="linearGradient4244"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(-297.23084,320.86007)"
+       x1="382.80234"
+       y1="585.22589"
+       x2="347.44287"
+       y2="645.02435" />
+    <linearGradient
+       id="linearGradient5041">
+      <stop
+         style="stop-color:#092d61;stop-opacity:1;"
+         offset="0"
+         id="stop5043" />
+      <stop
+         id="stop5047"
+         offset="1"
+         style="stop-color:#0575ce;stop-opacity:1;" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3493"
+       id="linearGradient4246"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(-297.23084,320.86007)"
+       x1="415.81378"
+       y1="620.09808"
+       x2="436.35599"
+       y2="486.43097" />
+    <linearGradient
+       id="linearGradient3493">
+      <stop
+         style="stop-color:#0f3060;stop-opacity:1;"
+         offset="0"
+         id="stop3495" />
+      <stop
+         id="stop3497"
+         offset="0.45698157"
+         style="stop-color:#0575ce;stop-opacity:1;" />
+      <stop
+         style="stop-color:#0575ce;stop-opacity:1;"
+         offset="0.73828435"
+         id="stop3501" />
+      <stop
+         id="stop3507"
+         offset="1"
+         style="stop-color:#0f3060;stop-opacity:1;" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3529"
+       id="linearGradient4248"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.245729,0,0,0.245729,34.35214,723.04037)"
+       x1="389.59329"
+       y1="763.3017"
+       x2="308.98642"
+       y2="420.01578" />
+    <linearGradient
+       id="linearGradient3529">
+      <stop
+         id="stop3531"
+         offset="0"
+         style="stop-color:#ff8000;stop-opacity:1;" />
+      <stop
+         style="stop-color:#fff4e1;stop-opacity:1;"
+         offset="0.6627211"
+         id="stop3533" />
+      <stop
+         id="stop3535"
+         offset="0.75"
+         style="stop-color:#fff4e1;stop-opacity:1;" />
+      <stop
+         id="stop3537"
+         offset="1.0000000"
+         style="stop-color:#ff8400;stop-opacity:1.0000000;" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3591"
+       id="linearGradient4250"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(-297.23084,320.86007)"
+       x1="528.82031"
+       y1="511.71811"
+       x2="458.46918"
+       y2="527.10736" />
+    <linearGradient
+       id="linearGradient3591">
+      <stop
+         style="stop-color:#ffffff;stop-opacity:1;"
+         offset="0"
+         id="stop3593" />
+      <stop
+         style="stop-color:#ffffff;stop-opacity:0;"
+         offset="1"
+         id="stop3595" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient7434"
+       id="linearGradient4252"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.245729,0,0,0.245729,34.35214,723.04037)"
+       x1="197.17841"
+       y1="703.80151"
+       x2="146.54216"
+       y2="785.90436" />
+    <linearGradient
+       id="linearGradient7434">
+      <stop
+         id="stop7436"
+         offset="0"
+         style="stop-color:#ffc768;stop-opacity:1;" />
+      <stop
+         style="stop-color:#ff8400;stop-opacity:1.0000000;"
+         offset="0.37842149"
+         id="stop7438" />
+      <stop
+         id="stop7440"
+         offset="0.77420431"
+         style="stop-color:#ff8400;stop-opacity:1.0000000;" />
+      <stop
+         id="stop7442"
+         offset="1"
+         style="stop-color:#ffc768;stop-opacity:1;" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3591"
+       id="linearGradient4254"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.0334058,0,0,1.0138319,-309.91012,314.18347)"
+       x1="413.63229"
+       y1="484.60083"
+       x2="423.52518"
+       y2="541.83301" />
+    <linearGradient
+       id="linearGradient5384">
+      <stop
+         style="stop-color:#ffffff;stop-opacity:1;"
+         offset="0"
+         id="stop5386" />
+      <stop
+         style="stop-color:#ffffff;stop-opacity:0;"
+         offset="1"
+         id="stop5388" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3509"
+       id="linearGradient4256"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.245729,0,0,0.245729,34.35214,723.04037)"
+       x1="409.69571"
+       y1="559.36359"
+       x2="467.88617"
+       y2="676.03516" />
+    <linearGradient
+       id="linearGradient3509">
+      <stop
+         id="stop3511"
+         offset="0"
+         style="stop-color:#ff8000;stop-opacity:1;" />
+      <stop
+         style="stop-color:#ffc768;stop-opacity:1.0000000;"
+         offset="0.34144846"
+         id="stop3513" />
+      <stop
+         id="stop3515"
+         offset="0.71198046"
+         style="stop-color:#ffc768;stop-opacity:1.0000000;" />
+      <stop
+         id="stop3517"
+         offset="1.0000000"
+         style="stop-color:#ff8400;stop-opacity:1.0000000;" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3651"
+       id="linearGradient4258"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.245729,0,0,0.245729,34.35214,723.04037)"
+       x1="522.63641"
+       y1="168.68723"
+       x2="585.93317"
+       y2="161.10866" />
+    <linearGradient
+       id="linearGradient3651">
+      <stop
+         style="stop-color:#ff8000;stop-opacity:1;"
+         offset="0"
+         id="stop3653" />
+      <stop
+         id="stop16786"
+         offset="0.5"
+         style="stop-color:#fff4e1;stop-opacity:1;" />
+      <stop
+         style="stop-color:#fff4e1;stop-opacity:1;"
+         offset="0.75"
+         id="stop17514" />
+      <stop
+         style="stop-color:#ff8400;stop-opacity:1.0000000;"
+         offset="1.0000000"
+         id="stop3655" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3509"
+       id="linearGradient4260"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.245729,0,0,0.245729,34.35214,723.04037)"
+       x1="481.48975"
+       y1="417.49979"
+       x2="405.41116"
+       y2="316.78976" />
+    <linearGradient
+       id="linearGradient5403">
+      <stop
+         id="stop5405"
+         offset="0"
+         style="stop-color:#ff8000;stop-opacity:1;" />
+      <stop
+         style="stop-color:#ffc768;stop-opacity:1.0000000;"
+         offset="0.34144846"
+         id="stop5407" />
+      <stop
+         id="stop5409"
+         offset="0.71198046"
+         style="stop-color:#ffc768;stop-opacity:1.0000000;" />
+      <stop
+         id="stop5411"
+         offset="1.0000000"
+         style="stop-color:#ff8400;stop-opacity:1.0000000;" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3591"
+       id="linearGradient4262"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(-297.23084,320.86007)"
+       x1="473.09195"
+       y1="499.58444"
+       x2="457.68967"
+       y2="477.5997" />
+    <linearGradient
+       id="linearGradient5414">
+      <stop
+         style="stop-color:#ffffff;stop-opacity:1;"
+         offset="0"
+         id="stop5416" />
+      <stop
+         style="stop-color:#ffffff;stop-opacity:0;"
+         offset="1"
+         id="stop5418" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3591"
+       id="linearGradient4264"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(-297.23084,320.86007)"
+       x1="394.34113"
+       y1="501.80154"
+       x2="446.31302"
+       y2="485.37762" />
+    <linearGradient
+       id="linearGradient5421">
+      <stop
+         style="stop-color:#ffffff;stop-opacity:1;"
+         offset="0"
+         id="stop5423" />
+      <stop
+         style="stop-color:#ffffff;stop-opacity:0;"
+         offset="1"
+         id="stop5425" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3591"
+       id="linearGradient4266"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(-297.23084,320.86007)"
+       x1="395.81326"
+       y1="590.73315"
+       x2="369.55322"
+       y2="572.16907" />
+    <linearGradient
+       id="linearGradient5428">
+      <stop
+         style="stop-color:#ffffff;stop-opacity:1;"
+         offset="0"
+         id="stop5430" />
+      <stop
+         style="stop-color:#ffffff;stop-opacity:0;"
+         offset="1"
+         id="stop5432" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2937"
+       id="linearGradient4268"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.245729,0,0,0.245729,34.35214,723.04037)"
+       x1="297.05316"
+       y1="667.16193"
+       x2="310.45529"
+       y2="713.86633" />
+    <linearGradient
+       id="linearGradient2937">
+      <stop
+         style="stop-color:#212a3a;stop-opacity:1.0000000;"
+         offset="0.0000000"
+         id="stop2939" />
+      <stop
+         style="stop-color:#404e67;stop-opacity:0.0000000;"
+         offset="1.0000000"
+         id="stop2941" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2937"
+       id="linearGradient4270"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.09307993,0,0,0.1860535,131.94157,665.87199)"
+       x1="246.30438"
+       y1="690.02673"
+       x2="366.87921"
+       y2="632.15985" />
+    <linearGradient
+       id="linearGradient5439">
+      <stop
+         style="stop-color:#212a3a;stop-opacity:1.0000000;"
+         offset="0.0000000"
+         id="stop5441" />
+      <stop
+         style="stop-color:#404e67;stop-opacity:0.0000000;"
+         offset="1.0000000"
+         id="stop5443" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2937"
+       id="linearGradient4272"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(-0.09281332,-0.00703915,0.01407026,-0.1855207,186.19996,929.70821)"
+       x1="301.05194"
+       y1="645.89917"
+       x2="367.94604"
+       y2="654.72131" />
+    <linearGradient
+       id="linearGradient5446">
+      <stop
+         style="stop-color:#212a3a;stop-opacity:1.0000000;"
+         offset="0.0000000"
+         id="stop5448" />
+      <stop
+         style="stop-color:#404e67;stop-opacity:0.0000000;"
+         offset="1.0000000"
+         id="stop5450" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2937"
+       id="linearGradient4274"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.245729,0,0,0.245729,98.31046,642.63728)"
+       x1="279.81714"
+       y1="688.32355"
+       x2="386.78625"
+       y2="667.6355" />
+    <linearGradient
+       id="linearGradient5453">
+      <stop
+         style="stop-color:#212a3a;stop-opacity:1.0000000;"
+         offset="0.0000000"
+         id="stop5455" />
+      <stop
+         style="stop-color:#404e67;stop-opacity:0.0000000;"
+         offset="1.0000000"
+         id="stop5457" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2937"
+       id="linearGradient4276"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.245729,0,0,0.245729,25.55056,660.77045)"
+       x1="338.14404"
+       y1="668.3009"
+       x2="266.215"
+       y2="702.89276" />
+    <linearGradient
+       id="linearGradient5460">
+      <stop
+         style="stop-color:#212a3a;stop-opacity:1.0000000;"
+         offset="0.0000000"
+         id="stop5462" />
+      <stop
+         style="stop-color:#404e67;stop-opacity:0.0000000;"
+         offset="1.0000000"
+         id="stop5464" />
+    </linearGradient>
+    <linearGradient
+       y2="702.89276"
+       x2="266.215"
+       y1="668.3009"
+       x1="338.14404"
+       gradientTransform="matrix(0.245729,0,0,0.245729,25.55056,660.77045)"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient5486"
+       xlink:href="#linearGradient2937"
+       inkscape:collect="always" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.35"
+     inkscape:cx="375"
+     inkscape:cy="520"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="798"
+     inkscape:window-height="690"
+     inkscape:window-x="20"
+     inkscape:window-y="20"
+     inkscape:window-maximized="0" />
+  <metadata
+     id="metadata5493">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <g
+       id="g4202"
+       transform="matrix(4.2070673,0,0,4.2070673,-152.93197,-3059.7049)">
+      <path
+         inkscape:export-ydpi="19.25"
+         inkscape:export-xdpi="19.25"
+         inkscape:export-filename="/home/emcho/storage/images/sc_logo/sc_logo136x203.png"
+         sodipodi:nodetypes="csssssssssssssssssssssssssssssssssccssssssssssccsssssssssccsssssssssscssssssssssccssssssscssssssssc"
+         id="path4204"
+         d="m 52.51386,963.95968 c -0.59762,-1.31419 -1.08659,-2.49776 -1.08659,-2.63031 0,-0.13252 -2.08061,-11.07486 -2.31769,-13.88681 -1.03506,-12.27691 -2.66752,-13.45265 -2.21175,-24.39857 0.2761,-6.63061 -0.64392,-8.89235 1.31344,-14.31478 4.03102,-11.16694 6.98662,-15.56085 12.2251,-18.17414 4.24974,-2.12004 8.23695,-1.83207 11.97929,0.86518 0.98634,0.71092 4.29577,2.95048 7.35426,4.97678 3.05851,2.02634 6.69103,4.48296 8.07229,5.45919 1.38125,0.97628 3.68511,2.38497 5.11972,3.13046 2.53058,1.3151 2.62411,1.3351 3.13729,0.67104 1.12761,-1.45907 3.84947,-5.55395 5.32124,-7.50854 7.37209,-9.79052 0.42914,-8.70222 -1.30592,-9.21291 -2.72783,-0.80294 -3.87684,-1.97938 -6.42642,-4.96758 -5.92163,-6.94035 -8.72797,-14.66214 -9.14262,-24.20339 -0.48154,-11.07972 0.63266,-20.01531 5.49789,-25.10088 2.08527,-2.17973 6.49949,-5.34524 10.53505,-7.55495 3.4177,-1.87139 24.37883,-5.78232 26.21837,-4.96975 0.91898,0.40593 -3.63796,-11.63862 -1.01552,-27.29896 0.52096,-3.11103 4.05934,-12.73788 4.43322,-13.39298 0.92313,-1.61744 16.13503,-6.82395 20.46221,-9.36515 4.83486,-2.83935 15.29074,-9.67883 16.07843,-14.76568 1.73099,-11.1786 2.03825,-15.9556 2.75981,-16.37531 1.07388,-0.62465 8.25251,14.17566 3.39352,28.13116 -0.95893,2.75414 -5.53206,9.14865 -7.57975,11.8233 -1.47452,1.92602 1.72775,2.82621 3.56985,5.60679 1.67534,2.52882 2.77675,8.71086 2.9918,11.91416 0.21959,3.27101 -0.81543,9.98235 -1.7425,11.2988 -0.37549,0.53325 -0.52929,1.05709 -0.34387,1.17136 0.18449,0.11368 1.43417,-0.0352 2.77708,-0.33076 1.34289,-0.29559 4.25366,-0.7663 6.46837,-1.046 3.94259,-0.49787 4.08384,-0.49095 6.7595,0.33213 4.01386,1.23476 23.53789,5.95177 11.07947,60.50742 -4.12753,18.07453 -23.38873,41.92489 -25.8106,40.0783 l -5.96407,-4.5474 -6.87325,10.19022 c -3.50798,3.60293 -7.29065,7.08029 -9.52167,8.75312 -3.52051,2.63972 -10.56453,6.79799 -11.51562,6.79799 -0.24492,0 -1.63251,0.56931 -3.08354,1.26516 -4.19508,2.01176 -10.47851,3.66081 -17.13429,4.09795 -25.77135,1.66956 -15.93975,-1.44192 -37.27858,-11.77515 -1.68316,-0.81506 -5.55753,3.29679 -10.22719,7.54571 -4.98524,4.53606 -7.60226,7.98846 -9.57996,12.63792 -0.66947,1.57389 -1.71168,3.98838 -2.31602,5.36552 -1.64444,3.74731 -2.99719,8.93229 -3.19818,12.25814 -0.0983,1.62685 -0.31514,3.04193 -0.48187,3.14469 -0.16673,0.10272 -0.7921,-0.88841 -1.38973,-2.20249 z m 28.33197,-45.90886 c 3.30313,-0.83536 3.66477,-1.01966 7.21765,-3.67825 2.54576,-1.90501 4.12321,-5.3446 4.8004,-5.64508 4.4324,-1.96673 -3.3083,-3.73691 -6.20226,-5.68891 -2.89398,-1.95202 -6.65724,-3.39508 -9.2209,-5.3521 -2.56367,-1.95698 -4.78356,-3.48279 -4.93308,-3.39068 -0.14952,0.0921 -0.27471,1.40551 -0.27821,2.91861 -0.01023,4.41323 -0.6021,8.84164 -1.42935,10.69448 -0.42105,0.94301 -0.84611,2.76087 -0.94461,4.03969 -0.22816,2.96221 0.46109,4.38683 2.91271,6.02036 2.06595,1.3766 2.92385,1.38532 8.07765,0.0819 z m 119.70575,-83.58495 c -1.07558,-7.22468 -0.18498,-17.16053 -5.81444,-20.77728 -12.98767,-8.34415 -14.4135,-0.0696 -15.21021,1.75899 -1.31072,3.0083 -2.11753,5.46892 -1.12273,8.7761 3.05641,10.16086 6.96643,19.15821 8.82255,26.38029 2.13021,8.28856 4.47797,13.53077 4.71288,20.02986 0.16678,4.61378 0.12302,5.09887 -0.55601,6.16474 -0.9655,1.51542 -15.55215,5.22752 -22.89767,7.39176 -2.52211,0.74311 -0.28804,6.12175 -0.57696,7.89239 -0.28892,1.77059 -4.23701,11.66713 -2.96729,13.65682 12.52923,19.63374 40.23506,-40.20634 35.60988,-71.27367 z m -26.98831,48.27201 c 1.95984,-0.47131 3.79219,-1.04626 4.0719,-1.27773 0.27971,-0.23143 0.99066,-0.38419 1.57991,-0.33941 1.05432,0.0801 5.18218,-1.21949 5.4995,-1.73148 0.0894,-0.14415 3.7722,-2.06556 3.0355,-2.55357 -0.73674,-0.48803 -15.13237,-11.07005 -17.19649,-12.14627 -23.77004,-12.39364 -36.26526,-13.24685 -44.11407,-11.66556 -13.44457,2.70866 -25.05657,9.52598 -26.45346,11.20755 -0.42178,0.50774 21.04913,-6.66097 36.28977,0.87085 2.81591,1.39161 8.62965,3.78304 13.00058,8.46269 3.99268,4.27467 7.63329,8.79778 10.49869,9.5617 3.10695,0.82834 9.47337,0.64882 13.78817,-0.38877 z m -28.65731,-51.09687 c 7.52619,-1.317 19.18627,-16.95235 18.27894,-18.41606 -0.12276,-0.19808 2.25527,-4.14459 1.86904,-4.22161 -14.0842,-2.80833 -13.74485,-16.83242 -13.94793,-16.95753 -0.20308,-0.12514 -20.76525,9.71022 -25.50376,11.75869 -4.02092,1.73825 0.57503,12.10208 2.99929,18.53437 1.07855,2.86172 7.01744,6.67785 9.40766,8.23365 2.82773,1.84055 6.25378,1.181 6.89676,1.06849 z m 16.21969,-47.71727 c 4.06405,-3.20289 14.88802,-19.23422 13.73961,-25.73988 -4.67874,-26.5048 -6.69569,-10.0962 -6.99598,-7.73088 -0.12436,0.97963 -0.47868,3.14936 -0.7874,4.82161 -0.30868,1.67227 -0.70224,4.08678 -0.87456,5.36555 -0.1723,1.2788 -0.64865,3.53233 -1.05855,5.00784 -0.4099,1.47554 -1.05724,3.92831 -1.43856,5.45066 -0.38128,1.52232 -1.66486,5.27236 -2.85234,8.33339 -1.18749,3.06104 -2.15908,5.63846 -2.15908,5.72759 0,0.447 0.82393,0.0274 2.42686,-1.23588 z"
+         style="fill:url(#linearGradient4242);fill-opacity:1" />
+      <path
+         sodipodi:nodetypes="ccssc"
+         id="path4206"
+         d="m 53.57922,963.11994 c 6.35309,-26.31992 12.10112,-29.26957 27.22751,-39.0261 -3.32781,-1.89079 -11.51745,-8.1594 -11.49606,-11.49606 0.05726,-9.74197 1.6676,-26.72201 -9.07584,-19.66431 -20.38515,13.39167 -13.0087,55.96766 -6.65561,70.18647 z"
+         style="fill:url(#linearGradient4244);fill-opacity:1;fill-rule:evenodd;stroke:none;display:inline" />
+      <path
+         sodipodi:nodetypes="ccccsc"
+         id="path4208"
+         d="m 75.93257,920.7226 c 8.98173,6.78619 16.91991,12.78559 22.55412,15.96751 54.94138,2.77762 68.66033,-27.59386 69.6583,-52.49321 -4.89006,0.22454 -13.22311,-0.97302 -17.71397,-7.7405 -11.21439,3.68089 -45.16617,14.00314 -45.75692,16.32304 -3.06467,12.03515 -12.17523,21.75574 -28.74153,27.94316 z"
+         style="fill:url(#linearGradient4246);fill-opacity:1;fill-rule:evenodd;stroke:none;display:inline" />
+      <path
+         inkscape:export-ydpi="19.25"
+         inkscape:export-xdpi="19.25"
+         inkscape:export-filename="/home/emcho/storage/images/sc_logo/sc_logo136x203.png"
+         sodipodi:nodetypes="cssssss"
+         id="path4210"
+         d="m 171.27539,910.58112 c 33.18508,-24.69965 28.91369,-74.159 29.44979,-76.69151 1.36299,-6.43866 -6.64955,-28.25155 -16.27383,-24.24947 -16.02424,6.66336 11.2767,44.44456 7.61407,65.1661 -0.76342,4.31907 -13.98948,7.50294 -22.98022,8.89864 -1.03689,0.16096 -0.1163,14.0955 -4.62673,20.50927 -0.64571,0.91818 6.21023,6.81854 6.81692,6.36697 z"
+         style="fill:url(#linearGradient4248);fill-opacity:1;fill-rule:evenodd;stroke:none;display:inline" />
+      <path
+         sodipodi:nodetypes="ccc"
+         id="path4212"
+         d="m 175.90353,907.19115 c 42.49251,-41.74886 20.98218,-116.27813 6.14636,-94.50993 20.37687,-4.1207 23.61854,64.2671 -6.14636,94.50993 z"
+         style="fill:url(#linearGradient4250);fill-opacity:1;fill-rule:evenodd;stroke:none" />
+      <path
+         inkscape:export-ydpi="19.25"
+         inkscape:export-xdpi="19.25"
+         inkscape:export-filename="/home/emcho/storage/images/sc_logo/sc_logo136x203.png"
+         sodipodi:nodetypes="csszs"
+         id="path4214"
+         d="m 95.21705,907.77562 c 0.78602,-1.06057 -13.88152,-6.08284 -22.74639,-14.06472 -0.98292,-0.88501 -1.14144,12.91242 -2.0703,18.66551 -0.57712,3.57451 5.4925,7.17351 6.87977,7.62184 1.15081,0.37191 14.71014,-7.86884 17.93692,-12.22263 z"
+         style="fill:url(#linearGradient4252);fill-opacity:1;fill-rule:evenodd;stroke:none;display:inline" />
+      <path
+         sodipodi:nodetypes="ccccc"
+         id="path4216"
+         d="m 83.58536,868.01334 c -5.099007,-25.84347 4.179267,-41.03939 41.3579,-46.37791 0.18024,0.56879 41.31909,-13.25606 55.79189,-13.35001 -3.92283,6.23323 0.0433,21.62385 4.80916,34.27609 0,0 -34.8276,9.08768 -101.95895,25.45183 z"
+         style="fill:url(#linearGradient4254);fill-opacity:1;fill-rule:evenodd;stroke:none" />
+      <path
+         inkscape:export-ydpi="19.25"
+         inkscape:export-xdpi="19.25"
+         inkscape:export-filename="/home/emcho/storage/images/sc_logo/sc_logo136x203.png"
+         sodipodi:nodetypes="csssss"
+         id="path4218"
+         d="m 101.65537,863.89391 c 4.41182,-1.58073 17.00722,-5.39035 32.05421,-0.1519 10.3057,3.58781 16.2195,12.63312 20.66816,16.112 10.4789,8.19457 29.82289,-0.90843 33.32172,-0.85341 1.43753,0.0226 -25.96694,-24.60193 -46.62844,-25.8478 -27.77538,-1.67482 -45.11147,12.78188 -39.41565,10.74111 z"
+         style="fill:url(#linearGradient4256);fill-opacity:1;fill-rule:evenodd;stroke:none;display:inline" />
+      <path
+         inkscape:export-ydpi="19.25"
+         inkscape:export-xdpi="19.25"
+         inkscape:export-filename="/home/emcho/storage/images/sc_logo/sc_logo136x203.png"
+         sodipodi:nodetypes="csss"
+         id="path4220"
+         d="m 169.42704,741.43522 c -0.1528,8.40398 -2.79669,28.46338 -11.30718,42.1727 -3.89274,6.27072 14.52008,-8.27661 16.50236,-20.78075 1.09029,-6.8775 -5.06881,-28.08959 -5.19518,-21.39195 z"
+         style="fill:url(#linearGradient4258);fill-opacity:1;fill-rule:evenodd;stroke:none" />
+      <path
+         inkscape:export-ydpi="19.25"
+         inkscape:export-xdpi="19.25"
+         inkscape:export-filename="/home/emcho/storage/images/sc_logo/sc_logo136x203.png"
+         sodipodi:nodetypes="cssscsc"
+         id="path4222"
+         d="m 141.79058,833.10697 c 9.23931,-4.71783 17.65654,-10.92192 22.99128,-22.44259 0.9032,-1.9505 -4.39968,-0.0586 -8.47555,-5.84433 -4.0357,-5.72867 -3.08341,-11.54295 -6.54001,-11.07789 -10.80021,1.4531 -25.20082,11.63758 -25.20082,11.63758 0,0 0.82824,11.14408 3.74751,15.67372 6.01898,9.33928 13.47759,12.05351 13.47759,12.05351 z"
+         style="fill:url(#linearGradient4260);fill-opacity:1;fill-rule:evenodd;stroke:none;display:inline" />
+      <path
+         sodipodi:nodetypes="ccc"
+         id="path4224"
+         d="m 156.09152,803.78111 c 2.72579,9.88879 17.87347,8.09668 14.70668,-11.15251 -1.36528,1.99619 -4.77608,12.95694 -14.70668,11.15251 z"
+         style="opacity:0.55223843;fill:url(#linearGradient4262);fill-opacity:1;fill-rule:evenodd;stroke:none" />
+      <path
+         sodipodi:nodetypes="csccsc"
+         id="path4226"
+         d="m 140.48504,832.28297 c -5.10096,-3.46887 -9.50419,-7.78444 -12.32504,-12.34966 -2.82085,-4.56521 -4.0593,-27.84724 0.68685,-33.63375 2.60826,-6.3637 11.84132,-12.51424 22.09721,-15.2308 -19.49494,22.60506 -13.17796,56.86685 4.07359,53.53526 0.86206,-0.16648 -12.82587,7.73026 -14.53261,7.67895 z"
+         style="fill:url(#linearGradient4264);fill-opacity:1;fill-rule:evenodd;stroke:none" />
+      <path
+         sodipodi:nodetypes="ccc"
+         id="path4228"
+         d="m 72.32239,893.02911 c 3.80886,2.81881 20.23882,15.53557 23.85984,13.52306 -8.41018,20.55895 -30.55349,3.14238 -23.85984,-13.52306 z"
+         style="fill:url(#linearGradient4266);fill-opacity:1;fill-rule:evenodd;stroke:none" />
+      <path
+         inkscape:export-ydpi="19.25"
+         inkscape:export-xdpi="19.25"
+         inkscape:export-filename="/home/emcho/storage/images/sc_logo/sc_logo136x203.png"
+         sodipodi:nodetypes="ccsssccssscccsssscscsccsssssccssssssccsssccssssccsscssssssssssssccsssccssssssccssssssssssssscsssscssscssc"
+         id="path4230"
+         d="m 140.05745,777.56053 c -3.94728,2.30056 -8.69302,4.87373 -9.63531,7.20019 -3.16217,6.64336 -4.34745,18.56552 -3.38465,17.88089 9.38401,-6.67285 20.24871,-8.52283 24.20205,-12.0967 11.71489,-10.59045 16.26287,-32.48353 13.80119,-29.2383 -7.96915,10.50564 -20.9111,13.88057 -24.98328,16.25392 z m -61.4085,147.01185 c 0,0 -11.08091,-7.66436 -10.87669,-10.1233 1.697,-20.4327 -1.92942,-21.625 -3.75706,-21.40828 -2.20803,0.26184 -13.6822,7.50553 -15.92058,27.95835 -1.62427,14.84147 4.24013,38.0296 5.13347,39.28426 4.62276,-12.42378 2.81919,-22.42331 25.42086,-35.71103 z m 121.05779,-90.52225 c -0.55133,-15.31772 -9.77382,-25.84979 -16.03746,-23.38322 -2.65737,1.04645 -4.4415,8.17887 -3.50846,12.60125 0.82755,3.92256 6.46634,19.10152 6.87179,20.38471 0.95868,3.03418 7.43087,21.62978 6.28035,30.56471 -1.00846,7.8316 -23.7097,10.5413 -23.7097,10.5413 0,0 -0.38149,6.37449 -1.16407,10.68177 -0.96202,5.29492 -2.86934,9.45033 -2.86934,9.45033 0,0 4.94251,4.60429 5.14224,4.59918 0.19973,-0.005 31.10844,-16.7127 28.99465,-75.44003 z m -23.69666,47.44964 c 1.59781,-0.44722 11.03201,-2.94175 10.52638,-3.27341 -3.50095,-2.2964 -7.48504,-5.18714 -14.1578,-10.37693 -37.04552,-28.81248 -70.69468,-4.54502 -70.03165,-4.76554 6.15153,-2.04594 12.64754,-3.20211 21.67232,-2.5993 7.26918,0.48555 18.69619,4.17029 28.70607,16.10244 3.72328,4.43828 7.94798,7.4497 23.28468,4.91274 z M 159.4851,818.95922 c 1.12085,-0.96962 5.01656,-8.31336 4.31505,-8.48395 -9.83076,-2.39074 -10.78824,-6.74031 -11.75043,-11.4727 -0.90123,-4.43254 -0.76219,-5.06465 -2.62452,-4.44953 -15.04703,4.96997 -19.20919,7.70131 -21.51157,9.61214 -3.60666,2.99331 -1.0768,11.03634 0.33349,14.23298 1.96563,4.4554 9.19232,12.51758 12.55437,13.16681 4.90391,0.94697 16.49344,-9.09974 18.68361,-12.60575 z m 10.93597,-27.14087 c -0.81548,-3.33524 -3.43538,-7.63066 -4.13349,-7.63066 -0.65744,0 -7.61876,5.38666 -9.4168,7.26354 -4.08086,4.25969 -2.09206,11.97893 2.43483,14.62671 7.66207,4.48154 13.28874,-5.37111 11.11546,-14.25959 z m -5.77705,-12.75642 c 3.35221,-3.34862 7.80844,-8.84532 8.99344,-17.19839 0.46443,-3.27384 0.73411,-6.15364 -1.30205,-12.62655 -0.47191,-1.50022 -1.90642,-5.8148 -1.96005,-5.24636 -1.05907,11.22382 -2.61083,23.68653 -9.93627,36.93167 -2.29934,4.15745 -1.06286,3.40174 4.20493,-1.86037 z m -78.6191,66.84814 c -3.99349,16.75103 6.58927,38.75062 15.16215,40.00456 8.28707,1.21213 32.04487,-5.96093 44.50484,-9.42568 3.26456,-0.90778 5.02074,-0.43427 -2.20131,-6.48455 -11.3226,-7.69755 -20.46099,-8.68868 -29.5776,-7.65392 -10.05581,1.14135 -17.59102,4.11772 -16.88606,3.52743 4.61053,-3.86056 12.8225,-10.76401 28.68812,-13.89013 6.89585,-1.35875 17.53788,-1.47808 30.38613,3.54005 15.93675,6.2244 24.23739,15.45542 30.97531,18.58381 2.39935,1.11401 3.25644,0.59604 4.20446,-2.18817 1.5883,-4.66452 -8.06663,-33.78938 -11.56456,-42.28236 -5.33802,-12.96073 0.89004,-19.15557 -0.47125,-18.85551 -6.6027,1.45543 -12.42508,5.69932 -17.306,10.06932 -11.93084,10.68198 -19.20273,12.95753 -20.28189,12.86579 -3.40727,-0.28972 -9.78249,-6.32022 -12.09877,-9.38241 -1.47795,-1.95389 -27.92949,3.15177 -35.72972,9.92827 -0.17845,0.15503 -6.19704,4.01498 -7.80385,11.6435 z M 77.5498,918.8389 c 8.72435,-3.6605 17.26585,-11.43939 16.52709,-11.56787 -4.52939,-0.78772 -21.23379,-12.23383 -21.22226,-12.11514 0.57378,5.90697 -0.71357,11.21473 -1.30092,17.06944 -0.24494,2.44161 3.00975,6.22428 5.99609,6.61357 z m 28.15174,-25.60492 c -1.66901,3.72058 -3.41156,18.3512 -27.53028,27.73991 -0.69864,0.27195 20.21205,14.57036 21.35002,14.62032 69.0544,3.03144 68.46916,-49.55437 67.5526,-50.88016 -0.13265,-0.19188 -6.03889,0.0138 -10.61352,-2.15692 -0.34887,-0.1655 -3.45204,-1.76502 -5.30227,-4.17423 -0.62757,-0.81715 -0.72524,-1.26122 -3.05074,-0.58541 -10.76269,3.12776 -42.89539,14.65696 -42.40581,15.43649 z M 84.18935,867.5847 c -12.33127,-43.08647 30.94329,-45.08535 37.80451,-46.74439 5.30593,-1.28297 4.14977,-1.78189 3.03089,-6.80033 -1.57099,-7.04629 -1.0816,-13.18841 0.26513,-19.77217 1.27351,-6.2258 1.93035,-9.37394 4.61998,-12.49571 2.16789,-2.51621 14.38379,-8.27049 17.4121,-9.72295 12.90529,-6.18974 17.83586,-12.99328 19.27422,-18.34465 1.014,-3.77255 2.49779,-13.07094 1.83081,-19.71727 -0.16416,-1.6359 0.89135,0.92745 6.14205,17.24159 2.68293,8.33593 1.36177,18.34502 -7.09994,27.38172 -1.14918,1.22726 -0.59185,1.4703 0.95354,4.02388 3.21078,5.30554 4.98573,11.93993 4.48566,16.76662 -0.40972,3.95432 -2.10008,7.6934 0.31754,7.40286 0.34951,-0.042 11.02834,-1.65875 18.33848,2.98757 4.95048,3.14653 12.00258,9.17808 10.17204,34.37342 -4.00842,55.17187 -30.9649,67.17892 -31.15387,67.18542 -0.4563,0.0156 -5.49956,-5.22074 -5.79806,-4.60407 -15.45677,31.93103 -47.3506,30.78387 -65.13826,31.11951 -3.55635,0.0671 -18.34444,-12.38469 -19.59088,-11.53286 -15.74294,10.75882 -20.11352,13.44589 -26.18374,40.94381 -0.23264,-0.43595 -8.41803,-14.95561 -8.07826,-44.15673 0.27838,-23.92523 13.34923,-35.21616 19.15997,-34.56498 7.05422,0.79055 10.59443,4.47084 20.52923,10.99795 1.77335,1.16509 8.0388,4.95377 9.46457,4.91926 3.5521,-0.0838 5.97534,-6.60969 6.55509,-10.83484 0.58475,-4.26168 1.15009,-3.23167 -2.17605,-4.37633 -4.1934,-1.44311 -11.42652,-9.296 -15.13675,-21.67633 z"
+         style="fill:#2c3b54;fill-opacity:1;fill-rule:nonzero;stroke:none" />
+      <path
+         inkscape:export-ydpi="19.25"
+         inkscape:export-xdpi="19.25"
+         inkscape:export-filename="/home/emcho/storage/images/sc_logo/sc_logo136x203.png"
+         sodipodi:nodetypes="czssss"
+         id="path4232"
+         d="m 106.11345,892.83442 c 6.47531,-3.53335 43.23312,-15.02538 43.56314,-15.74675 0.24089,-0.52655 -43.11579,17.30875 -52.93537,10.69734 -0.59185,-0.39849 2.29344,1.72127 4.90219,2.25652 0.18326,0.0376 0.17092,6.89208 -2.30068,10.77041 -0.6931,1.08758 5.26729,-7.15715 6.77072,-7.97752 z"
+         style="fill:url(#linearGradient4268);fill-opacity:1;fill-rule:evenodd;stroke:none" />
+      <path
+         inkscape:export-ydpi="19.25"
+         inkscape:export-xdpi="19.25"
+         inkscape:export-filename="/home/emcho/storage/images/sc_logo/sc_logo136x203.png"
+         sodipodi:nodetypes="csss"
+         id="path4234"
+         d="m 166.00752,784.18642 c -4.02929,2.81265 -9.5846,6.74255 -10.72057,9.77333 -0.66996,1.78745 -1.12247,-4.99153 3.45182,-9.03009 2.57716,-2.27533 8.09315,-1.31871 7.26875,-0.74324 z"
+         style="fill:url(#linearGradient4270);fill-opacity:1;fill-rule:evenodd;stroke:none;display:inline" />
+      <path
+         inkscape:export-ydpi="19.25"
+         inkscape:export-xdpi="19.25"
+         inkscape:export-filename="/home/emcho/storage/images/sc_logo/sc_logo136x203.png"
+         sodipodi:nodetypes="csss"
+         id="path4236"
+         d="m 160.58968,806.82846 c 4.57007,1.3627 8.10001,-1.42847 10.03503,-7.4422 0.22874,-0.71087 -0.56553,5.2666 -3.40562,7.48857 -2.70767,2.11837 -7.59288,-0.33365 -6.62941,-0.0464 z"
+         style="fill:url(#linearGradient4272);fill-opacity:1;fill-rule:evenodd;stroke:none;display:inline" />
+      <path
+         inkscape:export-ydpi="19.25"
+         inkscape:export-xdpi="19.25"
+         inkscape:export-filename="/home/emcho/storage/images/sc_logo/sc_logo136x203.png"
+         sodipodi:nodetypes="cscss"
+         id="path4238"
+         d="m 172.18948,813.03639 c 6.47531,-3.53335 13.10498,-2.9926 18.75586,-2.73805 3.75103,0.16896 -7.06591,-4.20527 -19.35477,-3.52147 -1.31254,1.11755 -5.21285,7.63595 -10.70973,14.23704 -0.82526,0.99104 9.80521,-7.15715 11.30864,-7.97752 z"
+         style="fill:url(#linearGradient4274);fill-opacity:1;fill-rule:evenodd;stroke:none;display:inline" />
+      <path
+         inkscape:export-ydpi="19.25"
+         inkscape:export-xdpi="19.25"
+         inkscape:export-filename="/home/emcho/storage/images/sc_logo/sc_logo136x203.png"
+         sodipodi:nodetypes="csccscs"
+         id="path4240"
+         d="m 110.55877,827.01111 c 3.80202,-0.78266 8.57073,-2.07478 11.72698,-2.39299 2.63172,-0.26532 5.92925,-1.74739 8.46259,0.97744 -1.41052,-1.93357 -4.10815,-5.83882 -4.94871,-8.39667 0.76528,3.00553 -2.65492,3.42931 -3.50504,3.56438 -19.11709,3.02037 -30.61745,8.16849 -36.41981,17.74206 10.99033,-8.50263 23.96641,-11.34651 24.68399,-11.49422 z"
+         style="fill:url(#linearGradient5486);fill-opacity:1;fill-rule:evenodd;stroke:none;display:inline" />
+    </g>
+  </g>
+</svg>
diff --git a/frontend/public/custom/assets/mattermost.svg b/frontend/public/custom/assets/mattermost.svg
new file mode 100644
index 0000000000000000000000000000000000000000..2fd174307012c3da2182ba391961479b83925f92
--- /dev/null
+++ b/frontend/public/custom/assets/mattermost.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 1437 1437" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <g transform="matrix(4.16667,0,0,4.16667,-1365.17,-1.88902)">
+        <path d="M601.285,33.35L603.099,69.934C632.758,102.699 644.465,149.102 629.701,192.713C607.663,257.815 534.992,292.037 467.386,269.151C399.78,246.265 362.84,174.936 384.879,109.835C399.692,66.078 437.379,36.272 481.027,28.379L504.609,0.516C431.034,-1.476 361.621,44.193 336.785,117.557C306.27,207.698 354.607,305.509 444.748,336.024C534.889,366.539 632.7,318.202 663.215,228.061C688.011,154.814 660.742,76.504 601.285,33.35Z" style="fill:rgb(29,50,92);"/>
+    </g>
+    <g transform="matrix(4.16667,0,0,4.16667,-1365.17,-1.88902)">
+        <path d="M559.032,141.297L557.783,90.155L556.781,60.726L556.103,35.23C556.103,35.23 556.245,22.936 555.816,20.047C555.725,19.439 555.534,18.945 555.307,18.513C555.279,18.452 555.251,18.392 555.22,18.332C555.188,18.277 555.156,18.226 555.122,18.174C554.651,17.363 553.909,16.704 552.951,16.379C551.969,16.047 550.955,16.128 550.073,16.509C550.055,16.516 550.037,16.523 550.019,16.53C549.914,16.577 549.814,16.63 549.714,16.686C549.296,16.889 548.871,17.153 548.455,17.556C546.359,19.59 539.004,29.442 539.004,29.442L522.979,49.283L504.307,72.052L472.249,111.919C472.249,111.919 457.537,130.28 460.788,152.879C464.039,175.479 480.841,186.489 493.875,190.902C506.91,195.314 526.944,196.775 543.255,180.797C559.565,164.819 559.032,141.297 559.032,141.297Z" style="fill:rgb(29,50,92);"/>
+    </g>
+</svg>
diff --git a/frontend/public/custom/markdown/forgejo.md b/frontend/public/custom/markdown/forgejo.md
new file mode 100644
index 0000000000000000000000000000000000000000..4c5bdde80842207eaf4901879edcfabac74a2af3
--- /dev/null
+++ b/frontend/public/custom/markdown/forgejo.md
@@ -0,0 +1,14 @@
+---
+title: 'Forgejo'
+tileExcerpt: 'Software development tool and version control using Git, including bug tracking, code review, tickets and wikis'
+---
+
+## Introduction
+
+> [Forgejo](https://forgejo.org/) is a self-hosted lightweight software forge. Easy to install and low maintenance, it just does the job.
+
+## Signing in
+
+Forgejo shows some public data without requiring a sign-in. To sign in, first
+click "Sign in" in the top-right corner, then on the resulting Forgejo sign-in
+page click the organisation logo at the bottom saying "Sign in with".
diff --git a/frontend/public/custom/markdown/jitsi.md b/frontend/public/custom/markdown/jitsi.md
new file mode 100644
index 0000000000000000000000000000000000000000..9c8ce191f32e71e5d0e11e56b21b617cbfc50e52
--- /dev/null
+++ b/frontend/public/custom/markdown/jitsi.md
@@ -0,0 +1,12 @@
+---
+title: 'Jitsi'
+tileExcerpt: 'Secure and flexible video conferencing'
+---
+
+## Introduction
+
+> Go ahead, video chat with the whole team. In fact, invite everyone you know. Jitsi Meet is a fully encrypted, 100% open source video conferencing solution that you can use all day, every day, for free — with no account needed. (from the [official website](https://jitsi.org/jitsi-meet/))
+
+## Signing in
+
+Jitsi does not require logins; every video session is end-to-end encrypted and is only visible to people currently on the call.
diff --git a/frontend/public/custom/markdown/mattermost.md b/frontend/public/custom/markdown/mattermost.md
new file mode 100644
index 0000000000000000000000000000000000000000..a044e2d23b75917fd77f2b20e997a4932c50d907
--- /dev/null
+++ b/frontend/public/custom/markdown/mattermost.md
@@ -0,0 +1,8 @@
+---
+title: 'Mattermost'
+tileExcerpt: 'Team collaboration and communication app'
+---
+
+## Introduction
+
+> Work together effectively with real-time communication, file and code snippet sharing, in-line code syntax highlighting, and workflow automation purpose-built for technical teams. From the [official website](https://mattermost.com/)
diff --git a/frontend/src/components/Form/Select/Select.tsx b/frontend/src/components/Form/Select/Select.tsx
index 06a5322f710e625ab06c5cbe4c2b13429fe1e8b9..aad51fa9d97789c95635086d21ff6655aabccda1 100644
--- a/frontend/src/components/Form/Select/Select.tsx
+++ b/frontend/src/components/Form/Select/Select.tsx
@@ -26,7 +26,7 @@ export const Select = ({ control, name, label, options, disabled = false }: Sele
         value={field.value ? field.value : ''} // input value
         name={name} // send down the input name
         ref={field.ref} // send input ref, so we can focus on input when error appear
-        className="block shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md"
+        className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md"
         disabled={disabled}
       >
         {options?.map((option) => (
diff --git a/frontend/src/components/MultiEditUserModal/MultiEditUserModal.tsx b/frontend/src/components/MultiEditUserModal/MultiEditUserModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..37b5614ec5a71d9ef8a3bbc6a2e894d24fa512b9
--- /dev/null
+++ b/frontend/src/components/MultiEditUserModal/MultiEditUserModal.tsx
@@ -0,0 +1,310 @@
+import React, { useEffect, useState } from 'react';
+import _ from 'lodash';
+import { useFieldArray, useForm, useWatch } from 'react-hook-form';
+import { Banner, ConfirmationModal, Modal } from 'src/components';
+import { Select } from 'src/components/Form';
+import { User, UserRole, MultiEditUser, useUsers, NoChange } from 'src/services/users';
+import { useAuth } from 'src/services/auth';
+import { AppStatusEnum } from 'src/services/apps/types';
+
+import { HIDDEN_APPS } from 'src/modules/dashboard/consts';
+// import { initialUserForm } from './consts';
+import { TrashIcon } from '@heroicons/react/outline';
+
+import { MultiEditUserModalProps } from './types';
+
+export const MultiEditUserModal = ({ open, onClose, userIds, setUserId, apps }: MultiEditUserModalProps) => {
+  const [deleteModal, setDeleteModal] = useState(false);
+  const [isAdminRoleSelected, setAdminRoleSelected] = useState(true);
+  const {
+    user,
+    editUserById,
+    deleteUserById,
+    // editMultipleUsers,
+    userModalLoading,
+    clearSelectedUser,
+  } = useUsers();
+  const { currentUser, isAdmin } = useAuth();
+
+  // Extending the app list with "No Change" value, so that
+  // there is a sane default selection
+  // when doing multi-user edits
+
+  interface AppListInt {
+    name: string;
+    role: UserRole | NoChange;
+  }
+  const appList: AppListInt[] = [];
+  const initialAppRoleLatest = () => {
+    apps
+      .filter((app) => app.status !== AppStatusEnum.NotInstalled)
+      .map((app) => appList.push({ name: app.slug, role: NoChange.NoChange }));
+  };
+  initialAppRoleLatest();
+
+  const userIdsList: string[] = [];
+  const populateUserIdsList = () => {
+    userIds.map((id: any) => userIdsList.push(id.original.id));
+  };
+  populateUserIdsList();
+
+  const userNamesList: string[] = [];
+  const populateUserNamesList = () => {
+    userIds.map((id: any) => userNamesList.push(id.original.name));
+  };
+  populateUserNamesList();
+
+  const userEmailsList: string[] = [];
+  const populateUserEmailsList = () => {
+    userIds.map((id: any) => userEmailsList.push(id.original.email));
+  };
+  populateUserEmailsList();
+
+  const initialUserForm = {
+    userEmails: userEmailsList,
+    userIds: userIdsList,
+    app_roles: appList,
+    userNames: userNamesList,
+  };
+
+  // populate the initial "New User" window with installed apps and default roles
+  const { control, reset, handleSubmit } = useForm<MultiEditUser>({
+    defaultValues: initialUserForm,
+  });
+
+  const { fields, update } = useFieldArray({
+    control,
+    name: 'app_roles',
+  });
+
+  useEffect(() => {
+    if (!_.isEmpty(user)) {
+      reset(user);
+    }
+  }, [user, reset, open]);
+
+  const dashboardRole = useWatch({
+    control,
+    name: 'app_roles.0.role',
+  });
+
+  useEffect(() => {
+    const isAdminDashboardRoleSelected = dashboardRole === UserRole.Admin;
+    setAdminRoleSelected(isAdminDashboardRoleSelected);
+    if (isAdminDashboardRoleSelected) {
+      fields.forEach((field, index) => update(index, { name: field.name, role: UserRole.Admin }));
+    } else {
+      fields.forEach((field, index) => update(index, { name: field.name, role: NoChange.NoChange }));
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [dashboardRole]);
+
+  function transformSubmitData(data: MultiEditUser) {
+    const userBatch: any = [];
+    const editedAppList: any = [];
+    const populateEditedAppList = () => {
+      data.app_roles
+        .filter((role) => role.role !== NoChange.NoChange)
+        .map((role) => editedAppList.push({ name: role.name, role: role.role }));
+    };
+    populateEditedAppList();
+    const populateUserBatch = () => {
+      data.userIds.map((userId, index) =>
+        userBatch.push({
+          email: data.userEmails[index],
+          name: data.userNames[index],
+          id: userId,
+          app_roles: editedAppList,
+        }),
+      );
+    };
+    populateUserBatch();
+
+    return userBatch;
+  }
+
+  const handleSave = async () => {
+    try {
+      await handleSubmit((data) => {
+        const transformedData = transformSubmitData(data);
+        // For now, this function loops over users and sends multiple individual PUT requests.
+        // Once the JSON payload schema issue is solved, we can test the batch edit
+        // with the below command
+        // (remember to also uncomment the import on top of this file)
+        // return editMultipleUsers(transformedData);
+        transformedData.forEach((userId: User) => {
+          return editUserById(userId);
+        });
+      })();
+    } catch (e: any) {
+      // Continue
+    }
+
+    onClose();
+    clearSelectedUser();
+  };
+
+  const handleClose = () => {
+    onClose();
+    clearSelectedUser();
+  };
+
+  const deleteModalOpen = () => setDeleteModal(true);
+  const deleteModalClose = () => setDeleteModal(false);
+
+  const handleDelete = () => {
+    userIdsList.forEach((id: string) => {
+      deleteUserById(id);
+    });
+
+    clearSelectedUser();
+    setUserId(null);
+    handleClose();
+    deleteModalClose();
+  };
+
+  // Button with delete option.
+  const buttonDelete = () => {
+    return (
+      !userIdsList.includes(currentUser?.id as string) && (
+        <button
+          onClick={deleteModalOpen}
+          type="button"
+          className="mb-4 sm:mb-0 inline-flex items-center px-4 py-2 text-sm font-medium rounded-md text-red-700 bg-red-50 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
+        >
+          <TrashIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
+          Delete {userIds.length} users
+        </button>
+      )
+    );
+  };
+  return (
+    <>
+      <Modal
+        onClose={handleClose}
+        open={open}
+        onSave={handleSave}
+        isLoading={userModalLoading}
+        leftActions={<>{buttonDelete()}</>}
+        useCancelButton
+      >
+        <div className="bg-white px-4">
+          <div className="space-y-10 divide-y divide-gray-200">
+            <div>
+              <div>
+                <h3 className="text-lg leading-6 font-medium text-gray-900">Edit {userIds.length} users</h3>
+              </div>
+
+              <div className="mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
+                {isAdmin && (
+                  <>
+                    <div className="sm:col-span-3">
+                      {fields
+                        .filter((field) => field.name === 'dashboard')
+                        .map((item, index) => (
+                          <Select
+                            key={item.name}
+                            control={control}
+                            name={`app_roles.${index}.role`}
+                            label="Role"
+                            options={[
+                              { value: UserRole.User, name: 'User' },
+                              { value: UserRole.Admin, name: 'Admin' },
+                            ]}
+                          />
+                        ))}
+                    </div>
+                    <div className="sm:col-span-3 opacity-40 cursor-default pointer-events-none select-none">
+                      <label htmlFor="status" className="block text-sm font-medium text-gray-700">
+                        Status
+                      </label>
+                      <div className="mt-1">
+                        <select
+                          id="status"
+                          name="status"
+                          className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md"
+                        >
+                          <option>Active</option>
+                          <option>Inactive</option>
+                          <option>Banned</option>
+                        </select>
+                      </div>
+                    </div>
+                  </>
+                )}
+              </div>
+            </div>
+            {isAdmin && !userModalLoading && (
+              <div>
+                <div className="mt-8">
+                  <h3 className="text-lg leading-6 font-medium text-gray-900">App Access</h3>
+                </div>
+
+                {isAdminRoleSelected && (
+                  <div className="sm:col-span-6">
+                    <Banner
+                      title="Admin users automatically have admin-level access to all apps."
+                      titleSm="Admin user"
+                    />
+                  </div>
+                )}
+
+                {!isAdminRoleSelected && (
+                  <div>
+                    <div className="flow-root mt-6">
+                      <ul className="-my-5 divide-y divide-gray-200">
+                        {fields.map((item, index) => {
+                          if (item.name != null && HIDDEN_APPS.indexOf(item.name) !== -1) {
+                            return null;
+                          }
+
+                          return (
+                            <li className="py-4" key={item.name}>
+                              <div className="flex items-center space-x-4">
+                                <div className="flex-shrink-0 flex-1 flex items-center">
+                                  <img
+                                    className="h-10 w-10 rounded-md overflow-hidden"
+                                    src={_.find(apps, ['slug', item.name!])?.assetSrc}
+                                    alt={item.name ?? 'Image'}
+                                  />
+                                  <h3 className="ml-4 text-md leading-6 font-medium text-gray-900">
+                                    {_.find(apps, ['slug', item.name!])?.name}
+                                  </h3>
+                                </div>
+                                <div>
+                                  <Select
+                                    key={item.id}
+                                    control={control}
+                                    name={`app_roles.${index}.role`}
+                                    disabled={isAdminRoleSelected}
+                                    options={[
+                                      { value: NoChange.NoChange, name: '...' },
+                                      { value: UserRole.NoAccess, name: 'No Access' },
+                                      { value: UserRole.User, name: 'User' },
+                                      { value: UserRole.Admin, name: 'Admin' },
+                                    ]}
+                                  />
+                                </div>
+                              </div>
+                            </li>
+                          );
+                        })}
+                      </ul>
+                    </div>
+                  </div>
+                )}
+              </div>
+            )}
+          </div>
+        </div>
+      </Modal>
+      <ConfirmationModal
+        onDeleteAction={handleDelete}
+        open={deleteModal}
+        onClose={deleteModalClose}
+        title="Delete user"
+        body={`You are about to delete ${userIds.length} users. Are sure you want to delete them? All of the user data will be permanently removed. This action cannot be undone.`}
+      />
+    </>
+  );
+};
diff --git a/frontend/src/components/MultiEditUserModal/consts.ts b/frontend/src/components/MultiEditUserModal/consts.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bb750df0b4d7952d5c3d3e9a85f9651ce579a09a
--- /dev/null
+++ b/frontend/src/components/MultiEditUserModal/consts.ts
@@ -0,0 +1,96 @@
+// This file is still being used in the AppSingle.tsx file
+// to populate the single app card with URLs and images.
+// Single App is not an active view at the moment,
+// so automating this is not a priority at the moment.
+// once we activate single app views, we will need to use the API call
+// to populate the AppSingle card with this info.
+// See UserModal.tsx for inspiration, search for initialAppRoleLatest()
+
+// import { UserRole } from 'src/services/users';
+
+export const appAccessList = [
+  {
+    name: 'hedgedoc',
+    image: '/assets/hedgedoc.svg',
+    label: 'HedgeDoc',
+    documentationUrl: 'https://docs.hedgedoc.org/',
+  },
+  {
+    name: 'wekan',
+    image: '/assets/wekan.svg',
+    label: 'Wekan',
+    documentationUrl: 'https://github.com/wekan/wekan/wiki',
+  },
+  {
+    name: 'wordpress',
+    image: '/assets/wordpress.svg',
+    label: 'Wordpress',
+    documentationUrl: 'https://wordpress.org/support/',
+  },
+  {
+    name: 'nextcloud',
+    image: '/assets/nextcloud.svg',
+    label: 'Nextcloud',
+    documentationUrl: 'https://docs.nextcloud.com/server/latest/user_manual/en/',
+  },
+  {
+    name: 'zulip',
+    image: '/assets/zulip.svg',
+    label: 'Zulip',
+    documentationUrl: 'https://docs.zulip.com/help/',
+  },
+  {
+    name: 'monitoring',
+    image: '/assets/monitoring.svg',
+    label: 'Monitoring',
+    documentationUrl: 'https://grafana.com/docs/',
+  },
+];
+
+export const allAppAccessList = [
+  {
+    name: 'dashboard',
+    image: '/assets/logo-small.svg',
+    label: 'Dashboard',
+  },
+  ...appAccessList,
+];
+
+// export const initialAppRoles = [
+//   {
+//     name: 'dashboard',
+//     role: UserRole.User,
+//   },
+//   {
+//     name: 'hedgedoc',
+//     role: UserRole.User,
+//   },
+//   {
+//     name: 'wekan',
+//     role: UserRole.User,
+//   },
+//   {
+//     name: 'wordpress',
+//     role: UserRole.User,
+//   },
+//   {
+//     name: 'nextcloud',
+//     role: UserRole.User,
+//   },
+//   {
+//     name: 'zulip',
+//     role: UserRole.User,
+//   },
+//   {
+//     name: 'monitoring',
+//     role: UserRole.NoAccess,
+//   },
+// ];
+
+// export const initialUserForm = {
+//   id: '',
+//   name: '',
+//   email: '',
+//   app_roles: initialAppRoles,
+//   status: '',
+// };
diff --git a/frontend/src/components/MultiEditUserModal/index.ts b/frontend/src/components/MultiEditUserModal/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..15b8809c59059122e88ff8dbd70794cdc9b90302
--- /dev/null
+++ b/frontend/src/components/MultiEditUserModal/index.ts
@@ -0,0 +1 @@
+export { MultiEditUserModal } from './MultiEditUserModal';
diff --git a/frontend/src/components/MultiEditUserModal/types.ts b/frontend/src/components/MultiEditUserModal/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a566c9b96c7af54862236d59caa85bde8ddc9e61
--- /dev/null
+++ b/frontend/src/components/MultiEditUserModal/types.ts
@@ -0,0 +1,9 @@
+import { App } from 'src/services/apps';
+
+export type MultiEditUserModalProps = {
+  open: boolean;
+  onClose: () => void;
+  userIds: any;
+  setUserId: any;
+  apps: App[];
+};
diff --git a/frontend/src/components/Table/Table.tsx b/frontend/src/components/Table/Table.tsx
index 95f22567a3a0760f239c38ab0f8a03b6f1e8840c..e8f46a5492cdb3533c469ab3d31ba4fcf5cfa981 100644
--- a/frontend/src/components/Table/Table.tsx
+++ b/frontend/src/components/Table/Table.tsx
@@ -1,6 +1,6 @@
 import { ArrowSmDownIcon, ArrowSmUpIcon } from '@heroicons/react/solid';
 import React, { useEffect } from 'react';
-import { useTable, useRowSelect, Column, IdType, useSortBy } from 'react-table';
+import { useTable, useRowSelect, Column, IdType, useSortBy, usePagination } from 'react-table';
 
 export interface ReactTableProps<T extends Record<string, unknown>> {
   columns: Column<T>[];
@@ -38,7 +38,7 @@ export const Table = <T extends Record<string, unknown>>({
   pagination = false,
   onRowClick,
   getSelectedRowIds,
-  selectable = false,
+  selectable = true,
   loading = false,
 }: ReactTableProps<T>) => {
   const {
@@ -55,6 +55,7 @@ export const Table = <T extends Record<string, unknown>>({
       data,
     },
     useSortBy,
+    usePagination,
     useRowSelect,
     selectable
       ? (hooks) => {
diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts
index 64e430d54ff3ea8a1af2dae1ab0feac9e7c82450..954427fdfa6348af31f2fbf716ad77ba6b7eb874 100644
--- a/frontend/src/components/index.ts
+++ b/frontend/src/components/index.ts
@@ -6,3 +6,4 @@ export { Tabs } from './Tabs';
 export { Modal, ConfirmationModal, InfoModal, StepsModal } from './Modal';
 export { UserModal } from './UserModal';
 export { ProgressSteps } from './ProgressSteps';
+export { MultiEditUserModal } from './MultiEditUserModal';
diff --git a/frontend/src/modules/users/Users.tsx b/frontend/src/modules/users/Users.tsx
index 157b4cda9e61e2fe753134413a401a20551ce7e3..b8bbefe45de85fd13cd35ee777d61c56393ac7c9 100644
--- a/frontend/src/modules/users/Users.tsx
+++ b/frontend/src/modules/users/Users.tsx
@@ -4,25 +4,64 @@
  *
  * Admin users can add one or more users, or edit a user.
  */
-import React, { useState, useCallback, useEffect, useMemo } from 'react';
-import { SearchIcon, PlusIcon, ViewGridAddIcon } from '@heroicons/react/solid';
-import { CogIcon, TrashIcon } from '@heroicons/react/outline';
-import { useUsers } from 'src/services/users';
-import { Table } from 'src/components';
-import { debounce } from 'lodash';
+
+// React main
+import React, { useState, useCallback, useEffect, useMemo, HTMLProps } from 'react';
+
+// Icons
+import {
+  SearchIcon,
+  PlusIcon,
+  ViewGridAddIcon,
+  ChevronDownIcon,
+  ChevronUpIcon,
+  ChevronDoubleLeftIcon,
+  ChevronDoubleRightIcon,
+  ChevronLeftIcon,
+  ChevronRightIcon,
+} from '@heroicons/react/solid';
+import { CogIcon } from '@heroicons/react/outline';
+
+// API - Redux
+import { useUsers, User } from 'src/services/users';
 import { useAuth } from 'src/services/auth';
 import { useApps } from 'src/services/apps';
 
+// Regular Table
+// import { Table } from 'src/components';
+import { debounce } from 'lodash';
+
+// User Table
+import {
+  // Column,
+  ColumnDef,
+  flexRender,
+  getCoreRowModel,
+  getFilteredRowModel,
+  getPaginationRowModel,
+  getSortedRowModel,
+  SortingState,
+  // Table,
+  useReactTable,
+} from '@tanstack/react-table';
+
+import { MultiEditUserModal } from 'src/components';
+
+// Local components
 import { UserModal } from '../../components/UserModal';
 import { MultipleUsersModal } from './components';
 
+// ///////////////////////////////////////////
+
 export const Users: React.FC = () => {
-  const [selectedRowsIds, setSelectedRowsIds] = useState({});
+  // const [selectedRowsIds, setSelectedRowsIds] = useState({});
   const [configureModal, setConfigureModal] = useState(false);
   const [multipleUsersModal, setMultipleUsersModal] = useState(false);
+  const [multiEditUserModal, setMultiEditUserModal] = useState(false);
   const [userId, setUserId] = useState(null);
+  const [multiUserIds, setMultiUserIds] = useState(null);
   const [search, setSearch] = useState('');
-  const { users, loadUsers, userTableLoading } = useUsers();
+  const { users, loadUsers } = useUsers();
   const { isAdmin } = useAuth();
 
   const { apps, loadApps } = useApps();
@@ -46,7 +85,11 @@ export const Users: React.FC = () => {
   }, []);
 
   const filterSearch = useMemo(() => {
-    return users.filter((item: any) => item.email?.toLowerCase().includes(search.toLowerCase()));
+    return users.filter(
+      (item: any) =>
+        item.email?.toLowerCase().includes(search.toLowerCase()) ||
+        item.name?.toLowerCase().includes(search.toLowerCase()),
+    );
   }, [search, users]);
 
   const configureModalOpen = (id: any) => {
@@ -54,87 +97,143 @@ export const Users: React.FC = () => {
     setConfigureModal(true);
   };
 
+  const multiEditUserModalOpen = (ids: any) => {
+    setMultiUserIds(ids);
+    setMultiEditUserModal(true);
+  };
+
   const configureModalClose = () => setConfigureModal(false);
 
   const multipleUsersModalClose = () => setMultipleUsersModal(false);
 
-  const columns: any = React.useMemo(
-    () => [
-      {
-        Header: 'Name',
-        accessor: 'name',
-        width: 'auto',
-      },
-      {
-        Header: 'Email',
-        accessor: 'email',
-        width: 'auto',
-      },
-      {
-        Header: 'Status',
-        accessor: 'status',
-        width: 'auto',
-      },
-      {
-        Header: ' ',
-        Cell: (props: any) => {
-          const { row } = props;
-
-          if (isAdmin) {
-            return (
-              <div className="text-right lg:opacity-0 group-hover:opacity-100 transition-opacity">
-                <button
-                  onClick={() => configureModalOpen(row.original.id)}
-                  type="button"
-                  className="inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
-                >
-                  <CogIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
-                  Configure
-                </button>
-              </div>
-            );
-          }
+  const multiEditUserModalClose = () => setMultiEditUserModal(false);
 
-          return null;
-        },
-        width: 'auto',
-      },
-    ],
-    [isAdmin],
-  );
+  // const selectedRows = useCallback((rows: Record<string, boolean>) => {
+  //   setSelectedRowsIds(rows);
+  // }, []);
 
-  const selectedRows = useCallback((rows: Record<string, boolean>) => {
-    setSelectedRowsIds(rows);
-  }, []);
+  // ////////////////////////
+  // New Table Start
+  // ////////////////////////
 
-  return (
-    <div className="relative">
-      <div className="max-w-7xl mx-auto py-4 px-3 sm:px-6 lg:px-8 h-full flex-grow">
-        <div className="pb-5 mt-6 border-b border-gray-200 sm:flex sm:items-center sm:justify-between">
-          <h1 className="text-3xl leading-6 font-bold text-gray-900">Users</h1>
+  function IndeterminateCheckbox({
+    indeterminate,
+    ...rest
+  }: { indeterminate?: boolean } & HTMLProps<HTMLInputElement>) {
+    const ref = React.useRef<HTMLInputElement>(null!);
 
-          {isAdmin && (
-            <div className="mt-3 sm:mt-0 sm:ml-4">
-              <button
-                onClick={() => configureModalOpen(null)}
-                type="button"
-                className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800 mx-5 "
-              >
-                <PlusIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
-                Add new user
-              </button>
-              <button
-                onClick={() => setMultipleUsersModal(true)}
-                type="button"
-                className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800"
-              >
-                <ViewGridAddIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
-                Add new users
-              </button>
+    React.useEffect(() => {
+      if (typeof indeterminate === 'boolean') {
+        ref.current.indeterminate = !rest.checked && indeterminate;
+      }
+    }, [ref, indeterminate]);
+
+    return (
+      <input
+        type="checkbox"
+        ref={ref}
+        className="focus:ring-primary-800 h-4 w-4 text-primary-700 border-gray-300 rounded cursor-pointer"
+        {...rest}
+      />
+    );
+  }
+  function CreateUserTable() {
+    const [rowSelection, setRowSelection] = React.useState({});
+    const [sorting, setSorting] = React.useState<SortingState>([]);
+    const userColumns = React.useMemo<ColumnDef<User>[]>(
+      () => [
+        {
+          id: 'select',
+          header: ({ table }) => (
+            <IndeterminateCheckbox
+              {...{
+                checked: table.getIsAllRowsSelected(),
+                indeterminate: table.getIsSomeRowsSelected(),
+                onChange: table.getToggleAllRowsSelectedHandler(),
+              }}
+            />
+          ),
+          cell: ({ row }) => (
+            <div className="flex items-center">
+              <IndeterminateCheckbox
+                {...{
+                  checked: row.getIsSelected(),
+                  disabled: !row.getCanSelect(),
+                  indeterminate: row.getIsSomeSelected(),
+                  onChange: row.getToggleSelectedHandler(),
+                }}
+              />
             </div>
-          )}
-        </div>
+          ),
+        },
+        {
+          header: 'Name',
+          footer: (props) => props.column.id,
+          accessorKey: 'name',
+        },
+        {
+          header: 'Email',
+          footer: (props) => props.column.id,
+          accessorKey: 'email',
+        },
+        {
+          header: 'Status',
+          footer: (props) => props.column.id,
+          accessorKey: 'status',
+        },
+        {
+          header: ' ',
+          cell: (props: any) => {
+            const { row } = props;
+
+            if (isAdmin) {
+              return (
+                <div className="text-right relative">
+                  <div className="absolute inline-flex px-2 py-1 text-transparent items-center font-medium border border-transparent right-0 z-0">
+                    Configure <CogIcon className="-mr-0.5 ml-2 h-4 w-4 text-gray-500" />
+                  </div>
+                  <button
+                    onClick={() => configureModalOpen(row.original.id)}
+                    type="button"
+                    className="relative z-10 opacity-0 group-hover:opacity-100 transition-opacity inline-flex items-center px-2 py-1 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
+                  >
+                    Configure <CogIcon className="-mr-0.5 ml-2 h-4 w-4" />
+                  </button>
+                </div>
+              );
+            }
+
+            return null;
+          },
+        },
+      ],
+      [isAdmin],
+    );
 
+    const table = useReactTable({
+      data: filterSearch,
+      columns: userColumns,
+      state: {
+        rowSelection,
+        sorting,
+      },
+      initialState: {
+        pagination: {
+          pageSize: 10,
+        },
+      },
+      onSortingChange: setSorting,
+      getSortedRowModel: getSortedRowModel(),
+      enableRowSelection: true, // enable row selection for all rows
+      onRowSelectionChange: setRowSelection,
+      getCoreRowModel: getCoreRowModel(),
+      getFilteredRowModel: getFilteredRowModel(),
+      getPaginationRowModel: getPaginationRowModel(),
+      debugTable: false, // make true if needed
+    });
+
+    return (
+      <>
         <div className="flex justify-between w-100 my-3 items-center mb-5 ">
           <div className="flex items-center">
             <div className="inline-block">
@@ -151,7 +250,7 @@ export const Users: React.FC = () => {
                     name="email"
                     id="email"
                     className="focus:ring-primary-500 focus:border-primary-500 block w-full rounded-md pl-10 sm:text-sm border-gray-200"
-                    placeholder="Search Users"
+                    placeholder="Search everything..."
                     onChange={debouncedSearch}
                   />
                 </div>
@@ -159,21 +258,194 @@ export const Users: React.FC = () => {
             </div>
           </div>
 
-          {selectedRowsIds && Object.keys(selectedRowsIds).length !== 0 && (
+          {Object.keys(rowSelection).length !== 0 && (
             <div className="flex items-center">
               <button
-                onClick={() => {}}
+                onClick={() => multiEditUserModalOpen(table.getSelectedRowModel().flatRows)}
                 type="button"
-                className="inline-flex items-center px-4 py-2 text-sm font-medium rounded-md text-red-700 bg-red-50 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
+                className="inline-flex items-center px-4 py-2 text-sm font-medium rounded-md text-gray-500 bg-primary-50 hover:bg-primary-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 border border-gray-200 shadow-sm"
               >
-                <TrashIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
-                Delete
+                <CogIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
+                Configure {Object.keys(rowSelection).length} user{Object.keys(rowSelection).length !== 1 ? 's' : null}
               </button>
             </div>
           )}
         </div>
+        <div className="flex justify-between items-center text-xs font-medium text-gray-500 uppercase tracking-wider">
+          <div className="py-3 text-left ">
+            Showing {table.getRowModel().rows.length} of {table.getCoreRowModel().rows.length} entries
+          </div>
+          <nav className="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
+            <button
+              className="relative inline-flex items-center rounded-l-md px-2 py-2 ring-1 ring-inset ring-gray-300 transition hover:bg-gray-100 focus:z-20 focus:outline-offset-0"
+              onClick={() => table.setPageIndex(0)}
+              disabled={!table.getCanPreviousPage()}
+            >
+              <ChevronDoubleLeftIcon className="w-4 h-4" />
+            </button>
+            <button
+              className="relative inline-flex items-center px-2 py-2 ring-1 ring-inset ring-gray-300 transition hover:bg-gray-100 focus:z-20 focus:outline-offset-0"
+              onClick={() => table.previousPage()}
+              disabled={!table.getCanPreviousPage()}
+            >
+              <ChevronLeftIcon className="w-4 h-4" />
+            </button>
+            <div className="relative inline-flex items-center px-2 py-2 ring-1 ring-inset ring-gray-300  focus:z-20 focus:outline-offset-0">
+              <span>
+                Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
+              </span>
+            </div>
+            <button
+              className="relative inline-flex items-center px-2 py-2 ring-1 ring-inset ring-gray-300 transition hover:bg-gray-100 focus:z-20 focus:outline-offset-0"
+              onClick={() => table.nextPage()}
+              disabled={!table.getCanNextPage()}
+            >
+              <ChevronRightIcon className="w-4 h-4" />
+            </button>
+            <button
+              className="relative inline-flex items-center rounded-r-md px-2 py-2 ring-1 ring-inset ring-gray-300 transition hover:bg-gray-100 focus:z-20 focus:outline-offset-0"
+              onClick={() => table.setPageIndex(table.getPageCount() - 1)}
+              disabled={!table.getCanNextPage()}
+            >
+              <ChevronDoubleRightIcon className="w-4 h-4" />
+            </button>
+          </nav>
+          <div className="flex items-center gap-2 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
+            <span>Results per page</span>
+            <select
+              value={table.getState().pagination.pageSize}
+              className="focus:ring-primary-500 focus:border-primary-500 py-1 block rounded-md text-sm border-gray-200"
+              onChange={(e) => {
+                table.setPageSize(Number(e.target.value));
+              }}
+            >
+              {[10, 20, 50, 100, 250, 500, 10000, table.getCoreRowModel().rows.length].map((pageSize) =>
+                pageSize <= table.getCoreRowModel().rows.length ? (
+                  <option key={pageSize} value={pageSize}>
+                    {pageSize === table.getCoreRowModel().rows.length ? `all` : `${pageSize}`}
+                  </option>
+                ) : null,
+              )}
+            </select>
+          </div>
+        </div>
+        <div className="shadow border-b border-gray-200 sm:rounded-lg overflow-hidden">
+          <table className="min-w-full divide-y divide-gray-200 table-auto">
+            <thead className="bg-gray-50">
+              {table.getHeaderGroups().map((headerGroup) => (
+                <tr key={headerGroup.id}>
+                  {headerGroup.headers.map((header) => {
+                    return (
+                      <th
+                        key={header.id}
+                        colSpan={header.colSpan}
+                        scope="col"
+                        className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
+                      >
+                        {header.isPlaceholder ? null : (
+                          <div
+                            {...{
+                              className: header.column.getCanSort() ? 'flex items-center' : '',
+                              onClick: header.column.getToggleSortingHandler(),
+                            }}
+                          >
+                            <span> {flexRender(header.column.columnDef.header, header.getContext())}</span>
+                            {{
+                              asc: <ChevronUpIcon className="w-4 h-4 text-gray-400 ml-1" />,
+                              desc: <ChevronDownIcon className="w-4 h-4 text-gray-400 ml-1" />,
+                            }[header.column.getIsSorted() as string] ?? null}
+                          </div>
+                        )}
+                      </th>
+                    );
+                  })}
+                </tr>
+              ))}
+            </thead>
+            <tbody className="">
+              {table.getRowModel().rows.map((row, rowIndex) => {
+                return (
+                  <tr
+                    key={row.id}
+                    role="row"
+                    className={
+                      rowIndex % 2 === 0
+                        ? 'bg-white group border-l-4 border-transparent transition hover:border-primary-600'
+                        : 'bg-gray-50 group border-l-4 border-transparent transition hover:border-primary-600'
+                    }
+                  >
+                    {row.getVisibleCells().map((cell) => {
+                      return (
+                        <td
+                          key={cell.id}
+                          className={
+                            cell.id.substring(2) === 'select'
+                              ? 'w-4 px-6 py-4 whitespace-nowrap text-sm text-gray-500'
+                              : 'px-6 py-4 whitespace-nowrap text-sm text-gray-500'
+                          }
+                        >
+                          {flexRender(cell.column.columnDef.cell, cell.getContext())}
+                        </td>
+                      );
+                    })}
+                  </tr>
+                );
+              })}
+            </tbody>
+          </table>
+          {/* Debugging buttons below, uncomment if needee */}
+          {/* <hr />
+          <br />
+          <div>
+            <button className="border rounded p-2 mb-2" onClick={() => console.info('rowSelection', rowSelection)}>
+              Log `rowSelection` state
+            </button>
+          </div>
+          <div>
+            <button
+              className="border rounded p-2 mb-2"
+              onClick={() => console.info('table.getSelectedRowModel().flatRows', table.getSelectedRowModel().flatRows)}
+            >
+              Log table.getSelectedRowModel().flatRows
+            </button>
+          </div> */}
+        </div>
+      </>
+    );
+  }
 
-        <div className="flex flex-col">
+  // ////////////////////////
+  // New Table End
+  // ////////////////////////
+  return (
+    <div className="relative">
+      <div className="max-w-7xl mx-auto py-4 px-3 sm:px-6 lg:px-8 h-full flex-grow">
+        <div className="pb-5 mt-6 border-b border-gray-200 sm:flex sm:items-center sm:justify-between">
+          <h1 className="text-3xl leading-6 font-bold text-gray-900">Users</h1>
+
+          {isAdmin && (
+            <div className="mt-3 sm:mt-0 sm:ml-4">
+              <button
+                onClick={() => configureModalOpen(null)}
+                type="button"
+                className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800 mx-5 "
+              >
+                <PlusIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
+                Add new user
+              </button>
+              <button
+                onClick={() => setMultipleUsersModal(true)}
+                type="button"
+                className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800"
+              >
+                <ViewGridAddIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
+                Add new users
+              </button>
+            </div>
+          )}
+        </div>
+
+        {/* <div className="flex flex-col">
           <div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
             <div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
               <div className="shadow border-b border-gray-200 sm:rounded-lg overflow-hidden">
@@ -186,6 +458,12 @@ export const Users: React.FC = () => {
               </div>
             </div>
           </div>
+        </div> */}
+
+        <div className="flex flex-col">
+          <div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
+            <div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">{CreateUserTable()}</div>
+          </div>
         </div>
 
         {configureModal && (
@@ -200,6 +478,15 @@ export const Users: React.FC = () => {
         {multipleUsersModal && (
           <MultipleUsersModal open={multipleUsersModal} onClose={multipleUsersModalClose} apps={apps} />
         )}
+        {multiEditUserModal && (
+          <MultiEditUserModal
+            open={multiEditUserModal}
+            onClose={multiEditUserModalClose}
+            apps={apps}
+            setUserId={setUserId}
+            userIds={multiUserIds}
+          />
+        )}
       </div>
     </div>
   );
diff --git a/frontend/src/modules/users/components/MultipleUsersModal/MultipleUsersModal.tsx b/frontend/src/modules/users/components/MultipleUsersModal/MultipleUsersModal.tsx
index 65b92c7348b1854f7d399aacf52d63be2eaee1b4..cab59812b7f73b9babddd79840bbf532dd0bbedd 100644
--- a/frontend/src/modules/users/components/MultipleUsersModal/MultipleUsersModal.tsx
+++ b/frontend/src/modules/users/components/MultipleUsersModal/MultipleUsersModal.tsx
@@ -4,6 +4,7 @@ import { useFieldArray, useForm, useWatch } from 'react-hook-form';
 
 import { Banner, StepsModal, ProgressSteps } from 'src/components';
 import { Select, TextArea } from 'src/components/Form';
+import { HIDDEN_APPS } from 'src/modules/dashboard/consts';
 import { MultipleUsersData, UserRole, useUsers } from 'src/services/users';
 import { AppStatusEnum } from 'src/services/apps';
 import { ProgressStepInfo, ProgressStepStatus } from 'src/components/ProgressSteps/types';
@@ -133,7 +134,7 @@ export const MultipleUsersModal = ({ open, onClose, apps }: MultipleUsersModalPr
                 ))}
               {!isAdminRoleSelected &&
                 fields.map((item, index) => {
-                  if (item.name === 'dashboard') {
+                  if (item.name != null && HIDDEN_APPS.indexOf(item.name) !== -1) {
                     return null;
                   }
 
diff --git a/frontend/src/services/users/hooks/use-users.ts b/frontend/src/services/users/hooks/use-users.ts
index 447bcce90e083a48866ad1a26b9e0e6b788225e1..cf189fc899b70be2da788eb9621bd953ea3e2265 100644
--- a/frontend/src/services/users/hooks/use-users.ts
+++ b/frontend/src/services/users/hooks/use-users.ts
@@ -5,6 +5,7 @@ import {
   fetchUserById,
   fetchPersonalInfo,
   updateUserById,
+  updateMultipleUsers,
   updatePersonalInfo,
   createUser,
   deleteUser,
@@ -42,6 +43,10 @@ export function useUsers() {
     return dispatch(updateUserById(data));
   }
 
+  function editMultipleUsers(data: any) {
+    return dispatch(updateMultipleUsers(data));
+  }
+
   function editPersonalInfo(data: any) {
     return dispatch(updatePersonalInfo(data));
   }
@@ -69,6 +74,7 @@ export function useUsers() {
     loadUsers,
     loadPersonalInfo,
     editUserById,
+    editMultipleUsers,
     editPersonalInfo,
     userModalLoading,
     userTableLoading,
diff --git a/frontend/src/services/users/redux/actions.ts b/frontend/src/services/users/redux/actions.ts
index 9fc8770d4ec06109a73a736c0a9aa9e4e0ae0159..394a93407288cbd18e7af40f6e1661c419715c89 100644
--- a/frontend/src/services/users/redux/actions.ts
+++ b/frontend/src/services/users/redux/actions.ts
@@ -7,8 +7,10 @@ import { AuthActionTypes } from 'src/services/auth';
 import {
   transformBatchResponse,
   transformRequestMultipleUsers,
+  transformRequestUpdateMultipleUsers,
   transformRequestUser,
   transformUser,
+  transformUpdateMultipleUsers,
   transformRecoveryLink,
 } from '../transformations';
 
@@ -22,6 +24,7 @@ export enum UserActionTypes {
   SET_USER_MODAL_LOADING = 'users/user_modal_loading',
   SET_USERS_LOADING = 'users/users_loading',
   CREATE_BATCH_USERS = 'users/create_batch_users',
+  UPDATE_MULTIPLE_USERS = '/users/multi-edit',
 }
 
 export const setUsersLoading = (isLoading: boolean) => (dispatch: Dispatch<any>) => {
@@ -132,6 +135,35 @@ export const updateUserById = (user: any) => async (dispatch: Dispatch<any>, get
   dispatch(setUserModalLoading(false));
 };
 
+// ////////////////////
+
+export const updateMultipleUsers = (users: any) => async (dispatch: Dispatch<any>) => {
+  dispatch(setUserModalLoading(true));
+
+  try {
+    const { data } = await performApiCall({
+      path: '/users/multi-edit',
+      method: 'PUT',
+      body: transformRequestUpdateMultipleUsers(users),
+    });
+
+    dispatch({
+      type: UserActionTypes.UPDATE_MULTIPLE_USERS,
+      payload: transformUpdateMultipleUsers(data),
+    });
+
+    showToast('Users updated successfully.', ToastType.Success);
+
+    dispatch(fetchUsers());
+  } catch (err) {
+    console.error(err);
+  }
+
+  dispatch(setUserModalLoading(false));
+};
+
+// /////////////////////
+
 export const updatePersonalInfo = (user: any) => async (dispatch: Dispatch<any>) => {
   dispatch(setUserModalLoading(true));
 
diff --git a/frontend/src/services/users/transformations.ts b/frontend/src/services/users/transformations.ts
index 545c947580639e57a95b3c89ad3b2a0d3a88632a..3d4a0ecb85084b1f5ede794f1c8a9c569446cb52 100644
--- a/frontend/src/services/users/transformations.ts
+++ b/frontend/src/services/users/transformations.ts
@@ -64,6 +64,22 @@ export const transformRequestUser = (data: Pick<User, 'app_roles' | 'name' | 'em
   };
 };
 
+export const transformUpdateMultipleUsers = (response: any): any => {
+  return {
+    success: response.success,
+    existing: response.existing,
+    failed: response.failed,
+  };
+};
+
+export const transformRequestUpdateMultipleUsers = (data: any) => {
+  return {
+    users: _.map(data, (user: Pick<User, 'email' | 'id' | 'app_roles'>) => {
+      return { email: user.email ?? '', id: user.id ?? '', app_roles: user.app_roles.map(transformRequestAppRoles) };
+    }),
+  };
+};
+
 const extractUsersFromCsv = (csvData: string) => {
   const csvRows = csvData.split('\n');
 
diff --git a/frontend/src/services/users/types.ts b/frontend/src/services/users/types.ts
index d22811cba75d1cd8aa84c983de14f632df089dda..bdc6cf7303ab798375668315d26eafcea9a8ed66 100644
--- a/frontend/src/services/users/types.ts
+++ b/frontend/src/services/users/types.ts
@@ -18,11 +18,20 @@ export enum UserRole {
   User = 'user',
 }
 
+export enum NoChange {
+  NoChange = 'no_change',
+}
+
 export interface AppRoles {
   name: string | null;
   role: UserRole | null;
 }
 
+export interface MultiEditAppRoles {
+  name: string | null;
+  role: UserRole | NoChange | null;
+}
+
 export interface UserApiRequest {
   id: number | null;
   email: string;
@@ -34,3 +43,11 @@ export interface MultipleUsersData {
   csvUserData: string;
   appRoles: AppRoles[];
 }
+
+export interface MultiEditUser {
+  userIds: string[];
+  userEmails: string[];
+  userNames: string[];
+  app_roles: MultiEditAppRoles[];
+  status: string;
+}
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 92616b45cbf0ea6149aced0eea9bab148cb745a3..2614f4c42f57c14c73a10a0e5fb38acd81c94226 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -1716,6 +1716,25 @@
     lodash.merge "^4.6.2"
     lodash.uniq "^4.5.0"
 
+"@tanstack/match-sorter-utils@^8.8.4":
+  version "8.8.4"
+  resolved "https://registry.yarnpkg.com/@tanstack/match-sorter-utils/-/match-sorter-utils-8.8.4.tgz#0b2864d8b7bac06a9f84cb903d405852cc40a457"
+  integrity sha512-rKH8LjZiszWEvmi01NR72QWZ8m4xmXre0OOwlRGnjU01Eqz/QnN+cqpty2PJ0efHblq09+KilvyR7lsbzmXVEw==
+  dependencies:
+    remove-accents "0.4.2"
+
+"@tanstack/react-table@^8.9.3":
+  version "8.9.3"
+  resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.9.3.tgz#03a52e9e15f65c82a8c697a445c42bfca0c5cfc4"
+  integrity sha512-Ng9rdm3JPoSCi6cVZvANsYnF+UoGVRxflMb270tVj0+LjeT/ZtZ9ckxF6oLPLcKesza6VKBqtdF9mQ+vaz24Aw==
+  dependencies:
+    "@tanstack/table-core" "8.9.3"
+
+"@tanstack/table-core@8.9.3":
+  version "8.9.3"
+  resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.9.3.tgz#991da6b015f6200fdc841c48048bee5e197f6a46"
+  integrity sha512-NpHZBoHTfqyJk0m/s/+CSuAiwtebhYK90mDuf5eylTvgViNOujiaOaxNDxJkQQAsVvHWZftUGAx1EfO1rkKtLg==
+
 "@testing-library/dom@^7.28.1":
   version "7.31.2"
   resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.31.2.tgz#df361db38f5212b88555068ab8119f5d841a8c4a"
@@ -10791,6 +10810,11 @@ remark-rehype@^9.0.0:
     mdast-util-to-hast "^11.0.0"
     unified "^10.0.0"
 
+remove-accents@0.4.2:
+  version "0.4.2"
+  resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5"
+  integrity sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==
+
 remove-trailing-separator@^1.0.1:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"