Repository: DefGuard/defguard (language: rust)

Issue:
Disable configuration of vanilla WG clients
In the Settings "Enterprise features" tab add a "toggle" for enabling/disabling users the ability to configure vanilla (manual) clients.

If ability to configure vanilla/manual clients is disabled:

- [x] We skip the screen when choosing which type of client the user wants to configure (defguard/manual) and go straight to defguard client configuration
- [x] On the defguard client configuration screen, since there is just the URL and token, we need to add the following information as help above:

```
Please download defguard desktop client [here](https://defguard.net/download),
then follow [this guide](https://defguard.gitbook.io/defguard/help/configuring-vpn/add-new-instance) to configure your client.
```


Discussion / hints:

Fix patch:
diff --git a/.sqlx/query-bcba1892b88c1aecf5fb4112c03b4ec9ffc2f61e38c74630dac5e32f3736c113.json b/.sqlx/query-205507ac888563e614157c6977abb744104284cb6bc7070127797bd6df0ac7af.json
similarity index 53%
rename from .sqlx/query-bcba1892b88c1aecf5fb4112c03b4ec9ffc2f61e38c74630dac5e32f3736c113.json
rename to .sqlx/query-205507ac888563e614157c6977abb744104284cb6bc7070127797bd6df0ac7af.json
index ba99ad563..749e67273 100644
--- a/.sqlx/query-bcba1892b88c1aecf5fb4112c03b4ec9ffc2f61e38c74630dac5e32f3736c113.json
+++ b/.sqlx/query-205507ac888563e614157c6977abb744104284cb6bc7070127797bd6df0ac7af.json
@@ -1,6 +1,6 @@
 {
   "db_name": "PostgreSQL",
-  "query": "SELECT id \"id?\", \"admin_device_management\" FROM \"enterprisesettings\"",
+  "query": "SELECT id \"id?\", \"admin_device_management\",\"only_client_activation\" FROM \"enterprisesettings\"",
   "describe": {
     "columns": [
       {
@@ -12,15 +12,21 @@
         "ordinal": 1,
         "name": "admin_device_management",
         "type_info": "Bool"
+      },
+      {
+        "ordinal": 2,
+        "name": "only_client_activation",
+        "type_info": "Bool"
       }
     ],
     "parameters": {
       "Left": []
     },
     "nullable": [
+      false,
       false,
       false
     ]
   },
-  "hash": "bcba1892b88c1aecf5fb4112c03b4ec9ffc2f61e38c74630dac5e32f3736c113"
+  "hash": "205507ac888563e614157c6977abb744104284cb6bc7070127797bd6df0ac7af"
 }
diff --git a/.sqlx/query-226259d2006f1a0bc904f5a25f9bbc71c19a867b626493b9f973a5cc09f5e3c0.json b/.sqlx/query-35950188e70029c8314055621857ea0091c302e9c27de798a2f50901ffd747e0.json
similarity index 60%
rename from .sqlx/query-226259d2006f1a0bc904f5a25f9bbc71c19a867b626493b9f973a5cc09f5e3c0.json
rename to .sqlx/query-35950188e70029c8314055621857ea0091c302e9c27de798a2f50901ffd747e0.json
index 0fda229cc..3fa364d32 100644
--- a/.sqlx/query-226259d2006f1a0bc904f5a25f9bbc71c19a867b626493b9f973a5cc09f5e3c0.json
+++ b/.sqlx/query-35950188e70029c8314055621857ea0091c302e9c27de798a2f50901ffd747e0.json
@@ -1,15 +1,16 @@
 {
   "db_name": "PostgreSQL",
-  "query": "UPDATE \"enterprisesettings\" SET \"admin_device_management\" = $2 WHERE id = $1",
+  "query": "UPDATE \"enterprisesettings\" SET \"admin_device_management\" = $2,\"only_client_activation\" = $3 WHERE id = $1",
   "describe": {
     "columns": [],
     "parameters": {
       "Left": [
         "Int8",
+        "Bool",
         "Bool"
       ]
     },
     "nullable": []
   },
-  "hash": "226259d2006f1a0bc904f5a25f9bbc71c19a867b626493b9f973a5cc09f5e3c0"
+  "hash": "35950188e70029c8314055621857ea0091c302e9c27de798a2f50901ffd747e0"
 }
