From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id 006621FF0E1 for ; Mon, 10 Aug 2026 14:36:36 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 0089D216FB; Mon, 10 Aug 2026 14:36:16 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=azharul.com; s=t28hkp5; t=1786340390; x=1786945190; darn=lists.proxmox.com; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=+oVvN4WlYPYp/30e96HPB6FMeBEKv8JGqmKFu4ipy+c=; b=Q5Np8ZAuKedNHmkdktvjiNi7nU5Vuzdo31araksGwLkkGa1WJ20Lf3cBga6t5s4tSu mLPlE3UKFE+cbMtxhdQLln1hKl8Z+O2We2VfQ8NR6IqdoY/Ied+8fNqJHJT5mNt1lkqd OHgaw66Wl+mtTb4Oy/b1ycwiVKmV02OzchPwO17lEskVaeChUDww9O0rjQ7hoKlBiFjS mIZEvuiKAh6N+pRzs5E3HCmqtEDn3DZkQj2TvSwoP03QUPmj3tf1AiBRAuPfgJd5TODW eEhq0gi8ykVAS/Z48YW8Ne8YXeTPvusbm7VDwDzeT0AZvksxLFYrnqCG2ESrwUBip/tn JTBg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1786340390; x=1786945190; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=+oVvN4WlYPYp/30e96HPB6FMeBEKv8JGqmKFu4ipy+c=; b=jxHUd290dJDAqBXD8Fwj89+NausTSUSHylGRoINmEIxS0G8uFTN41gU1OhO/qr0t2t GcbVnDjtY0IkgyrzTyKbHrpwMCIPu0HXATLEz686/VviWlp1/ACEIxj9LUQ25PiHPdpb /gLtn1u3q6QyWanxqdYqrwqew2fquFF9UAMd5iilUYB7lIL1rYU7kGQvbKP3vRuN23Yx RCC3N6V9QY2fXkY+m9/dH6ffOYYp7iOBr6u1KoTq5KS2DW12k8TGvk1ag/wxWuhp3flk GvFnAI/usqslrX6YaZOuPpnBcAoCKBZsTAJlFVwKWZHM7eSGaqb5DNWcFUiKOEEWORkB yWtQ== X-Gm-Message-State: AOJu0YwQ7M8TPgJqhiD8f+wf81HBkxNJ8YcUMoRbsLcd2vcDmtTyNtws o5y7NWsYQe8W90QBsjtc3gJh1UW9CvxucaQRG6M2RLDcSKOsYavpvVwN32ag2fobjYz3l3tvmH2 gmjpJJQ== X-Gm-Gg: AR+sD11u+ukf2Ec7X4iVn0yJy5GPTGHYzrATf3L7YKqre8oR+q+PZcbJ8wCS1suTdI5 hcuCE0mSAgv5D34lxYLJdUisyoVj/P2qfDTgbhC+aA2e1ja+4CrRHa5JGuuDNtgO0ifZxKICWOy oQeXMJtbh1zlOhJMqvubPRZ1e77a1GrmUzr8BHmkJ/tylLm8Q3JgzlmTjc0cfuCVUI+4alxUFDM sF0jXwmS1fT8jn7ePNbNdO9pGff/qzOKJodWsZNpgUv2wP6JuC5ZWnLkWz39+KBN0tHTIVdYvKX 5YzYUHk5rED7bFYrwvyuC4LdCVQZWUnWhXttFWlodMS1Qw3u0aF+4DYA098IaN1fqwhkBlPlPad BPDR98R1aUiuCcSTkXzCRwhSy8Ne3LSIY5MNQBkpw47/8BjyFGmZPavfHUUCJjU+E7acy4PufGk Gql/OZUPrUhqQiV3ZwjiWfHkcjyl4UW9+MeiC1jHLccIAOyaFNy/4epq60DYCI5TQgf2mUvF2HQ 8v8ddJ5vMeYHmnCdSSRKgwFWWbi6JCQ7/ZEboBKNVWrcp+Vq++QvupmxAsIhNs0L0k2X2A6rUW4 GNYht6H2H8wYh7P47SiVETabmDOJHgCyZtzREC02mFex4jLpAN1pW/3YwS+TSqtsUzr4iRyexdJ HjK7Wm6z/9sFuUR6/lmUB4mifsNo= X-Received: by 2002:a05:620a:a0cc:10b0:914:bd3f:b6db with SMTP id af79cd13be357-93648fc7095mr3295748085a.10.1786340389498; Sun, 09 Aug 2026 22:39:49 -0700 (PDT) From: Azharul Haque To: pve-devel@lists.proxmox.com Subject: [PATCH 1/3] fix #4281: ui: add OpenID Connect login flow to login form Date: Mon, 10 Aug 2026 01:39:39 -0400 Message-ID: <20260810053941.17000-2-haque@azharul.com> X-Mailer: git-send-email 2.50.1 In-Reply-To: <20260810053941.17000-1-haque@azharul.com> References: <20260810053941.17000-1-haque@azharul.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.225 Adjusted score from AWL reputation of From: address DKIM_SIGNED 0.1 Message has a DKIM or DK signature, not necessarily valid DKIM_VALID -0.1 Message has at least one valid DKIM or DK signature DKIM_VALID_AU -0.1 Message has a valid DKIM or DK signature from author's domain DKIM_VALID_EF -0.1 Message has a valid DKIM or DK signature from envelope-from domain DMARC_PASS -0.1 DMARC pass policy KAM_LOTSOFHASH 0.25 Emails with lots of hash-like gibberish RCVD_IN_DNSWL_NONE -0.0001 Sender listed at https://www.dnswl.org/, no trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record X-MailFrom: haque@azharul.com X-Mailman-Rule-Hits: nonmember-moderation X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation Message-ID-Hash: 7FAUURCXVH25CCPXD32FPD3TUQTGTEVS X-Message-ID-Hash: 7FAUURCXVH25CCPXD32FPD3TUQTGTEVS X-Mailman-Approved-At: Mon, 10 Aug 2026 14:36:15 +0200 CC: haque@azharul.com X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Realms of type openid never got a distinct login flow: the form kept showing username/password fields, and submitting against an OpenID realm just sent a password login request the realm can't handle. When the selected realm is OpenID, the form now hides the username/password fields and, on Continue, drives the browser-based OpenID flow instead of the password one: - request the provider's authorization URL via openIdAuthUrl() - open it with flutter_web_auth_2, which uses the system browser / ASWebAuthenticationSession and captures the provider's redirect to a pveauth:// callback without needing an in-app webview - exchange the returned state/code for a ticket via openIdLogin() The remainder of the login sequence (TFA challenge, fetching cluster status, persisting the login) is identical between password and OpenID logins, so it's factored out of _onLoginButtonPressed() into a shared _finishLogin() used by both flows. Signed-off-by: Azharul Haque --- lib/proxmox_login_form.dart | 419 +++++++++++++++++++++++------------- pubspec.lock | 160 ++++++++++++-- pubspec.yaml | 1 + 3 files changed, 409 insertions(+), 171 deletions(-) diff --git a/lib/proxmox_login_form.dart b/lib/proxmox_login_form.dart index 5b9a64e..b73165c 100644 --- a/lib/proxmox_login_form.dart +++ b/lib/proxmox_login_form.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:collection/collection.dart'; import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart' as proxclient; @@ -12,6 +13,12 @@ import 'package:proxmox_login_manager/proxmox_tfa_form.dart'; import 'package:proxmox_login_manager/extension.dart'; import 'package:proxmox_login_manager/proxmox_password_store.dart'; +/// Custom URL scheme the identity provider redirects back to once an +/// OpenID Connect login completes. Must be registered as a valid redirect +/// URI with the realm's provider, as well as in the platform manifests +/// (AndroidManifest.xml / Info.plist). +const String openIdCallbackScheme = 'pveauth'; + class ProxmoxProgressModel { int inProgress = 0; String message = 'Loading...'; @@ -85,6 +92,8 @@ class _ProxmoxLoginFormState extends State { ); } + final isOpenId = widget.selectedDomain?.isOpenIdRealm ?? false; + return AutofillGroup( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -97,20 +106,6 @@ class _ProxmoxLoginFormState extends State { controller: widget.originController, enabled: false, ), - TextFormField( - decoration: const InputDecoration( - icon: Icon(Icons.person), - labelText: 'Username', - ), - controller: widget.usernameController, - validator: (value) { - if (value!.isEmpty) { - return 'Please enter username'; - } - return null; - }, - autofillHints: const [AutofillHints.username], - ), DropdownButtonFormField( decoration: const InputDecoration(icon: Icon(Icons.domain)), items: widget.accessDomains! @@ -127,54 +122,80 @@ class _ProxmoxLoginFormState extends State { widget.accessDomains!.map((e) => Text(e!.realm)).toList(), initialValue: widget.selectedDomain, ), - Stack( - children: [ - TextFormField( - decoration: const InputDecoration( - icon: Icon(Icons.lock), - labelText: 'Password', + if (isOpenId) + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Text( + "This realm signs you in through your browser. " + "Tap Continue to proceed.", + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + ) + else ...[ + TextFormField( + decoration: const InputDecoration( + icon: Icon(Icons.person), + labelText: 'Username', + ), + controller: widget.usernameController, + validator: (value) { + if (value!.isEmpty) { + return 'Please enter username'; + } + return null; + }, + autofillHints: const [AutofillHints.username], + ), + Stack( + children: [ + TextFormField( + decoration: const InputDecoration( + icon: Icon(Icons.lock), + labelText: 'Password', + ), + controller: widget.passwordController, + obscureText: _obscure, + autocorrect: false, + focusNode: passwordFocusNode, + validator: (value) { + if (value!.isEmpty) { + return 'Please enter password'; + } + return null; + }, + onFieldSubmitted: (value) => widget.onPasswordSubmitted!(), + autofillHints: const [AutofillHints.password], ), - controller: widget.passwordController, - obscureText: _obscure, - autocorrect: false, - focusNode: passwordFocusNode, - validator: (value) { - if (value!.isEmpty) { - return 'Please enter password'; + Align( + alignment: Alignment.bottomRight, + child: IconButton( + constraints: BoxConstraints.tight(const Size(58, 58)), + iconSize: 24, + tooltip: _obscure ? "Show password" : "Hide password", + icon: Icon( + _obscure ? Icons.visibility : Icons.visibility_off), + onPressed: () => setState(() { + _obscure = !_obscure; + }), + ), + ) + ], + ), + if (widget.canSavePassword ?? false) + CheckboxListTile( + title: const Text('Save password in biometric storage'), + value: _savePwCheckbox ?? widget.passwordSaved ?? false, + onChanged: (value) { + if (widget.onSavePasswordChanged != null) { + widget.onSavePasswordChanged!(value!); } - return null; + setState(() { + _savePwCheckbox = value!; + }); }, - onFieldSubmitted: (value) => widget.onPasswordSubmitted!(), - autofillHints: const [AutofillHints.password], - ), - Align( - alignment: Alignment.bottomRight, - child: IconButton( - constraints: BoxConstraints.tight(const Size(58, 58)), - iconSize: 24, - tooltip: _obscure ? "Show password" : "Hide password", - icon: - Icon(_obscure ? Icons.visibility : Icons.visibility_off), - onPressed: () => setState(() { - _obscure = !_obscure; - }), - ), ) - ], - ), - if (widget.canSavePassword ?? false) - CheckboxListTile( - title: const Text('Save password in biometric storage'), - value: _savePwCheckbox ?? widget.passwordSaved ?? false, - onChanged: (value) { - if (widget.onSavePasswordChanged != null) { - widget.onSavePasswordChanged!(value!); - } - setState(() { - _savePwCheckbox = value!; - }); - }, - ) + ], ], ), ); @@ -418,7 +439,13 @@ class _ProxmoxLoginPageState extends State { }); if (isValid) { if (snapshot.hasData) { - _onLoginButtonPressed(); + if (_selectedDomain + ?.isOpenIdRealm == + true) { + _onOpenIdLoginButtonPressed(); + } else { + _onLoginButtonPressed(); + } } else { setState(() { _accessDomains = @@ -478,98 +505,13 @@ class _ProxmoxLoginPageState extends State { var client = await proxclient.authenticate( '$username@$realm', password, origin, settings.sslValidation!); - if (client.credentials.tfa != null && - client.credentials.tfa!.kinds().isNotEmpty) { - if (!mounted) return; - ProxmoxApiClient? tfaclient = - await Navigator.of(context).push(MaterialPageRoute( - builder: (context) => ProxmoxTfaForm( - apiClient: client, - ), - )); - - if (tfaclient != null) { - client = tfaclient; - } else { - setState(() { - _progressModel.inProgress -= 1; - }); - return; - } - } - - final status = await client.getClusterStatus(); - final hostname = - status.singleWhereOrNull((element) => element.local ?? false)?.name; - var loginStorage = await ProxmoxLoginStorage.fromLocalStorage(); - - final savePW = enteredPassword != '' && - _savePasswordCB && - enteredPassword != savedPassword; - final deletePW = enteredPassword != '' && !savePW && !_savePasswordCB; - String? id; - - if (widget.isCreate!) { - final newLogin = ProxmoxLoginModel((b) => b - ..origin = origin - ..username = username - ..realm = realm - ..productType = ProxmoxProductType.pve - ..ticket = client.credentials.ticket - ..passwordSaved = savePW - ..hostname = hostname); - - loginStorage = loginStorage!.rebuild((b) => b..logins.add(newLogin)); - id = newLogin.identifier; - } else { - loginStorage = loginStorage!.rebuild((b) => b - ..logins.rebuildWhere( - (m) => m == widget.userModel, - (b) => b - ..ticket = client.credentials.ticket - ..passwordSaved = - savePW || (deletePW ? false : b.passwordSaved ?? false) - ..hostname = hostname)); - id = widget.userModel!.identifier; - } - - if (id != null) { - try { - if (savePW) { - await savePassword(id, enteredPassword); - } else if (deletePW) { - await deletePassword(id); - } - } catch (e) { - if (!mounted) return; - await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Password saving error'), - scrollable: true, - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Could not save or delete password.'), - ExpansionTile( - title: const Text('Details'), - children: [Text(e.toString())], - ) - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Continue')), - ], - )); - } - } - await loginStorage.saveToDisk(); - - if (mounted) { - Navigator.of(context).pop(client); - } + await _finishLogin( + client, + realm: realm!, + username: username, + enteredPassword: enteredPassword, + savedPassword: savedPassword, + ); } on proxclient.ProxmoxApiException catch (e) { print(e); if (!mounted) return; @@ -618,6 +560,181 @@ class _ProxmoxLoginPageState extends State { }); } + Future _onOpenIdLoginButtonPressed() async { + setState(() { + _progressModel + ..inProgress += 1 + ..message = 'Connecting to identity provider...'; + }); + + try { + final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage(); + final origin = normalizeUrl(_originController.text.trim()); + final realm = _selectedDomain!.realm; + final redirectUrl = + Uri(scheme: openIdCallbackScheme, host: 'openid-callback'); + + final authUrl = await proxclient.openIdAuthUrl( + realm, origin, redirectUrl, settings.sslValidation!); + + final result = await FlutterWebAuth2.authenticate( + url: authUrl, + callbackUrlScheme: openIdCallbackScheme, + ); + + final callbackUri = Uri.parse(result); + final state = callbackUri.queryParameters['state']; + final code = callbackUri.queryParameters['code']; + if (state == null || code == null) { + throw proxclient.ProxmoxApiException( + 'Identity provider did not return an authorization code', 400); + } + + final client = await proxclient.openIdLogin( + state, code, origin, redirectUrl, settings.sslValidation!); + + final fullUsername = client.credentials.username; + final username = fullUsername.contains('@') + ? fullUsername.substring(0, fullUsername.lastIndexOf('@')) + : fullUsername; + + await _finishLogin(client, realm: realm, username: username); + } on proxclient.ProxmoxApiException catch (e) { + print(e); + if (mounted) { + showDialog( + context: context, + builder: (context) => ProxmoxApiErrorDialog( + exception: e, + ), + ); + } + } catch (e, trace) { + print(e); + print(trace); + if (mounted) { + if (e.runtimeType == HandshakeException) { + showDialog( + context: context, + builder: (context) => const ProxmoxCertificateErrorDialog(), + ); + } else { + showDialog( + context: context, + builder: (context) => ConnectionErrorDialog(exception: e), + ); + } + } + } + setState(() { + _progressModel.inProgress -= 1; + }); + } + + /// Shared tail of both the password and OpenID login flows: handles a + /// pending TFA challenge, fetches cluster status, persists the login and + /// closes the login page. Returns early (without closing the page) if the + /// user cancels a TFA challenge. + Future _finishLogin( + ProxmoxApiClient client, { + required String realm, + required String username, + String enteredPassword = '', + String? savedPassword, + }) async { + if (client.credentials.tfa != null && + client.credentials.tfa!.kinds().isNotEmpty) { + if (!mounted) return; + ProxmoxApiClient? tfaclient = + await Navigator.of(context).push(MaterialPageRoute( + builder: (context) => ProxmoxTfaForm( + apiClient: client, + ), + )); + + if (tfaclient != null) { + client = tfaclient; + } else { + return; + } + } + + final status = await client.getClusterStatus(); + final hostname = + status.singleWhereOrNull((element) => element.local ?? false)?.name; + var loginStorage = await ProxmoxLoginStorage.fromLocalStorage(); + + final savePW = enteredPassword != '' && + _savePasswordCB && + enteredPassword != savedPassword; + final deletePW = enteredPassword != '' && !savePW && !_savePasswordCB; + String? id; + + final origin = normalizeUrl(_originController.text.trim()); + + if (widget.isCreate!) { + final newLogin = ProxmoxLoginModel((b) => b + ..origin = origin + ..username = username + ..realm = realm + ..productType = ProxmoxProductType.pve + ..ticket = client.credentials.ticket + ..passwordSaved = savePW + ..hostname = hostname); + + loginStorage = loginStorage!.rebuild((b) => b..logins.add(newLogin)); + id = newLogin.identifier; + } else { + loginStorage = loginStorage!.rebuild((b) => b + ..logins.rebuildWhere( + (m) => m == widget.userModel, + (b) => b + ..ticket = client.credentials.ticket + ..passwordSaved = + savePW || (deletePW ? false : b.passwordSaved ?? false) + ..hostname = hostname)); + id = widget.userModel!.identifier; + } + + if (id != null) { + try { + if (savePW) { + await savePassword(id, enteredPassword); + } else if (deletePW) { + await deletePassword(id); + } + } catch (e) { + if (!mounted) return; + await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Password saving error'), + scrollable: true, + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Could not save or delete password.'), + ExpansionTile( + title: const Text('Details'), + children: [Text(e.toString())], + ) + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Continue')), + ], + )); + } + } + await loginStorage.saveToDisk(); + + if (mounted) { + Navigator.of(context).pop(client); + } + } + Future?> _loadAccessDomains(Uri uri) async { final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage(); List? response; diff --git a/pubspec.lock b/pubspec.lock index ac4ba18..9250de9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -133,10 +133,10 @@ packages: dependency: transitive description: name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.2.1" code_builder: dependency: transitive description: @@ -193,6 +193,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.7" + desktop_webview_window: + dependency: transitive + description: + name: desktop_webview_window + sha256: b6fdae2cbf9571879b1761c12f27facaf82e22d0bdc74d049907c2a09a432957 + url: "https://pub.dev" + source: hosted + version: "0.3.0" fake_async: dependency: transitive description: @@ -243,6 +251,22 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_auth_2: + dependency: "direct main" + description: + name: flutter_web_auth_2 + sha256: "8f9303471dcd96670878c9b7c0c4e14c37595b2add67465f6a868f17a5872dfc" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + flutter_web_auth_2_platform_interface: + dependency: transitive + description: + name: flutter_web_auth_2_platform_interface + sha256: ba0fbba55bffb47242025f96852ad1ffba34bc451568f56ef36e613612baffab + url: "https://pub.dev" + source: hosted + version: "5.0.0" flutter_web_plugins: dependency: transitive description: flutter @@ -268,10 +292,10 @@ packages: dependency: transitive description: name: hooks - sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "2.0.2" http: dependency: transitive description: @@ -388,10 +412,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mime: dependency: transitive description: @@ -400,22 +424,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" - url: "https://pub.dev" - source: hosted - version: "0.17.6" objective_c: dependency: transitive description: name: objective_c - sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + sha256: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e url: "https://pub.dev" source: hosted - version: "9.3.0" + version: "9.5.0" package_config: dependency: transitive description: @@ -432,6 +448,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 + url: "https://pub.dev" + source: hosted + version: "2.1.6" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba" + url: "https://pub.dev" + source: hosted + version: "2.2.23" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" path_provider_linux: dependency: transitive description: @@ -503,6 +543,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" retry: dependency: transitive description: @@ -648,10 +696,10 @@ packages: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" typed_data: dependency: transitive description: @@ -660,6 +708,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 + url: "https://pub.dev" + source: hosted + version: "6.3.32" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" vector_math: dependency: transitive description: @@ -716,6 +828,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.15.0" + window_to_front: + dependency: transitive + description: + name: window_to_front + sha256: "14fad8984db4415e2eeb30b04bb77140b180e260d6cb66b26de126a8657a9241" + url: "https://pub.dev" + source: hosted + version: "0.0.4" xdg_directories: dependency: transitive description: @@ -733,5 +853,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.10.0 <4.0.0" - flutter: ">=3.35.6" + dart: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4652ced..be38e6d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: built_collection: ^5.0.0 proxmox_dart_api_client: path: ../proxmox_dart_api_client + flutter_web_auth_2: ^5.0.3 dev_dependencies: -- 2.50.1 (Apple Git-155)