diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 4e10544f4..d63137357 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -546,7 +546,8 @@ "width": "Width", "wifi": "Wi-Fi", "windows": "Windows", - "yes": "Yes" + "yes": "Yes", + "username": "Username" }, "control-center": { "power-profile": { @@ -2161,6 +2162,10 @@ "fair": "Fair", "good": "Good", "poor": "Poor" + }, + "enterprise": { + "username": "Identity / Username", + "password": "User password" } } } diff --git a/Modules/Panels/Settings/Tabs/Connections/WifiSubTab.qml b/Modules/Panels/Settings/Tabs/Connections/WifiSubTab.qml index 6a37613a7..4c2b81832 100644 --- a/Modules/Panels/Settings/Tabs/Connections/WifiSubTab.qml +++ b/Modules/Panels/Settings/Tabs/Connections/WifiSubTab.qml @@ -21,6 +21,7 @@ Item { // State properties property string passwordSsid: "" + property string identity: "" property string expandedSsid: "" property string infoSsid: "" property int ipVersion: 4 @@ -91,10 +92,11 @@ Item { // Actions function requestPassword(ssid) { passwordSsid = ssid; + identity = ""; expandedSsid = ""; } - function submitPassword(ssid, password) { - NetworkService.connect(ssid, password); + function submitPassword(ssid, password, identity = "") { + NetworkService.connect(ssid, password, false, identity); passwordSsid = ""; } function cancelPassword() { @@ -355,6 +357,7 @@ Item { property string customSsid: "" property string customPassword: "" + property string customIdentity: "" property string customSecurityKey: "wpa2-psk" onOpened: { @@ -425,7 +428,7 @@ Item { onTextChanged: addNetworkPopup.customSsid = text onAccepted: { if (addNetworkPopup.customSsid.length > 0 && (addNetworkPopup.customSecurityKey === "open" || addNetworkPopup.customPassword.length > 0)) { - NetworkService.connectManual(addNetworkPopup.customSsid, addNetworkPopup.customPassword, addNetworkPopup.customSecurityKey); + NetworkService.connectManual(addNetworkPopup.customSsid, addNetworkPopup.customPassword, addNetworkPopup.customSecurityKey, addNetworkPopup.customIdentity); addNetworkPopup.close(); } } @@ -440,6 +443,17 @@ Item { } } + NTextInput { + id: customIdentityInput + Layout.fillWidth: true + inputIconName: "user" + visible: addNetworkPopup.customSecurityKey.indexOf("-eap") !== -1 + placeholderText: I18n.tr("wifi.enterprise.username") + label: I18n.tr("wifi.enterprise.username") + text: addNetworkPopup.customIdentity + onTextChanged: addNetworkPopup.customIdentity = text + } + NTextInput { id: customPasswordInput Layout.fillWidth: true @@ -452,7 +466,7 @@ Item { inputItem.echoMode: TextInput.Password onAccepted: { if (addNetworkPopup.customSsid.length > 0 && addNetworkPopup.customPassword.length > 0) { - NetworkService.connectManual(addNetworkPopup.customSsid, addNetworkPopup.customPassword, addNetworkPopup.customSecurityKey); + NetworkService.connectManual(addNetworkPopup.customSsid, addNetworkPopup.customPassword, addNetworkPopup.customSecurityKey, addNetworkPopup.customIdentity); addNetworkPopup.close(); } } @@ -480,9 +494,9 @@ Item { text: I18n.tr("common.connect") backgroundColor: Color.mPrimary textColor: Color.mOnPrimary - enabled: addNetworkPopup.customSsid.length > 0 && (addNetworkPopup.customSecurityKey === "open" || addNetworkPopup.customPassword.length > 0) + enabled: addNetworkPopup.customSsid.length > 0 && (addNetworkPopup.customSecurityKey === "open" || addNetworkPopup.customPassword.length > 0) && (addNetworkPopup.customSecurityKey.indexOf("-eap") === -1 || addNetworkPopup.customIdentity.length > 0) onClicked: { - NetworkService.connectManual(addNetworkPopup.customSsid, addNetworkPopup.customPassword, addNetworkPopup.customSecurityKey); + NetworkService.connectManual(addNetworkPopup.customSsid, addNetworkPopup.customPassword, addNetworkPopup.customSecurityKey, addNetworkPopup.customIdentity); addNetworkPopup.close(); } } @@ -498,6 +512,7 @@ Item { readonly property bool isBusy: NetworkService.connectingTo === modelData.ssid || NetworkService.disconnectingFrom === modelData.ssid || NetworkService.forgettingNetwork === modelData.ssid readonly property bool isExpanded: root.infoSsid === modelData.ssid + readonly property bool isEnterprise: NetworkService.isEnterprise(modelData.security) function getContentColor(defaultColor = Color.mOnSurface) { if (root.passwordSsid === modelData.ssid || NetworkService.connectingTo === modelData.ssid) { @@ -987,72 +1002,126 @@ Item { Rectangle { visible: root.passwordSsid === modelData.ssid && !networkItem.isBusy Layout.fillWidth: true - height: passwordRow.implicitHeight + Style.margin2S + height: passwordLayout.implicitHeight + Style.margin2S color: Color.mSurfaceVariant border.color: Color.mOutline border.width: Style.borderS radius: Style.iRadiusXS - RowLayout { - id: passwordRow + ColumnLayout { + id: passwordLayout anchors.fill: parent anchors.margins: Style.marginS - spacing: Style.marginM + spacing: Style.marginS - Rectangle { + // Inputs Container + ColumnLayout { Layout.fillWidth: true - Layout.fillHeight: true - radius: Style.iRadiusXS - color: Color.mSurface - border.color: pwdInput.activeFocus ? Color.mSecondary : Color.mOutline - border.width: Style.borderS + spacing: Style.marginS - TextInput { - id: pwdInput - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Style.marginS - font.family: Settings.data.ui.fontFixed - font.pointSize: Style.fontSizeS - color: Color.mOnSurface - echoMode: TextInput.Password - selectByMouse: true - focus: visible - passwordCharacter: "●" - onVisibleChanged: { - if (visible) { - forceActiveFocus(); - } - } - onAccepted: { - if (text && !NetworkService.connecting) { - root.submitPassword(modelData.ssid, text); - } - } + // Identity field (Enterprise only) + Rectangle { + visible: networkItem.isEnterprise + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize * 0.9 + radius: Style.iRadiusXS + color: Color.mSurface + border.color: identityInput.activeFocus ? Color.mSecondary : Color.mOutline + border.width: Style.borderS - NText { - visible: parent.text.length === 0 + TextInput { + id: identityInput + anchors.left: parent.left + anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - text: I18n.tr("wifi.panel.enter-password") - color: Color.mOnSurfaceVariant - pointSize: Style.fontSizeS + anchors.margins: Style.marginS + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeS + color: Color.mOnSurface + selectByMouse: true + onVisibleChanged: { + if (visible) { + forceActiveFocus(); + } + } + onAccepted: pwdInput.forceActiveFocus() + + NText { + visible: parent.text.length === 0 + anchors.verticalCenter: parent.verticalCenter + text: I18n.tr("wifi.enterprise.username") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + } + } + } + + // Password field + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize * 0.9 + radius: Style.iRadiusXS + color: Color.mSurface + border.color: pwdInput.activeFocus ? Color.mSecondary : Color.mOutline + border.width: Style.borderS + + TextInput { + id: pwdInput + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Style.marginS + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeS + color: Color.mOnSurface + echoMode: TextInput.Password + selectByMouse: true + passwordCharacter: "●" + onVisibleChanged: { + if (visible && !networkItem.isEnterprise) { + forceActiveFocus(); + } + } + onAccepted: { + if (text && !NetworkService.connecting) { + if (!networkItem.isEnterprise || identityInput.text.length > 0) { + root.submitPassword(modelData.ssid, text, identityInput.text); + } + } + } + + NText { + visible: parent.text.length === 0 + anchors.verticalCenter: parent.verticalCenter + text: networkItem.isEnterprise ? I18n.tr("wifi.enterprise.password") : I18n.tr("wifi.panel.enter-password") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + } } } } - NButton { - text: I18n.tr("common.connect") - fontSize: Style.fontSizeS - enabled: pwdInput.text.length > 0 && !NetworkService.connecting - outlined: true - onClicked: root.submitPassword(modelData.ssid, pwdInput.text) - } + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS - NIconButton { - icon: "close" - baseSize: Style.baseWidgetSize * 0.8 - onClicked: root.cancelPassword() + Item { + Layout.fillWidth: true + } + + NButton { + text: I18n.tr("common.connect") + fontSize: Style.fontSizeS + enabled: pwdInput.text.length > 0 && (!networkItem.isEnterprise || identityInput.text.length > 0) && !NetworkService.connecting + outlined: true + onClicked: root.submitPassword(modelData.ssid, pwdInput.text, identityInput.text) + } + + NIconButton { + icon: "close" + baseSize: Style.baseWidgetSize * 0.8 + onClicked: root.cancelPassword() + } } } } diff --git a/Services/Networking/NetworkService.qml b/Services/Networking/NetworkService.qml index 5daf1ba33..8fda06ccc 100644 --- a/Services/Networking/NetworkService.qml +++ b/Services/Networking/NetworkService.qml @@ -342,10 +342,18 @@ Singleton { refreshActiveEthernetDetails(); } - function connect(ssid, password = "", isHidden = false) { + function connect(ssid, password = "", isHidden = false, identity = "") { if (!ProgramCheckerService.nmcliAvailable || connecting) { return; } + + // For enterprise networks, use the robust manual connection logic (profile creation) + // as it handles 802.1X parameters much more reliably than 'device wifi connect'. + if (isEnterprise(networks[ssid] ? networks[ssid].security : "")) { + connectManual(ssid, password, "wpa-eap", identity); + return; + } + connecting = true; connectingTo = ssid; lastError = ""; @@ -366,7 +374,7 @@ Singleton { connectProcess.running = true; } - function connectManual(ssid, password, securityKey) { + function connectManual(ssid, password, securityKey, identity = "") { if (!ProgramCheckerService.nmcliAvailable || connecting) { return; } @@ -377,6 +385,7 @@ Singleton { manualConnectProcess.ssid = ssid; manualConnectProcess.password = password; manualConnectProcess.securityKey = securityKey; + manualConnectProcess.identity = identity; manualConnectProcess.running = true; } @@ -468,6 +477,14 @@ Singleton { return security && security !== "--" && security.trim() !== ""; } + function isEnterprise(security) { + if (!security) { + return false; + } + const s = security.toUpperCase(); + return s.indexOf("802.1X") !== -1 || s.indexOf("EAP") !== -1 || s.indexOf("ENTERPRISE") !== -1; + } + function getSignalStrengthLabel(signal) { switch (true) { case (signal >= 80): @@ -1434,6 +1451,7 @@ Singleton { property string ssid: "" property string password: "" property string securityKey: "" + property string identity: "" running: false command: { @@ -1441,6 +1459,7 @@ Singleton { SSID="$1" PWD="$2" SEC="$3" +IDENT="$4" # Remove existing profile to avoid conflict nmcli connection delete id "$SSID" 2>/dev/null || true @@ -1453,9 +1472,8 @@ elif [ "$SEC" = "sae" ]; then elif [ "$SEC" = "wep" ]; then nmcli connection add type wifi con-name "$SSID" ssid "$SSID" -- wifi-sec.key-mgmt none wifi-sec.wep-key0 "$PWD" 802-11-wireless.hidden yes elif [[ "$SEC" == *-eap ]]; then - # Enterprise not fully supported in Stage 1 - echo "Enterprise networks not supported yet" - exit 1 + # WPA Enterprise (PEAP-MSCHAPv2 default) + nmcli connection add type wifi con-name "$SSID" ssid "$SSID" -- wifi-sec.key-mgmt wpa-eap 802-1x.eap peap 802-1x.phase2-auth mschapv2 802-1x.identity "$IDENT" 802-1x.password "$PWD" 802-11-wireless.hidden yes else nmcli connection add type wifi con-name "$SSID" ssid "$SSID" -- 802-11-wireless.hidden yes fi @@ -1464,7 +1482,7 @@ fi nmcli connection up id "$SSID" `; - return ["sh", "-c", script, "--", ssid, password, securityKey]; + return ["sh", "-c", script, "--", ssid, password, securityKey, identity]; } stdout: StdioCollector {