diff --git a/.sqlx/query-a1836fa232271fdd5d61f0d34048741a269f654ef6eb1fa2f6b95e68ed7e04e1.json b/.sqlx/query-6332fe33694ebd090eba2ef0777ea91e86978f827cbff94a6a28338a7b0bf6ee.json
similarity index 54%
rename from .sqlx/query-a1836fa232271fdd5d61f0d34048741a269f654ef6eb1fa2f6b95e68ed7e04e1.json
rename to .sqlx/query-6332fe33694ebd090eba2ef0777ea91e86978f827cbff94a6a28338a7b0bf6ee.json
index 31a8e79fb..b861d4934 100644
--- a/.sqlx/query-a1836fa232271fdd5d61f0d34048741a269f654ef6eb1fa2f6b95e68ed7e04e1.json
+++ b/.sqlx/query-6332fe33694ebd090eba2ef0777ea91e86978f827cbff94a6a28338a7b0bf6ee.json
@@ -1,6 +1,6 @@
 {
   "db_name": "PostgreSQL",
-  "query": "SELECT id \"id?\", \"admin_device_management\" FROM \"enterprisesettings\" WHERE id = $1",
+  "query": "SELECT id \"id?\", \"admin_device_management\",\"only_client_activation\" FROM \"enterprisesettings\" WHERE id = $1",
   "describe": {
     "columns": [
       {
@@ -12,6 +12,11 @@
         "ordinal": 1,
         "name": "admin_device_management",
         "type_info": "Bool"
+      },
+      {
+        "ordinal": 2,
+        "name": "only_client_activation",
+        "type_info": "Bool"
       }
     ],
     "parameters": {
@@ -20,9 +25,10 @@
       ]
     },
     "nullable": [
+      false,
       false,
       false
     ]
   },
-  "hash": "a1836fa232271fdd5d61f0d34048741a269f654ef6eb1fa2f6b95e68ed7e04e1"
+  "hash": "6332fe33694ebd090eba2ef0777ea91e86978f827cbff94a6a28338a7b0bf6ee"
 }
diff --git a/.sqlx/query-c0883840b92550c4a2ba682d15ee4841bf9cdce2d87fa952457b157afbe2d756.json b/.sqlx/query-bd0665f9401b5919b55e8b68ef238b514c12becf8e9dfbb8641b02e4deb8b5e5.json
similarity index 66%
rename from .sqlx/query-c0883840b92550c4a2ba682d15ee4841bf9cdce2d87fa952457b157afbe2d756.json
rename to .sqlx/query-bd0665f9401b5919b55e8b68ef238b514c12becf8e9dfbb8641b02e4deb8b5e5.json
index 9c7724501..b2dae11eb 100644
--- a/.sqlx/query-c0883840b92550c4a2ba682d15ee4841bf9cdce2d87fa952457b157afbe2d756.json
+++ b/.sqlx/query-bd0665f9401b5919b55e8b68ef238b514c12becf8e9dfbb8641b02e4deb8b5e5.json
@@ -1,6 +1,6 @@
 {
   "db_name": "PostgreSQL",
-  "query": "INSERT INTO \"enterprisesettings\" (\"admin_device_management\") VALUES ($1) RETURNING id",
+  "query": "INSERT INTO \"enterprisesettings\" (\"admin_device_management\",\"only_client_activation\") VALUES ($1,$2) RETURNING id",
   "describe": {
     "columns": [
       {
@@ -11,6 +11,7 @@
     ],
     "parameters": {
       "Left": [
+        "Bool",
         "Bool"
       ]
     },
@@ -18,5 +19,5 @@
       false
     ]
   },
-  "hash": "c0883840b92550c4a2ba682d15ee4841bf9cdce2d87fa952457b157afbe2d756"
+  "hash": "bd0665f9401b5919b55e8b68ef238b514c12becf8e9dfbb8641b02e4deb8b5e5"
 }
diff --git a/migrations/20240830094910_wireguard_manual_setup_setting.down.sql b/migrations/20240830094910_wireguard_manual_setup_setting.down.sql
new file mode 100644
index 000000000..4db133f79
--- /dev/null
+++ b/migrations/20240830094910_wireguard_manual_setup_setting.down.sql
@@ -0,0 +1,1 @@
+ALTER TABLE enterprisesettings DROP COLUMN only_client_activation;
diff --git a/migrations/20240830094910_wireguard_manual_setup_setting.up.sql b/migrations/20240830094910_wireguard_manual_setup_setting.up.sql
new file mode 100644
index 000000000..491044d25
--- /dev/null
+++ b/migrations/20240830094910_wireguard_manual_setup_setting.up.sql
@@ -0,0 +1,1 @@
+ALTER TABLE enterprisesettings ADD COLUMN only_client_activation BOOLEAN NOT NULL DEFAULT false;
diff --git a/proto b/proto
index c71f37847..1b8f7c5cf 160000
--- a/proto
+++ b/proto
@@ -1,1 +1,1 @@
-Subproject commit c71f37847279ee23220fcf9e0e45d2c365b3b8ee
+Subproject commit 1b8f7c5cfccf7e4f3a7a8422215c9db1d6fcc68e
diff --git a/src/enterprise/db/models/enterprise_settings.rs b/src/enterprise/db/models/enterprise_settings.rs
index 2d97e2c66..cebf54e74 100644
--- a/src/enterprise/db/models/enterprise_settings.rs
+++ b/src/enterprise/db/models/enterprise_settings.rs
@@ -11,6 +11,8 @@ pub struct EnterpriseSettings {
     pub id: Option<i64>,
     // If true, only admins can manage devices
     pub admin_device_management: bool,
+    // If true, manual WireGuard setup is disabled
+    pub only_client_activation: bool,
 }
 
 // We want to be conscious of what the defaults are here
@@ -20,6 +22,7 @@ impl Default for EnterpriseSettings {
         Self {
             id: None,
             admin_device_management: false,
+            only_client_activation: false,
         }
     }
 }
diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs
index 59e902750..60ce1d536 100644
--- a/src/grpc/enrollment.rs
+++ b/src/grpc/enrollment.rs
@@ -21,6 +21,7 @@ use crate::{
         },
         DbPool, Device, GatewayEvent, Settings, User,
     },
+    enterprise::db::models::enterprise_settings::EnterpriseSettings,
     handlers::{mail::send_new_device_added_email, user::check_password_strength},
     headers::get_device_info,
     ldap::utils::ldap_add_user,
@@ -191,6 +192,17 @@ impl EnrollmentServer {
             debug!("Admin info {admin_info:?}");
 
             debug!("Create a new enrollment response.");
+            let enterprise_settings =
+                EnterpriseSettings::get(&mut *transaction)
+                    .await
+                    .map_err(|_| {
+                        error!("Failed to get enterprise settings");
+                        Status::internal("unexpected error")
+                    })?;
+            let enrollment_settings = super::proto::Settings {
+                vpn_setup_optional,
+                only_client_activation: enterprise_settings.only_client_activation,
+            };
             let response = super::proto::EnrollmentStartResponse {
                 admin: admin_info,
                 user: Some(user_info),
@@ -198,8 +210,8 @@ impl EnrollmentServer {
                 final_page_content: enrollment
                     .get_welcome_page_content(&mut transaction)
                     .await?,
-                vpn_setup_optional,
                 instance: Some(instance_info.into()),
+                settings: Some(enrollment_settings),
             };
             debug!("Response {response:?}");
 
diff --git a/web/src/components/Navigation/components/ApplicationVersion/ApplicationVersion.tsx b/web/src/components/Navigation/components/ApplicationVersion/ApplicationVersion.tsx
index 5df8a7724..a6c88d7b6 100644
--- a/web/src/components/Navigation/components/ApplicationVersion/ApplicationVersion.tsx
+++ b/web/src/components/Navigation/components/ApplicationVersion/ApplicationVersion.tsx
@@ -24,7 +24,9 @@ export const ApplicationVersion = ({ isOpen }: Props) => {
           <a rel="noreferrer" href={`https://github.com/DefGuard/defguard/releases/`}>
             {isOpen
               ? LL.navigation.version.open({ version })
-              : LL.navigation.version.closed({ version })}
+              : LL.navigation.version.closed({
+                  version: version.split('-')[0],
+                })}
           </a>
         </p>
       )}
diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts
index 960b92475..57cb72d09 100644
--- a/web/src/i18n/en/index.ts
+++ b/web/src/i18n/en/index.ts
@@ -443,6 +443,7 @@ const en: BaseTranslation = {
     title: 'Add device',
     helpers: {
       setupOpt: `You can add a device using this wizard. Opt for our native application "defguard" or any other WireGuard client. If you're unsure, we recommend using defguard for simplicity.`,
+      client: `Please download defguard desktop client <a href="https://defguard.net/download" target="_blank">here</a> and then follow <a href="https://defguard.gitbook.io/defguard/help/configuring-vpn/add-new-instance" target="_blank">this guide</a>.`,
     },
     messages: {
       deviceAdded: 'Device added',
@@ -1216,6 +1217,11 @@ const en: BaseTranslation = {
           helper:
             "When this option is enabled, only users in the Admin group can manage devices in user profile (it's disabled for all other users)",
         },
+        manualConfig: {
+          label: 'Disable users ability to download manual WireGuard configuration',
+          helper:
+            "When this option is enabled, users won't be presented with a WireGuard configuration for manual client setup",
+        },
       },
     },
   },
@@ -1727,7 +1733,7 @@ const en: BaseTranslation = {
       },
     },
     messageBox:
