--- /dev/null
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+build/
+
+# avoid commiting generated files
+*.g.dart
+
+# Android related
+**/android/**/gradle-wrapper.jar
+**/android/.gradle
+**/android/captures/
+**/android/gradlew
+**/android/gradlew.bat
+**/android/local.properties
+**/android/**/GeneratedPluginRegistrant.java
+
+# iOS/XCode related
+**/ios/**/*.mode1v3
+**/ios/**/*.mode2v3
+**/ios/**/*.moved-aside
+**/ios/**/*.pbxuser
+**/ios/**/*.perspectivev3
+**/ios/**/*sync/
+**/ios/**/.sconsign.dblite
+**/ios/**/.tags*
+**/ios/**/.vagrant/
+**/ios/**/DerivedData/
+**/ios/**/Icon?
+**/ios/**/Pods/
+**/ios/**/.symlinks/
+**/ios/**/profile
+**/ios/**/xcuserdata
+**/ios/.generated/
+**/ios/Flutter/App.framework
+**/ios/Flutter/Flutter.framework
+**/ios/Flutter/Flutter.podspec
+**/ios/Flutter/Generated.xcconfig
+**/ios/Flutter/app.flx
+**/ios/Flutter/app.zip
+**/ios/Flutter/flutter_assets/
+**/ios/Flutter/flutter_export_environment.sh
+**/ios/ServiceDefinitions.json
+**/ios/Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!**/ios/**/default.mode1v3
+!**/ios/**/default.mode2v3
+!**/ios/**/default.pbxuser
+!**/ios/**/default.perspectivev3
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
--- /dev/null
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: 9c3f0faa6da061f4f69ee1988a1416be985360f0
+ channel: master
+
+project_type: package
--- /dev/null
+TODO: Add your license here.
--- /dev/null
+# proxmox_login_manager
+
+A basic login management library which handles authentication in connection
+with the proxmox_dart_api_client library.
+
+You need to build the model classes before using it, to do this use:
+```Bash
+flutter packages pub run build_runner build
+```
+Basic usage:
+```dart
+ProxmoxLoginSelector(
+ onLogin: (ProxmoxApiClient client) => whatever you want to do with the client,
+),
+```
+This will give you an authenticated ProxmoxApiClient for usage in your project.
+
+The latest session will be saved and can be recovered.
+```dart
+loginStorage = await ProxmoxLoginStorage.fromLocalStorage();
+final apiClient = await loginStorage.recoverLatestSession();
+```
+This will either return an authenticated ProxmoxApiClient or will result in an
+Exception (ProxmoxApiException).
+
+To clear all session on logout you can use:
+```dart
+ProxmoxLoginStorage.fromLocalStorage()
+ .then((storage) => storage?.invalidateAllSessions());
+```
\ No newline at end of file
--- /dev/null
+import 'package:flutter/material.dart';
+import 'package:proxmox_login_manager/proxmox_general_settings_model.dart';
+
+class ProxmoxGeneralSettingsForm extends StatefulWidget {
+ @override
+ _ProxmoxGeneralSettingsFormState createState() =>
+ _ProxmoxGeneralSettingsFormState();
+}
+
+class _ProxmoxGeneralSettingsFormState
+ extends State<ProxmoxGeneralSettingsForm> {
+ Future<ProxmoxGeneralSettingsModel> _settings;
+ @override
+ void initState() {
+ super.initState();
+ _settings = ProxmoxGeneralSettingsModel.fromLocalStorage();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: Text('Settings'),
+ ),
+ body: FutureBuilder<ProxmoxGeneralSettingsModel>(
+ future: _settings,
+ builder: (context, snaptshot) {
+ if (snaptshot.hasData) {
+ final settings = snaptshot.data;
+ return SingleChildScrollView(
+ child: Column(
+ children: [
+ SwitchListTile(
+ title: Text('Validate SSL connections'),
+ subtitle: Text('e.g. validates certificates'),
+ value: settings.sslValidation,
+ onChanged: (value) async {
+ await settings
+ .rebuild((b) => b.sslValidation = value)
+ .toLocalStorage();
+ setState(() {
+ _settings =
+ ProxmoxGeneralSettingsModel.fromLocalStorage();
+ });
+ },
+ )
+ ],
+ ),
+ );
+ }
+
+ return Center(
+ child: CircularProgressIndicator(),
+ );
+ }),
+ );
+ }
+}
--- /dev/null
+import 'dart:convert';
+
+import 'package:built_value/built_value.dart';
+import 'package:built_value/serializer.dart';
+import 'package:proxmox_login_manager/serializers.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+part 'proxmox_general_settings_model.g.dart';
+
+abstract class ProxmoxGeneralSettingsModel
+ implements
+ Built<ProxmoxGeneralSettingsModel, ProxmoxGeneralSettingsModelBuilder> {
+ bool get sslValidation;
+
+ ProxmoxGeneralSettingsModel._();
+ factory ProxmoxGeneralSettingsModel(
+ [void Function(ProxmoxGeneralSettingsModelBuilder) updates]) =
+ _$ProxmoxGeneralSettingsModel;
+
+ factory ProxmoxGeneralSettingsModel.defaultValues() =>
+ ProxmoxGeneralSettingsModel((b) => b..sslValidation = true);
+
+ Object toJson() {
+ return serializers.serializeWith(
+ ProxmoxGeneralSettingsModel.serializer, this);
+ }
+
+ static ProxmoxGeneralSettingsModel fromJson(Object json) {
+ return serializers.deserializeWith(
+ ProxmoxGeneralSettingsModel.serializer, json);
+ }
+
+ static Serializer<ProxmoxGeneralSettingsModel> get serializer =>
+ _$proxmoxGeneralSettingsModelSerializer;
+
+ static Future<ProxmoxGeneralSettingsModel> fromLocalStorage() async {
+ final SharedPreferences prefs = await SharedPreferences.getInstance();
+ if (prefs.containsKey('ProxmoxGeneralSettings')) {
+ final decodedJson =
+ json.decode(prefs.getString('ProxmoxGeneralSettings'));
+ return fromJson(decodedJson);
+ }
+ return ProxmoxGeneralSettingsModel.defaultValues();
+ }
+
+ Future<void> toLocalStorage() async {
+ final SharedPreferences prefs = await SharedPreferences.getInstance();
+ prefs.setString('ProxmoxGeneralSettings', json.encode(toJson()));
+ }
+}
--- /dev/null
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart'
+ as proxclient;
+import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart';
+import 'package:proxmox_login_manager/proxmox_general_settings_form.dart';
+import 'package:proxmox_login_manager/proxmox_general_settings_model.dart';
+import 'package:proxmox_login_manager/proxmox_login_model.dart';
+import 'package:proxmox_login_manager/proxmox_tfa_form.dart';
+
+class ProxmoxProgressModel {
+ bool inProgress;
+ String message;
+ ProxmoxProgressModel({
+ this.inProgress = false,
+ this.message = 'Loading...',
+ });
+}
+
+class ProxmoxLoginForm extends StatefulWidget {
+ final TextEditingController originController;
+ final FormFieldValidator<String> originValidator;
+ final TextEditingController usernameController;
+ final TextEditingController passwordController;
+ final List<PveAccessDomainModel> accessDomains;
+ final PveAccessDomainModel selectedDomain;
+ final ValueChanged<PveAccessDomainModel> onDomainChanged;
+
+ const ProxmoxLoginForm({
+ Key key,
+ @required this.originController,
+ @required this.usernameController,
+ @required this.passwordController,
+ @required this.accessDomains,
+ @required this.originValidator,
+ this.selectedDomain,
+ @required this.onDomainChanged,
+ }) : super(key: key);
+
+ @override
+ _ProxmoxLoginFormState createState() => _ProxmoxLoginFormState();
+}
+
+class _ProxmoxLoginFormState extends State<ProxmoxLoginForm> {
+ bool _obscure = true;
+ FocusNode passwordFocusNode;
+
+ @override
+ void initState() {
+ super.initState();
+
+ if (widget.usernameController.text.isNotEmpty) {
+ passwordFocusNode = FocusNode();
+ passwordFocusNode.requestFocus();
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (widget.accessDomains == null) {
+ return TextFormField(
+ decoration: InputDecoration(
+ icon: Icon(Icons.vpn_lock),
+ labelText: 'Origin',
+ hintText: 'e.g. 192.168.1.2',
+ helperText: 'Protocol (https) and default port (8006) implied'),
+ controller: widget.originController,
+ validator: widget.originValidator,
+ );
+ }
+
+ return Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ TextFormField(
+ decoration: InputDecoration(
+ icon: Icon(Icons.vpn_lock),
+ labelText: 'Origin',
+ ),
+ controller: widget.originController,
+ enabled: false,
+ ),
+ TextFormField(
+ decoration: InputDecoration(
+ icon: Icon(Icons.person),
+ labelText: 'Username',
+ ),
+ controller: widget.usernameController,
+ validator: (value) {
+ if (value.isEmpty) {
+ return 'Please enter username';
+ }
+ return null;
+ },
+ ),
+ DropdownButtonFormField(
+ decoration: InputDecoration(icon: Icon(Icons.domain)),
+ items: widget.accessDomains
+ .map((e) => DropdownMenuItem(
+ child: ListTile(
+ title: Text(e.realm),
+ subtitle: Text(e.comment ?? ''),
+ ),
+ value: e,
+ ))
+ .toList(),
+ onChanged: widget.onDomainChanged,
+ selectedItemBuilder: (context) =>
+ widget.accessDomains.map((e) => Text(e.realm)).toList(),
+ value: widget.selectedDomain,
+ ),
+ Stack(
+ children: [
+ TextFormField(
+ decoration: 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;
+ },
+ ),
+ Align(
+ alignment: Alignment.bottomRight,
+ child: IconButton(
+ constraints: BoxConstraints.tight(Size(58, 58)),
+ iconSize: 24,
+ icon: Icon(_obscure ? Icons.visibility : Icons.visibility_off),
+ onPressed: () => setState(() {
+ _obscure = !_obscure;
+ }),
+ ),
+ )
+ ],
+ ),
+ ],
+ );
+ }
+
+ @override
+ void dispose() {
+ passwordFocusNode?.dispose();
+ super.dispose();
+ }
+}
+
+class ProxmoxLoginPage extends StatefulWidget {
+ final ProxmoxLoginModel userModel;
+ final bool isCreate;
+
+ const ProxmoxLoginPage({
+ Key key,
+ this.userModel,
+ this.isCreate,
+ }) : super(key: key);
+ @override
+ _ProxmoxLoginPageState createState() => _ProxmoxLoginPageState();
+}
+
+class _ProxmoxLoginPageState extends State<ProxmoxLoginPage> {
+ final _originController = TextEditingController();
+ final _usernameController = TextEditingController();
+ final _passwordController = TextEditingController();
+ Future<List<PveAccessDomainModel>> _accessDomains;
+ PveAccessDomainModel _selectedDomain;
+ final _formKey = GlobalKey<FormState>();
+ ProxmoxProgressModel _progressModel;
+ bool _submittButtonEnabled = true;
+ @override
+ void initState() {
+ super.initState();
+ final userModel = widget.userModel;
+ _progressModel = ProxmoxProgressModel();
+ if (!widget.isCreate && userModel != null) {
+ _progressModel
+ ..inProgress = true
+ ..message = 'Connection test...';
+ _originController.text =
+ '${userModel.origin?.host}:${userModel.origin?.port}';
+ _accessDomains = _getAccessDomains();
+ _usernameController.text = userModel.username;
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Theme(
+ data: ThemeData.dark().copyWith(accentColor: Color(0xFFE47225)),
+ child: Material(
+ color: Theme.of(context).primaryColor,
+ child: Stack(
+ children: [
+ SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints.tightFor(
+ height: MediaQuery.of(context).size.height),
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: FutureBuilder<List<PveAccessDomainModel>>(
+ future: _accessDomains,
+ builder: (context, snapshot) {
+ return Form(
+ key: _formKey,
+ onChanged: () {
+ setState(() {
+ _submittButtonEnabled =
+ _formKey.currentState.validate();
+ });
+ },
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Expanded(
+ child: Container(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ 'PROXMOX',
+ style: TextStyle(
+ fontFamily: 'Proxmox',
+ fontSize: 36,
+ ),
+ ),
+ Text(
+ 'Open Source',
+ style: TextStyle(
+ fontSize: 18,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ProxmoxLoginForm(
+ originController: _originController,
+ originValidator: (value) {
+ if (value.isEmpty) {
+ return 'Please enter origin';
+ }
+ if (value.startsWith('https://') ||
+ value.startsWith('http://')) {
+ return 'Do not prefix with scheme';
+ }
+ try {
+ Uri.https(value, '');
+ return null;
+ } on FormatException catch (_) {
+ return 'Invalid URI';
+ }
+ },
+ usernameController: _usernameController,
+ passwordController: _passwordController,
+ accessDomains: snapshot.data,
+ selectedDomain: _selectedDomain,
+ onDomainChanged: (value) {
+ setState(() {
+ _selectedDomain = value;
+ });
+ },
+ ),
+ if (snapshot.hasData)
+ Expanded(
+ child: Align(
+ alignment: Alignment.bottomCenter,
+ child: Container(
+ width: MediaQuery.of(context).size.width,
+ child: FlatButton(
+ onPressed: _submittButtonEnabled
+ ? () {
+ final isValid = _formKey
+ .currentState
+ .validate();
+ setState(() {
+ _submittButtonEnabled =
+ isValid;
+ });
+ if (isValid) {
+ _onLoginButtonPressed();
+ }
+ }
+ : null,
+ color: Color(0xFFE47225),
+ disabledColor: Colors.grey,
+ child: Text('Continue'),
+ ),
+ ),
+ ),
+ ),
+ if (!snapshot.hasData)
+ Expanded(
+ child: Align(
+ alignment: Alignment.bottomCenter,
+ child: Container(
+ width: MediaQuery.of(context).size.width,
+ child: FlatButton(
+ onPressed: _submittButtonEnabled
+ ? () {
+ final isValid = _formKey
+ .currentState
+ .validate();
+ setState(() {
+ _submittButtonEnabled =
+ isValid;
+ });
+ if (isValid) {
+ setState(() {
+ _accessDomains =
+ _getAccessDomains();
+ });
+ }
+ }
+ : null,
+ color: Color(0xFFE47225),
+ child: Text('Continue'),
+ disabledColor: Colors.grey,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }),
+ ),
+ ),
+ ),
+ if (_progressModel.inProgress)
+ ProxmoxProgressOverlay(message: _progressModel.message),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Future<void> _onLoginButtonPressed() async {
+ setState(() {
+ _progressModel
+ ..inProgress = true
+ ..message = 'Authenticating...';
+ });
+
+ try {
+ final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
+ final origin = Uri.https(_originController.text, '');
+ var client = await proxclient.authenticate(
+ '${_usernameController.text}@${_selectedDomain.realm}',
+ _passwordController.text,
+ origin,
+ settings.sslValidation);
+
+ if (client.credentials.tfa) {
+ client = await Navigator.of(context).push(MaterialPageRoute(
+ builder: (context) => ProxmoxTfaForm(
+ apiClient: client,
+ ),
+ ));
+ }
+
+ var loginStorage = await ProxmoxLoginStorage.fromLocalStorage();
+
+ if (widget.isCreate) {
+ final newLogin = ProxmoxLoginModel((b) => b
+ ..origin = origin
+ ..username = _usernameController.text
+ ..realm = _selectedDomain.realm
+ ..productType = ProxmoxProductType.pve
+ ..ticket = client.credentials.ticket);
+ loginStorage = loginStorage.rebuild((b) => b..logins.add(newLogin));
+ } else {
+ loginStorage = loginStorage.rebuild((b) => b
+ ..logins.remove(widget.userModel)
+ ..logins.add(widget.userModel
+ .rebuild((b) => b..ticket = client.credentials.ticket)));
+ }
+ await loginStorage.saveToDisk();
+
+ Navigator.of(context).pop(client);
+ } on proxclient.ProxmoxApiException catch (e) {
+ print(e);
+ showDialog(
+ context: context,
+ builder: (context) => ProxmoxApiErrorDialog(
+ exception: e,
+ ),
+ );
+ } catch (e, trace) {
+ print(e);
+ print(trace);
+ if (e.runtimeType == HandshakeException) {
+ showDialog(
+ context: context,
+ builder: (context) => ProxmoxCertificateErrorDialog(),
+ );
+ }
+ }
+ setState(() {
+ _progressModel.inProgress = false;
+ });
+ }
+
+ Future<List<PveAccessDomainModel>> _getAccessDomains() async {
+ setState(() {
+ _progressModel
+ ..inProgress = true
+ ..message = 'Connection test...';
+ });
+ var apiBaseUrl = Uri.https(_originController.text, '');
+
+ if (!apiBaseUrl.hasPort) {
+ _originController.text += ':8006';
+ apiBaseUrl = apiBaseUrl.replace(port: 8006);
+ }
+
+ final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
+ List<PveAccessDomainModel> response;
+ try {
+ response =
+ await proxclient.accessDomains(apiBaseUrl, settings.sslValidation);
+ } on proxclient.ProxmoxApiException catch (e) {
+ print(e);
+ showDialog(
+ context: context,
+ builder: (context) => ProxmoxApiErrorDialog(
+ exception: e,
+ ),
+ );
+ } catch (e, trace) {
+ print(e);
+ print(trace);
+ if (e.runtimeType == HandshakeException) {
+ showDialog(
+ context: context,
+ builder: (context) => ProxmoxCertificateErrorDialog(),
+ );
+ } else {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: Text('Connection error'),
+ content: Text('Could not establish connection.'),
+ actions: [
+ FlatButton(
+ onPressed: () => Navigator.of(context).pop(),
+ child: Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+ }
+ final selection = response?.singleWhere(
+ (e) => e.realm == widget.userModel?.realm,
+ orElse: () => response?.first,
+ );
+ setState(() {
+ _progressModel.inProgress = false;
+ _selectedDomain = selection;
+ });
+ return response;
+ }
+
+ @override
+ void dispose() {
+ _originController.dispose();
+ _usernameController.dispose();
+ _passwordController.dispose();
+ super.dispose();
+ }
+}
+
+class ProxmoxProgressOverlay extends StatelessWidget {
+ const ProxmoxProgressOverlay({
+ Key key,
+ @required this.message,
+ }) : super(key: key);
+
+ final String message;
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ decoration: new BoxDecoration(color: Colors.black.withOpacity(0.5)),
+ child: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ message,
+ style: TextStyle(
+ color: Theme.of(context).accentColor,
+ fontSize: 20,
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.only(top: 20.0),
+ child: CircularProgressIndicator(),
+ )
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class ProxmoxApiErrorDialog extends StatelessWidget {
+ final proxclient.ProxmoxApiException exception;
+
+ const ProxmoxApiErrorDialog({
+ Key key,
+ @required this.exception,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return AlertDialog(
+ title: Text('API Error'),
+ content: SingleChildScrollView(
+ child: Text(exception.message),
+ ),
+ actions: [
+ FlatButton(
+ onPressed: () => Navigator.of(context).pop(),
+ child: Text('Close'),
+ ),
+ ],
+ );
+ }
+}
+
+class ProxmoxCertificateErrorDialog extends StatelessWidget {
+ const ProxmoxCertificateErrorDialog({
+ Key key,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return AlertDialog(
+ title: Text('Certificate error'),
+ content: SingleChildScrollView(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Your connection is not private.'),
+ Text(
+ 'Note: Consider to disable SSL validation,'
+ ' if you use a self signed, not commonly trusted, certificate.',
+ style: Theme.of(context).textTheme.caption,
+ ),
+ ],
+ ),
+ ),
+ actions: [
+ FlatButton(
+ onPressed: () => Navigator.of(context).pop(),
+ child: Text('Close'),
+ ),
+ FlatButton(
+ onPressed: () => Navigator.of(context).pushReplacement(
+ MaterialPageRoute(
+ builder: (context) => ProxmoxGeneralSettingsForm())),
+ child: Text('Settings'),
+ )
+ ],
+ );
+ }
+}
--- /dev/null
+library proxmox_login_manager;
+
+export 'proxmox_login_selector.dart';
+export 'proxmox_login_model.dart';
--- /dev/null
+import 'package:built_collection/built_collection.dart';
+import 'package:built_value/built_value.dart';
+import 'package:built_value/serializer.dart';
+import 'package:proxmox_login_manager/proxmox_general_settings_model.dart';
+import 'package:proxmox_login_manager/serializers.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+import 'dart:convert';
+import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart'
+ as proxclient;
+part 'proxmox_login_model.g.dart';
+
+abstract class ProxmoxLoginStorage
+ implements Built<ProxmoxLoginStorage, ProxmoxLoginStorageBuilder> {
+ BuiltList<ProxmoxLoginModel> get logins;
+
+ ProxmoxLoginStorage._();
+ factory ProxmoxLoginStorage(
+ [void Function(ProxmoxLoginStorageBuilder) updates]) =
+ _$ProxmoxLoginStorage;
+
+ Object toJson() {
+ return serializers.serializeWith(ProxmoxLoginStorage.serializer, this);
+ }
+
+ static ProxmoxLoginStorage fromJson(Object json) {
+ return serializers.deserializeWith(ProxmoxLoginStorage.serializer, json);
+ }
+
+ static Serializer<ProxmoxLoginStorage> get serializer =>
+ _$proxmoxLoginStorageSerializer;
+
+ static Future<ProxmoxLoginStorage> fromLocalStorage() async {
+ final SharedPreferences prefs = await SharedPreferences.getInstance();
+ if (prefs.containsKey('ProxmoxLoginList')) {
+ final decodedJson = json.decode(prefs.getString('ProxmoxLoginList'));
+ return fromJson(decodedJson);
+ }
+ return ProxmoxLoginStorage();
+ }
+
+ Future<void> saveToDisk() async {
+ final SharedPreferences prefs = await SharedPreferences.getInstance();
+ prefs.setString('ProxmoxLoginList', json.encode(toJson()));
+ }
+
+ Future<proxclient.ProxmoxApiClient> recoverLatestSession() async {
+ final latestSession = logins.singleWhere((e) => e.ticket.isNotEmpty);
+ final settings = await ProxmoxGeneralSettingsModel.fromLocalStorage();
+ final apiClient = await proxclient.authenticate(latestSession.fullUsername,
+ latestSession.ticket, latestSession.origin, settings.sslValidation);
+ return apiClient;
+ }
+
+ Future<void> invalidateAllSessions() async {
+ final invalidatedList =
+ logins.map((e) => e.rebuild((login) => login..ticket = ''));
+ await rebuild((e) => e.logins.replace(invalidatedList)).saveToDisk();
+ }
+}
+
+abstract class ProxmoxLoginModel
+ implements Built<ProxmoxLoginModel, ProxmoxLoginModelBuilder> {
+ Uri get origin;
+
+ String get username;
+
+ String get realm;
+
+ ProxmoxProductType get productType;
+
+ String get ticket;
+
+ /// The username with the corresponding realm e.g. root@pam
+ String get fullUsername => '$username@$realm';
+
+ ProxmoxLoginModel._();
+
+ factory ProxmoxLoginModel([void Function(ProxmoxLoginModelBuilder) updates]) =
+ _$ProxmoxLoginModel;
+
+ Map<String, dynamic> toJson() {
+ return serializers.serializeWith(ProxmoxLoginModel.serializer, this);
+ }
+
+ static ProxmoxLoginModel fromJson(Map<String, dynamic> json) {
+ return serializers.deserializeWith(ProxmoxLoginModel.serializer, json);
+ }
+
+ static Serializer<ProxmoxLoginModel> get serializer =>
+ _$proxmoxLoginModelSerializer;
+}
+
+class ProxmoxProductType extends EnumClass {
+ static const ProxmoxProductType pve = _$pve;
+ static const ProxmoxProductType pmg = _$pmg;
+ static const ProxmoxProductType pbs = _$pbs;
+
+ const ProxmoxProductType._(String name) : super(name);
+
+ static BuiltSet<ProxmoxProductType> get values => _$ProxmoxProductTypeValues;
+ static ProxmoxProductType valueOf(String name) =>
+ _$ProxmoxProductTypeValueOf(name);
+
+ static Serializer<ProxmoxProductType> get serializer =>
+ _$proxmoxProductTypeSerializer;
+}
--- /dev/null
+import 'package:flutter/material.dart';
+import 'package:proxmox_login_manager/proxmox_general_settings_form.dart';
+import 'package:proxmox_login_manager/proxmox_login_form.dart';
+import 'package:proxmox_login_manager/proxmox_login_model.dart';
+import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart'
+ as proxclient;
+
+typedef OnLoginCallback = Function(proxclient.ProxmoxApiClient client);
+
+class ProxmoxLoginSelector extends StatefulWidget {
+ final OnLoginCallback onLogin;
+
+ const ProxmoxLoginSelector({Key key, this.onLogin}) : super(key: key);
+
+ @override
+ _ProxmoxLoginSelectorState createState() => _ProxmoxLoginSelectorState();
+}
+
+class _ProxmoxLoginSelectorState extends State<ProxmoxLoginSelector> {
+ Future<ProxmoxLoginStorage> loginStorage;
+ @override
+ void initState() {
+ super.initState();
+ loginStorage = ProxmoxLoginStorage.fromLocalStorage();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return SafeArea(
+ child: Scaffold(
+ appBar: AppBar(
+ title: Text(
+ 'PROXMOX',
+ style: TextStyle(
+ fontFamily: 'Proxmox',
+ fontSize: 28,
+ ),
+ ),
+ actions: [
+ IconButton(
+ icon: Icon(Icons.settings),
+ onPressed: () {
+ Navigator.of(context).push(MaterialPageRoute(
+ builder: (context) => ProxmoxGeneralSettingsForm(),
+ ));
+ })
+ ],
+ ),
+ body: FutureBuilder<ProxmoxLoginStorage>(
+ future: loginStorage,
+ builder: (context, snapshot) {
+ return ListView(
+ children: snapshot.data?.logins
+ ?.map((l) => ListTile(
+ title: Text(l.origin.host),
+ subtitle: Text(l.fullUsername),
+ trailing: Icon(Icons.navigate_next),
+ onTap: () => _login(user: l),
+ onLongPress: () {
+ snapshot.data
+ .rebuild((b) => b.logins.remove(l))
+ .saveToDisk();
+ refreshFromStorage();
+ },
+ ))
+ ?.toList() ??
+ [],
+ );
+ }),
+ floatingActionButton: FloatingActionButton.extended(
+ onPressed: () => _login(isCreate: true),
+ label: Text('Add'),
+ icon: Icon(Icons.account_circle),
+ ),
+ ),
+ );
+ }
+
+ Future<void> _login({ProxmoxLoginModel user, bool isCreate = false}) async {
+ final client = await Navigator.of(context).push(MaterialPageRoute(
+ builder: (context) => ProxmoxLoginPage(
+ userModel: user,
+ isCreate: isCreate,
+ )));
+ refreshFromStorage();
+ if (client != null) {
+ widget.onLogin(client);
+ }
+ }
+
+ void refreshFromStorage() {
+ setState(() {
+ loginStorage = ProxmoxLoginStorage.fromLocalStorage();
+ });
+ }
+}
--- /dev/null
+import 'package:flutter/material.dart';
+import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart';
+import 'package:proxmox_login_manager/proxmox_login_form.dart';
+
+class ProxmoxTfaForm extends StatefulWidget {
+ final ProxmoxApiClient apiClient;
+
+ const ProxmoxTfaForm({Key key, this.apiClient}) : super(key: key);
+
+ @override
+ _ProxmoxTfaFormState createState() => _ProxmoxTfaFormState();
+}
+
+class _ProxmoxTfaFormState extends State<ProxmoxTfaForm> {
+ final TextEditingController _codeController = TextEditingController();
+ bool _isLoading = false;
+ @override
+ Widget build(BuildContext context) {
+ return Theme(
+ data: ThemeData.dark().copyWith(accentColor: Color(0xFFE47225)),
+ child: Material(
+ color: Theme.of(context).primaryColor,
+ child: Stack(
+ alignment: Alignment.center,
+ children: [
+ SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints.tightFor(
+ height: MediaQuery.of(context).size.height),
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: <Widget>[
+ Padding(
+ padding: const EdgeInsets.fromLTRB(0, 100.0, 0, 30.0),
+ child: Icon(
+ Icons.lock,
+ size: 48,
+ ),
+ ),
+ Text(
+ 'Verify',
+ style: TextStyle(
+ fontSize: 36,
+ color: Colors.white,
+ fontWeight: FontWeight.bold),
+ ),
+ Text(
+ 'Check your second factor provider',
+ style: TextStyle(
+ color: Colors.white38, fontWeight: FontWeight.bold),
+ ),
+ Padding(
+ padding: const EdgeInsets.fromLTRB(0, 50.0, 0, 8.0),
+ child: Container(
+ width: 150,
+ child: TextField(
+ controller: _codeController,
+ textAlign: TextAlign.center,
+ decoration: InputDecoration(labelText: 'Code'),
+ autofocus: true,
+ onSubmitted: (value) => _submitTfaCode()),
+ ),
+ ),
+ Expanded(
+ child: Align(
+ alignment: Alignment.bottomCenter,
+ child: Container(
+ width: MediaQuery.of(context).size.width,
+ child: FlatButton(
+ onPressed: () => _submitTfaCode(),
+ color: Color(0xFFE47225),
+ child: Text('Continue'),
+ disabledColor: Colors.grey,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ if (_isLoading)
+ ProxmoxProgressOverlay(
+ message: 'Verify One-Time password...',
+ )
+ ],
+ ),
+ ),
+ );
+ }
+
+ Future<void> _submitTfaCode() async {
+ setState(() {
+ _isLoading = true;
+ });
+ try {
+ final client =
+ await widget.apiClient.finishTfaChallenge(_codeController.text);
+ Navigator.of(context).pop(client);
+ } on ProxmoxApiException catch (e) {
+ showDialog(
+ context: context,
+ builder: (context) => ProxmoxApiErrorDialog(
+ exception: e,
+ ),
+ );
+ } catch (e, trace) {
+ print(e);
+ print(trace);
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: Text('Connection error'),
+ content: Text('Could not establish connection.'),
+ actions: [
+ FlatButton(
+ onPressed: () => Navigator.of(context).pop(),
+ child: Text('Close'),
+ ),
+ ],
+ ),
+ );
+ }
+ setState(() {
+ _isLoading = false;
+ });
+ }
+}
--- /dev/null
+import 'package:built_value/serializer.dart';
+import 'package:built_collection/built_collection.dart';
+import 'package:proxmox_login_manager/proxmox_general_settings_model.dart';
+import 'package:proxmox_login_manager/proxmox_login_model.dart';
+part 'serializers.g.dart';
+
+@SerializersFor([ProxmoxLoginStorage, ProxmoxGeneralSettingsModel])
+final Serializers serializers = (_$serializers.toBuilder()).build();
--- /dev/null
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ _fe_analyzer_shared:
+ dependency: transitive
+ description:
+ name: _fe_analyzer_shared
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "5.0.0"
+ analyzer:
+ dependency: transitive
+ description:
+ name: analyzer
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.39.12"
+ args:
+ dependency: transitive
+ description:
+ name: args
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.6.0"
+ async:
+ dependency: transitive
+ description:
+ name: async
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.4.1"
+ boolean_selector:
+ dependency: transitive
+ description:
+ name: boolean_selector
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.0"
+ build:
+ dependency: transitive
+ description:
+ name: build
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.3.0"
+ build_config:
+ dependency: transitive
+ description:
+ name: build_config
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.4.2"
+ build_daemon:
+ dependency: transitive
+ description:
+ name: build_daemon
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.4"
+ build_resolvers:
+ dependency: transitive
+ description:
+ name: build_resolvers
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.3.10"
+ build_runner:
+ dependency: "direct dev"
+ description:
+ name: build_runner
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.10.0"
+ build_runner_core:
+ dependency: transitive
+ description:
+ name: build_runner_core
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "5.2.0"
+ built_collection:
+ dependency: "direct main"
+ description:
+ name: built_collection
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "4.3.2"
+ built_value:
+ dependency: "direct main"
+ description:
+ name: built_value
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "7.1.0"
+ built_value_generator:
+ dependency: "direct dev"
+ description:
+ name: built_value_generator
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "7.1.0"
+ characters:
+ dependency: transitive
+ description:
+ name: characters
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.0"
+ charcode:
+ dependency: transitive
+ description:
+ name: charcode
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.3"
+ checked_yaml:
+ dependency: transitive
+ description:
+ name: checked_yaml
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.2"
+ clock:
+ dependency: transitive
+ description:
+ name: clock
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.1"
+ code_builder:
+ dependency: transitive
+ description:
+ name: code_builder
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.4.0"
+ collection:
+ dependency: transitive
+ description:
+ name: collection
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.14.12"
+ convert:
+ dependency: transitive
+ description:
+ name: convert
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.1"
+ crypto:
+ dependency: transitive
+ description:
+ name: crypto
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.5"
+ csslib:
+ dependency: transitive
+ description:
+ name: csslib
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.16.1"
+ dart_style:
+ dependency: transitive
+ description:
+ name: dart_style
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.3.6"
+ fake_async:
+ dependency: transitive
+ description:
+ name: fake_async
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.0"
+ file:
+ dependency: transitive
+ description:
+ name: file
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "5.2.1"
+ fixnum:
+ dependency: transitive
+ description:
+ name: fixnum
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.10.11"
+ flutter:
+ dependency: "direct main"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ flutter_test:
+ dependency: "direct dev"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ flutter_web_plugins:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ glob:
+ dependency: transitive
+ description:
+ name: glob
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.2.0"
+ graphs:
+ dependency: transitive
+ description:
+ name: graphs
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.2.0"
+ html:
+ dependency: transitive
+ description:
+ name: html
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.14.0+3"
+ http:
+ dependency: transitive
+ description:
+ name: http
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.12.1"
+ http_multi_server:
+ dependency: transitive
+ description:
+ name: http_multi_server
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.2.0"
+ http_parser:
+ dependency: transitive
+ description:
+ name: http_parser
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.1.4"
+ intl:
+ dependency: transitive
+ description:
+ name: intl
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.16.1"
+ io:
+ dependency: transitive
+ description:
+ name: io
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.3.4"
+ js:
+ dependency: transitive
+ description:
+ name: js
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.6.2"
+ json_annotation:
+ dependency: transitive
+ description:
+ name: json_annotation
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.1"
+ logging:
+ dependency: transitive
+ description:
+ name: logging
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.11.4"
+ matcher:
+ dependency: transitive
+ description:
+ name: matcher
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.12.6"
+ meta:
+ dependency: transitive
+ description:
+ name: meta
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.8"
+ mime:
+ dependency: transitive
+ description:
+ name: mime
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.9.6+3"
+ node_interop:
+ dependency: transitive
+ description:
+ name: node_interop
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.1"
+ node_io:
+ dependency: transitive
+ description:
+ name: node_io
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.1"
+ package_config:
+ dependency: transitive
+ description:
+ name: package_config
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.9.3"
+ path:
+ dependency: transitive
+ description:
+ name: path
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.7.0"
+ path_provider_linux:
+ dependency: transitive
+ description:
+ name: path_provider_linux
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.0.1+2"
+ path_provider_platform_interface:
+ dependency: transitive
+ description:
+ name: path_provider_platform_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.2"
+ pedantic:
+ dependency: transitive
+ description:
+ name: pedantic
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.9.0"
+ platform:
+ dependency: transitive
+ description:
+ name: platform
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.2.1"
+ plugin_platform_interface:
+ dependency: transitive
+ description:
+ name: plugin_platform_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.2"
+ pool:
+ dependency: transitive
+ description:
+ name: pool
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.4.0"
+ process:
+ dependency: transitive
+ description:
+ name: process
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.13"
+ proxmox_dart_api_client:
+ dependency: "direct main"
+ description:
+ path: "../proxmox_dart_api_client"
+ relative: true
+ source: path
+ version: "0.0.0"
+ pub_semver:
+ dependency: transitive
+ description:
+ name: pub_semver
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.4.4"
+ pubspec_parse:
+ dependency: transitive
+ description:
+ name: pubspec_parse
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.1.5"
+ quiver:
+ dependency: transitive
+ description:
+ name: quiver
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.3"
+ retry:
+ dependency: transitive
+ description:
+ name: retry
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.1"
+ shared_preferences:
+ dependency: "direct main"
+ description:
+ name: shared_preferences
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.5.8"
+ shared_preferences_linux:
+ dependency: transitive
+ description:
+ name: shared_preferences_linux
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.0.2+1"
+ shared_preferences_macos:
+ dependency: transitive
+ description:
+ name: shared_preferences_macos
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.0.1+10"
+ shared_preferences_platform_interface:
+ dependency: transitive
+ description:
+ name: shared_preferences_platform_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.4"
+ shared_preferences_web:
+ dependency: transitive
+ description:
+ name: shared_preferences_web
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.1.2+7"
+ shelf:
+ dependency: transitive
+ description:
+ name: shelf
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.7.7"
+ shelf_web_socket:
+ dependency: transitive
+ description:
+ name: shelf_web_socket
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.2.3"
+ sky_engine:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.99"
+ source_gen:
+ dependency: transitive
+ description:
+ name: source_gen
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.9.6"
+ source_span:
+ dependency: transitive
+ description:
+ name: source_span
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.7.0"
+ stack_trace:
+ dependency: transitive
+ description:
+ name: stack_trace
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.9.3"
+ stream_channel:
+ dependency: transitive
+ description:
+ name: stream_channel
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.0"
+ stream_transform:
+ dependency: transitive
+ description:
+ name: stream_transform
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.2.0"
+ string_scanner:
+ dependency: transitive
+ description:
+ name: string_scanner
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.5"
+ term_glyph:
+ dependency: transitive
+ description:
+ name: term_glyph
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.0"
+ test_api:
+ dependency: transitive
+ description:
+ name: test_api
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.2.16"
+ timing:
+ dependency: transitive
+ description:
+ name: timing
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.1.1+2"
+ typed_data:
+ dependency: transitive
+ description:
+ name: typed_data
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.6"
+ vector_math:
+ dependency: transitive
+ description:
+ name: vector_math
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.8"
+ watcher:
+ dependency: transitive
+ description:
+ name: watcher
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.9.7+15"
+ web_socket_channel:
+ dependency: transitive
+ description:
+ name: web_socket_channel
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.0"
+ xdg_directories:
+ dependency: transitive
+ description:
+ name: xdg_directories
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.1.0"
+ yaml:
+ dependency: transitive
+ description:
+ name: yaml
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.2.1"
+sdks:
+ dart: ">=2.9.0-14.0.dev <3.0.0"
+ flutter: ">=1.12.13+hotfix.5 <2.0.0"
--- /dev/null
+name: proxmox_login_manager
+description: A new Flutter package project.
+version: 0.0.1
+author:
+
+environment:
+ sdk: ">=2.7.0 <3.0.0"
+
+dependencies:
+ flutter:
+ sdk: flutter
+ shared_preferences: ^0.5.8
+ built_value: ^7.1.0
+ built_collection: ^4.3.2
+ proxmox_dart_api_client:
+ path: ../proxmox_dart_api_client
+
+
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+ build_runner: ^1.10.0
+ built_value_generator: ^7.1.0
+
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/tools/pub/pubspec
+
+# The following section is specific to Flutter.
+flutter:
+
+ # To add assets to your package, add an assets section, like this:
+ # assets:
+ # - images/a_dot_burr.jpeg
+ # - images/a_dot_ham.jpeg
+ #
+ # For details regarding assets in packages, see
+ # https://flutter.dev/assets-and-images/#from-packages
+ #
+ # An image asset can refer to one or more resolution-specific "variants", see
+ # https://flutter.dev/assets-and-images/#resolution-aware.
+
+ # To add custom fonts to your package, add a fonts section here,
+ # in this "flutter" section. Each entry in this list should have a
+ # "family" key with the font family name, and a "fonts" key with a
+ # list giving the asset and other descriptors for the font. For
+ # example:
+ fonts:
+ - family: Proxmox
+ fonts:
+ - asset: assets/fonts/Proxmox.ttf
+ #
+ # For details regarding fonts in packages, see
+ # https://flutter.dev/custom-fonts/#from-packages
--- /dev/null
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:proxmox_login_manager/proxmox_login_manager.dart';
+
+void main() {}