-      'Enrollment is process by which the new employee will be able to confirm their new account, create a password and configurate VPN device. In this panel you can custom messages for it.',
+      'Enrollment is a process by which a new employee will be able to activate their new account, create a password and configure a VPN device. You can customize it here.',
     settings: {
       welcomeMessage: {
         title: 'Welcome message',
diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts
index 47721301a..7d0dcc7bb 100644
--- a/web/src/i18n/i18n-types.ts
+++ b/web/src/i18n/i18n-types.ts
@@ -1034,6 +1034,10 @@ type RootTranslation = {
 			 * Y​o​u​ ​c​a​n​ ​a​d​d​ ​a​ ​d​e​v​i​c​e​ ​u​s​i​n​g​ ​t​h​i​s​ ​w​i​z​a​r​d​.​ ​O​p​t​ ​f​o​r​ ​o​u​r​ ​n​a​t​i​v​e​ ​a​p​p​l​i​c​a​t​i​o​n​ ​"​d​e​f​g​u​a​r​d​"​ ​o​r​ ​a​n​y​ ​o​t​h​e​r​ ​W​i​r​e​G​u​a​r​d​ ​c​l​i​e​n​t​.​ ​I​f​ ​y​o​u​'​r​e​ ​u​n​s​u​r​e​,​ ​w​e​ ​r​e​c​o​m​m​e​n​d​ ​u​s​i​n​g​ ​d​e​f​g​u​a​r​d​ ​f​o​r​ ​s​i​m​p​l​i​c​i​t​y​.
 			 */
 			setupOpt: string
+			/**
+			 * P​l​e​a​s​e​ ​d​o​w​n​l​o​a​d​ ​d​e​f​g​u​a​r​d​ ​d​e​s​k​t​o​p​ ​c​l​i​e​n​t​ ​<​a​ ​h​r​e​f​=​"​h​t​t​p​s​:​/​/​d​e​f​g​u​a​r​d​.​n​e​t​/​d​o​w​n​l​o​a​d​"​ ​t​a​r​g​e​t​=​"​_​b​l​a​n​k​"​>​h​e​r​e​<​/​a​>​ ​a​n​d​ ​t​h​e​n​ ​f​o​l​l​o​w​ ​<​a​ ​h​r​e​f​=​"​h​t​t​p​s​:​/​/​d​e​f​g​u​a​r​d​.​g​i​t​b​o​o​k​.​i​o​/​d​e​f​g​u​a​r​d​/​h​e​l​p​/​c​o​n​f​i​g​u​r​i​n​g​-​v​p​n​/​a​d​d​-​n​e​w​-​i​n​s​t​a​n​c​e​"​ ​t​a​r​g​e​t​=​"​_​b​l​a​n​k​"​>​t​h​i​s​ ​g​u​i​d​e​<​/​a​>​.
+			 */
+			client: string
 		}
 		messages: {
 			/**
@@ -2883,6 +2887,16 @@ type RootTranslation = {
 					 */
 					helper: string
 				}
+				manualConfig: {
+					/**
+					 * D​i​s​a​b​l​e​ ​u​s​e​r​s​ ​a​b​i​l​i​t​y​ ​t​o​ ​d​o​w​n​l​o​a​d​ ​m​a​n​u​a​l​ ​W​i​r​e​G​u​a​r​d​ ​c​o​n​f​i​g​u​r​a​t​i​o​n
+					 */
+					label: string
+					/**
+					 * W​h​e​n​ ​t​h​i​s​ ​o​p​t​i​o​n​ ​i​s​ ​e​n​a​b​l​e​d​,​ ​u​s​e​r​s​ ​w​o​n​'​t​ ​b​e​ ​p​r​e​s​e​n​t​e​d​ ​w​i​t​h​ ​a​ ​W​i​r​e​G​u​a​r​d​ ​c​o​n​f​i​g​u​r​a​t​i​o​n​ ​f​o​r​ ​m​a​n​u​a​l​ ​c​l​i​e​n​t​ ​s​e​t​u​p
+					 */
+					helper: string
+				}
 			}
 		}
 	}
@@ -4082,7 +4096,7 @@ type RootTranslation = {
 			}
 		}
 		/**
-		 * E​n​r​o​l​l​m​e​n​t​ ​i​s​ ​p​r​o​c​e​s​s​ ​b​y​ ​w​h​i​c​h​ ​t​h​e​ ​n​e​w​ ​e​m​p​l​o​y​e​e​ ​w​i​l​l​ ​b​e​ ​a​b​l​e​ ​t​o​ ​c​o​n​f​i​r​m​ ​t​h​e​i​r​ ​n​e​w​ ​a​c​c​o​u​n​t​,​ ​c​r​e​a​t​e​ ​a​ ​p​a​s​s​w​o​r​d​ ​a​n​d​ ​c​o​n​f​i​g​u​r​a​t​e​ ​V​P​N​ ​d​e​v​i​c​e​.​ ​I​n​ ​t​h​i​s​ ​p​a​n​e​l​ ​y​o​u​ ​c​a​n​ ​c​u​s​t​o​m​ ​m​e​s​s​a​g​e​s​ ​f​o​r​ ​i​t​.
+		 * E​n​r​o​l​l​m​e​n​t​ ​i​s​ ​a​ ​p​r​o​c​e​s​s​ ​b​y​ ​w​h​i​c​h​ ​a​ ​n​e​w​ ​e​m​p​l​o​y​e​e​ ​w​i​l​l​ ​b​e​ ​a​b​l​e​ ​t​o​ ​a​c​t​i​v​a​t​e​ ​t​h​e​i​r​ ​n​e​w​ ​a​c​c​o​u​n​t​,​ ​c​r​e​a​t​e​ ​a​ ​p​a​s​s​w​o​r​d​ ​a​n​d​ ​c​o​n​f​i​g​u​r​e​ ​a​ ​V​P​N​ ​d​e​v​i​c​e​.​ ​Y​o​u​ ​c​a​n​ ​c​u​s​t​o​m​i​z​e​ ​i​t​ ​h​e​r​e​.
 		 */
 		messageBox: string
 		settings: {
@@ -5222,6 +5236,10 @@ export type TranslationFunctions = {
 			 * You can add a device using this wizard. Opt for our native application "defguard" or any other WireGuard client. If you're unsure, we recommend using defguard for simplicity.
 			 */
 			setupOpt: () => LocalizedString
+			/**
+			 * Please download defguard desktop client <a href="https://defguard.net/download" target="_blank">here</a> and then follow <a href="https://defguard.gitbook.io/defguard/help/configuring-vpn/add-new-instance" target="_blank">this guide</a>.
+			 */
+			client: () => LocalizedString
 		}
 		messages: {
 			/**
@@ -7061,6 +7079,16 @@ export type TranslationFunctions = {
 					 */
 					helper: () => LocalizedString
 				}
+				manualConfig: {
+					/**
+					 * Disable users ability to download manual WireGuard configuration
+					 */
+					label: () => LocalizedString
+					/**
+					 * When this option is enabled, users won't be presented with a WireGuard configuration for manual client setup
+					 */
+					helper: () => LocalizedString
+				}
 			}
 		}
 	}
@@ -8249,7 +8277,7 @@ export type TranslationFunctions = {
 			}
 		}
 		/**
-		 * Enrollment is process by which the new employee will be able to confirm their new account, create a password and configurate VPN device. In this panel you can custom messages for it.
+		 * Enrollment is a process by which a new employee will be able to activate their new account, create a password and configure a VPN device. You can customize it here.
 		 */
 		messageBox: () => LocalizedString
 		settings: {
diff --git a/web/src/i18n/pl/index.ts b/web/src/i18n/pl/index.ts
index 1d6a739a3..877dd5cf3 100644
--- a/web/src/i18n/pl/index.ts
+++ b/web/src/i18n/pl/index.ts
@@ -448,7 +448,9 @@ const pl: Translation = {
     },
     helpers: {
       setupOpt: `Możesz dodać urządzenie używając naszego klienta lub samemu skonfigurwać urządzenie.`,
+      client: `Pobierz klienta defguard <a href="https://defguard.net/download" target="_blank">tutaj</a>, a następnie postępuj zgodnie z <a href="https://defguard.gitbook.io/defguard/help/configuring-vpn/add-new-instance" target="_blank">instrukcją</a> w celu jego konfiguracji.`,
     },
+
     steps: {
       setupDevice: {
         title: 'Dodaj urządzenie',
@@ -1202,6 +1204,11 @@ Uwaga, podane tutaj konfiguracje nie posiadają klucza prywatnego. Musisz uzupe
           helper:
             'Kiedy ta opcja jest włączona, tylko użytkownicy w grupie "Admin" mogą zarządzać urządzeniami w profilu użytkownika',
         },
+        manualConfig: {
+          label: 'Wyłącz wyświetlanie konfiguracji WireGuard',
+          helper:
+            'Kiedy ta opcja jest włączona, konfiguracja WireGuard nie będzie wyświetlana użytkownikom podczas konfiguracji urządzenia.',
+        },
       },
     },
   },
diff --git a/web/src/pages/addDevice/AddDevicePage.tsx b/web/src/pages/addDevice/AddDevicePage.tsx
index b21b828ab..1e2a70e1b 100644
--- a/web/src/pages/addDevice/AddDevicePage.tsx
+++ b/web/src/pages/addDevice/AddDevicePage.tsx
@@ -16,6 +16,7 @@ import {
   ButtonSize,
   ButtonStyleVariant,
 } from '../../shared/defguard-ui/components/Layout/Button/types';
+import { useAppStore } from '../../shared/hooks/store/useAppStore';
 import { useAddDevicePageStore } from './hooks/useAddDevicePageStore';
 import { AddDeviceConfigStep } from './steps/AddDeviceConfigStep/AddDeviceConfigStep';
 import { AddDeviceSetupMethodStep } from './steps/AddDeviceSetupMethodStep/AddDeviceSetupMethodStep';
@@ -29,6 +30,7 @@ export const AddDevicePage = () => {
   const navigate = useNavigate();
 
   const userData = useAddDevicePageStore((state) => state.userData);
+  const enterpriseSettings = useAppStore((state) => state.enterprise_settings);
 
   const [currentStep, setupMethod] = useAddDevicePageStore(
     (state) => [state.currentStep, state.method],
@@ -68,6 +70,7 @@ export const AddDevicePage = () => {
               onClick={() => {
                 navigate('/', { replace: true });
               }}
+              disabled={currentStep === 0 && enterpriseSettings?.only_client_activation}
             />
             <Button
               data-testid="next-step"
@@ -85,6 +88,7 @@ export const AddDevicePage = () => {
                 />
               }
               onClick={() => nextSubject.next()}
+              disabled={currentStep === 0 && enterpriseSettings?.only_client_activation}
             />
           </div>
         </header>
diff --git a/web/src/pages/addDevice/steps/AddDeviceSetupMethodStep/AddDeviceSetupMethodStep.tsx b/web/src/pages/addDevice/steps/AddDeviceSetupMethodStep/AddDeviceSetupMethodStep.tsx
index 3bb8d8bbf..7d3aaa985 100644
--- a/web/src/pages/addDevice/steps/AddDeviceSetupMethodStep/AddDeviceSetupMethodStep.tsx
+++ b/web/src/pages/addDevice/steps/AddDeviceSetupMethodStep/AddDeviceSetupMethodStep.tsx
@@ -8,8 +8,11 @@ import { useI18nContext } from '../../../../i18n/i18n-react';
 import SvgDefguardNavLogo from '../../../../shared/components/svg/DefguardNavLogo';
 import SvgWireguardLogo from '../../../../shared/components/svg/WireguardLogo';
 import { Card } from '../../../../shared/defguard-ui/components/Layout/Card/Card';
+import { LoaderSpinner } from '../../../../shared/defguard-ui/components/Layout/LoaderSpinner/LoaderSpinner';
 import { MessageBox } from '../../../../shared/defguard-ui/components/Layout/MessageBox/MessageBox';
 import { MessageBoxType } from '../../../../shared/defguard-ui/components/Layout/MessageBox/types';
+import useEffectOnce from '../../../../shared/helpers/useEffectOnce';
+import { useAppStore } from '../../../../shared/hooks/store/useAppStore';
 import useApi from '../../../../shared/hooks/useApi';
 import { externalLink } from '../../../../shared/links';
 import { useAddDevicePageStore } from '../../hooks/useAddDevicePageStore';
@@ -26,6 +29,7 @@ export const AddDeviceSetupMethodStep = () => {
   const methodRef = useRef(setupMethod);
 
   const userData = useAddDevicePageStore((state) => state.userData);
+  const enterpriseSettings = useAppStore((state) => state.enterprise_settings);
 
   const [setPageState, next, nextSubject] = useAddDevicePageStore(
     (state) => [state.setState, state.nextStep, state.nextSubject],
@@ -69,43 +73,58 @@ export const AddDeviceSetupMethodStep = () => {
     setPageState({ loading: isLoading });
   }, [isLoading, setPageState]);
 
+  useEffectOnce(() => {
+    if (enterpriseSettings?.only_client_activation) {
+      setPageState({ method: AddDeviceMethod.DESKTOP });
+      nextSubject.next();
+    }
+  });
+
   return (
     <>
-      <MessageBox
-        type={MessageBoxType.WARNING}
-        message={LL.addDevicePage.helpers.setupOpt()}
-        dismissId="add-device-page-method-opt-message"
-      />
-      <Card shaded id="setup-method-step">
-        <DeviceSetupMethodCard
-          testId="choice-desktop"
-          title={localLL.remote.title()}
-          subtitle={localLL.remote.subTitle()}
-          logo={<SvgDefguardNavLogo />}
-          linkText={localLL.remote.link()}
-          link={externalLink.defguardReleases}
-          selected={setupMethod === AddDeviceMethod.DESKTOP}
-          onSelect={() => {
-            if (setupMethod !== AddDeviceMethod.DESKTOP) {
-              setPageState({ method: AddDeviceMethod.DESKTOP });
-            }
-          }}
-        />
-        <DeviceSetupMethodCard
-          testId="choice-manual"
-          title={localLL.manual.title()}
-          subtitle={localLL.manual.subTitle()}
-          logo={<SvgWireguardLogo />}
-          linkText={localLL.manual.link()}
-          link={externalLink.wireguard.download}
-          selected={setupMethod === AddDeviceMethod.MANUAL}
-          onSelect={() => {
-            if (setupMethod !== AddDeviceMethod.MANUAL) {
-              setPageState({ method: AddDeviceMethod.MANUAL });
-            }
-          }}
-        />
-      </Card>
+      {!enterpriseSettings?.only_client_activation ? (
+        <>
+          <MessageBox
+            type={MessageBoxType.WARNING}
+            message={LL.addDevicePage.helpers.setupOpt()}
+            dismissId="add-device-page-method-opt-message"
+          />
+          <Card shaded id="setup-method-step">
+            <DeviceSetupMethodCard
+              testId="choice-desktop"
+              title={localLL.remote.title()}
+              subtitle={localLL.remote.subTitle()}
+              logo={<SvgDefguardNavLogo />}
+              linkText={localLL.remote.link()}
+              link={externalLink.defguardReleases}
+              selected={setupMethod === AddDeviceMethod.DESKTOP}
+              onSelect={() => {
+                if (setupMethod !== AddDeviceMethod.DESKTOP) {
+                  setPageState({ method: AddDeviceMethod.DESKTOP });
+                }
+              }}
+            />
+            <DeviceSetupMethodCard
+              testId="choice-manual"
+              title={localLL.manual.title()}
+              subtitle={localLL.manual.subTitle()}
+              logo={<SvgWireguardLogo />}
+              linkText={localLL.manual.link()}
+              link={externalLink.wireguard.download}
+              selected={setupMethod === AddDeviceMethod.MANUAL}
+              onSelect={() => {
+                if (setupMethod !== AddDeviceMethod.MANUAL) {
+                  setPageState({ method: AddDeviceMethod.MANUAL });
+                }
+              }}
+            />
+          </Card>
+        </>
+      ) : (
+        <div id="spinner-box">
+          <LoaderSpinner size={80} />
+        </div>
+      )}
     </>
   );
 };
diff --git a/web/src/pages/addDevice/steps/AddDeviceSetupMethodStep/style.scss b/web/src/pages/addDevice/steps/AddDeviceSetupMethodStep/style.scss
index 8d6df3eff..2911287b7 100644
--- a/web/src/pages/addDevice/steps/AddDeviceSetupMethodStep/style.scss
+++ b/web/src/pages/addDevice/steps/AddDeviceSetupMethodStep/style.scss
@@ -28,4 +28,12 @@
       }
     }
   }
+
+  #spinner-box {
+    display: flex;
+    flex-flow: column;
+    align-items: center;
+    justify-content: center;
+    min-height: 500px;
+  }
 }
diff --git a/web/src/pages/addDevice/steps/AddDeviceTokenStep/AddDeviceTokenStep.tsx b/web/src/pages/addDevice/steps/AddDeviceTokenStep/AddDeviceTokenStep.tsx
index 2da1830d6..1349ecb76 100644
--- a/web/src/pages/addDevice/steps/AddDeviceTokenStep/AddDeviceTokenStep.tsx
+++ b/web/src/pages/addDevice/steps/AddDeviceTokenStep/AddDeviceTokenStep.tsx
@@ -1,5 +1,6 @@
 import './style.scss';
 
+import parse from 'html-react-parser';
 import { isUndefined } from 'lodash-es';
 import { ReactNode, useEffect, useMemo } from 'react';
 import { useNavigate } from 'react-router';
@@ -10,6 +11,8 @@ import { ActionButton } from '../../../../shared/defguard-ui/components/Layout/A
 import { ActionButtonVariant } from '../../../../shared/defguard-ui/components/Layout/ActionButton/types';
 import { Card } from '../../../../shared/defguard-ui/components/Layout/Card/Card';
 import { ExpandableCard } from '../../../../shared/defguard-ui/components/Layout/ExpandableCard/ExpandableCard';
+import { MessageBox } from '../../../../shared/defguard-ui/components/Layout/MessageBox/MessageBox';
+import { MessageBoxType } from '../../../../shared/defguard-ui/components/Layout/MessageBox/types';
 import { useClipboard } from '../../../../shared/hooks/useClipboard';
 import { useAddDevicePageStore } from '../../hooks/useAddDevicePageStore';
 
@@ -78,14 +81,21 @@ export const AddDeviceTokenStep = () => {
   }, [resetPage, nextSubject, navigate, userData]);
 
   return (
-    <Card id="add-device-token-step" shaded>
-      <h2>{localLL.title()}</h2>
-      <ExpandableCard title={localLL.urlCardTitle()} actions={urlActions} expanded>
-        <p>{url}</p>
-      </ExpandableCard>
-      <ExpandableCard title={localLL.tokenCardTitle()} actions={tokenActions} expanded>
-        <p>{token}</p>
-      </ExpandableCard>
-    </Card>
+    <>
+      <MessageBox
+        type={MessageBoxType.WARNING}
+        message={parse(LL.addDevicePage.helpers.client())}
+        dismissId="add-device-page-method-opt-message"
+      />
+      <Card id="add-device-token-step" shaded>
+        <h2>{localLL.title()}</h2>
+        <ExpandableCard title={localLL.urlCardTitle()} actions={urlActions} expanded>
+          <p>{url}</p>
+        </ExpandableCard>
+        <ExpandableCard title={localLL.tokenCardTitle()} actions={tokenActions} expanded>
+          <p>{token}</p>
+        </ExpandableCard>
+      </Card>
+    </>
   );
 };
diff --git a/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx b/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx
index 9b2b39f9e..912fd3d80 100644
--- a/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx
+++ b/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx
@@ -62,6 +62,19 @@ export const EnterpriseForm = () => {
             {parse(LL.settingsPage.enterprise.fields.deviceManagement.helper())}
           </Helper>
         </div>
+        <div className="checkbox-row">
+          <LabeledCheckbox
+            disabled={isLoading}
+            label={LL.settingsPage.enterprise.fields.manualConfig.label()}
+            value={settings.only_client_activation}
+            onChange={() =>
+              mutate({ only_client_activation: !settings.only_client_activation })
+            }
+          />
+          <Helper>
+            {parse(LL.settingsPage.enterprise.fields.manualConfig.helper())}
+          </Helper>
+        </div>
       </Card>
     </section>
   );
diff --git a/web/src/pages/settings/components/GlobalSettings/components/LicenseSettings/LicenseSettings.tsx b/web/src/pages/settings/components/GlobalSettings/components/LicenseSettings/LicenseSettings.tsx
index aed537302..99c1d1151 100644
--- a/web/src/pages/settings/components/GlobalSettings/components/LicenseSettings/LicenseSettings.tsx
+++ b/web/src/pages/settings/components/GlobalSettings/components/LicenseSettings/LicenseSettings.tsx
@@ -132,7 +132,7 @@ export const LicenseSettings = () => {
               placeholder={LL.settingsPage.license.form.fields.key.placeholder()}
             />
           </form>
-          <ExpandableCard title={LL.settingsPage.license.licenseInfo.title()}>
+          <ExpandableCard title={LL.settingsPage.license.licenseInfo.title()} expanded>
             {licenseInfo ? (
               <div id="license-info">
                 <div>
diff --git a/web/src/pages/users/UserProfile/UserDevices/DeviceCard/DeviceCard.tsx b/web/src/pages/users/UserProfile/UserDevices/DeviceCard/DeviceCard.tsx
index 54e1407c5..e2ce410f9 100644
--- a/web/src/pages/users/UserProfile/UserDevices/DeviceCard/DeviceCard.tsx
+++ b/web/src/pages/users/UserProfile/UserDevices/DeviceCard/DeviceCard.tsx
@@ -20,6 +20,7 @@ import { EditButtonOption } from '../../../../../shared/defguard-ui/components/L
 import { EditButtonOptionStyleVariant } from '../../../../../shared/defguard-ui/components/Layout/EditButton/types';
 import { Label } from '../../../../../shared/defguard-ui/components/Layout/Label/Label';
 import { NoData } from '../../../../../shared/defguard-ui/components/Layout/NoData/NoData';
+import { useAppStore } from '../../../../../shared/hooks/store/useAppStore';
 import { useUserProfileStore } from '../../../../../shared/hooks/store/useUserProfileStore';
 import { Device, DeviceNetworkInfo } from '../../../../../shared/types';
 import { sortByDate } from '../../../../../shared/utils/sortByDate';
@@ -48,6 +49,7 @@ export const DeviceCard = ({ device, modifiable }: Props) => {
   const setDeleteDeviceModal = useDeleteDeviceModal((state) => state.setState);
   const setEditDeviceModal = useEditDeviceModal((state) => state.setState);
   const openDeviceConfigModal = useDeviceConfigModal((state) => state.open);
+  const enterpriseSettings = useAppStore((state) => state.enterprise_settings);
 
   const cn = useMemo(
     () =>
@@ -153,23 +155,25 @@ export const DeviceCard = ({ device, modifiable }: Props) => {
               });
             }}
           />
-          <EditButtonOption
-            styleVariant={EditButtonOptionStyleVariant.STANDARD}
-            text={LL.userPage.devices.card.edit.showConfigurations()}
-            disabled={!device.networks?.length}
-            onClick={() => {
-              openDeviceConfigModal({
-                deviceName: device.name,
-                publicKey: device.wireguard_pubkey,
-                deviceId: device.id,
-                userId: user.user.id,
-                networks: device.networks.map((n) => ({
-                  networkId: n.network_id,
-                  networkName: n.network_name,
-                })),
-              });
-            }}
-          />
+          {!enterpriseSettings?.only_client_activation && (
+            <EditButtonOption
+              styleVariant={EditButtonOptionStyleVariant.STANDARD}
+              text={LL.userPage.devices.card.edit.showConfigurations()}
+              disabled={!device.networks?.length}
+              onClick={() => {
+                openDeviceConfigModal({
+                  deviceName: device.name,
+                  publicKey: device.wireguard_pubkey,
+                  deviceId: device.id,
+                  userId: user.user.id,
+                  networks: device.networks.map((n) => ({
+                    networkId: n.network_id,
+                    networkName: n.network_name,
+                  })),
+                });
+              }}
+            />
+          )}
           <EditButtonOption
             styleVariant={EditButtonOptionStyleVariant.WARNING}
             text={LL.userPage.devices.card.edit.delete()}
diff --git a/web/src/shared/helpers/useEffectOnce.ts b/web/src/shared/helpers/useEffectOnce.ts
new file mode 100644
index 000000000..51244c413
--- /dev/null
+++ b/web/src/shared/helpers/useEffectOnce.ts
@@ -0,0 +1,19 @@
+import { useEffect, useRef } from 'react';
+
+/** 
+Under normal circumstances, useEffect should run only once when passed an empty dependency array.
+However, in dev mode with react strict mode enabled, everything is rendered twice for debugging purposes.
+This also causes useEffect to run twice, which is not always desirable.
+This custom hook ensures that the effect runs only once in dev mode as well.
+*/
+export default function useEffectOnce(fn: () => void) {
+  const isMounted = useRef(false);
+  useEffect(() => {
+    if (isMounted.current) {
+      return;
+    }
+
+    fn();
+    isMounted.current = true;
+  }, [fn]);
+}
diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts
index 62235d244..06336c7ae 100644
--- a/web/src/shared/types.ts
+++ b/web/src/shared/types.ts
@@ -871,6 +871,7 @@ export type SettingsLicense = {
 
 export type SettingsEnterprise = {
   admin_device_management: boolean;
+  only_client_activation: boolean;
 };
 
 export type LicenseInfo = {


Test patch:
diff --git a/tests/enterprise_settings.rs b/tests/enterprise_settings.rs
index 3239870e8..6f064d2a3 100644
--- a/tests/enterprise_settings.rs
+++ b/tests/enterprise_settings.rs
@@ -41,6 +41,7 @@ async fn test_only_enterprise_can_modify() {
     let settings = EnterpriseSettings {
         id: None,
         admin_device_management: true,
+        only_client_activation: false,
     };
 
     let response = client
@@ -84,6 +85,7 @@ async fn test_admin_devices_management_is_enforced() {
     let settings = EnterpriseSettings {
         id: None,
         admin_device_management: true,
+        only_client_activation: false,
     };
     let response = client
         .patch("/api/v1/settings_enterprise")
@@ -160,6 +162,7 @@ async fn test_regular_user_device_management() {
     let settings = EnterpriseSettings {
         id: None,
         admin_device_management: false,
+        only_client_activation: false,
     };
     let response = client
         .patch("/api/v1/settings_enterprise")