From 67d7bb45f513d050b0da70ff215684fc9e6c17b6 Mon Sep 17 00:00:00 2001 From: estevez-dev Date: Wed, 20 Mar 2019 23:05:25 +0200 Subject: [PATCH] Resolves #338 OAuth with Home Assistant --- lib/home_assistant.class.dart | 69 ++++++++++++++++------------ lib/main.dart | 85 +++++++++++++++++------------------ lib/settings.page.dart | 21 +-------- 3 files changed, 84 insertions(+), 91 deletions(-) diff --git a/lib/home_assistant.class.dart b/lib/home_assistant.class.dart index 4c3cb32..d795e7f 100644 --- a/lib/home_assistant.class.dart +++ b/lib/home_assistant.class.dart @@ -3,6 +3,7 @@ part of 'main.dart'; class HomeAssistant { String _webSocketAPIEndpoint; String httpWebHost; + String oauthUrl; //String _password; String _token; String _tempToken; @@ -68,6 +69,7 @@ class HomeAssistant { throw("Check connection settings"); } else { isSettingsLoaded = true; + oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent('http://ha-client.homemade.systems/')}&redirect_uri=${Uri.encodeComponent('http://ha-client.homemade.systems/service/auth_callback.html')}"; entities = EntityCollection(httpWebHost); } } @@ -85,6 +87,7 @@ class HomeAssistant { } else { Logger.d("Fetching..."); _fetchCompleter = new Completer(); + _fetchTimer?.cancel(); _fetchTimer = Timer(fetchTimeout, () { Logger.e( "Data fetching timeout"); disconnect().then((_) { @@ -104,13 +107,12 @@ class HomeAssistant { } disconnect() async { - if ((_hassioChannel != null) && (_hassioChannel.closeCode == null) && (_hassioChannel.sink != null)) { - await _hassioChannel.sink.close().timeout(Duration(seconds: 3), + Logger.d( "Socket disconnecting..."); + await _socketSubscription?.cancel(); + await _hassioChannel?.sink?.close()?.timeout(Duration(seconds: 3), onTimeout: () => Logger.d( "Socket sink closed") - ); - await _socketSubscription.cancel(); - _hassioChannel = null; - } + ); + _hassioChannel = null; } @@ -119,6 +121,7 @@ class HomeAssistant { Logger.d("Previous connection is not complited"); } else { if ((_hassioChannel == null) || (_hassioChannel.closeCode != null)) { + _connectionTimer?.cancel(); _connectionCompleter = new Completer(); autoReconnect = false; disconnect().then((_){ @@ -127,9 +130,7 @@ class HomeAssistant { Logger.e( "Socket connection timeout"); _handleSocketError(null); }); - if (_socketSubscription != null) { - _socketSubscription.cancel(); - } + _socketSubscription?.cancel(); _hassioChannel = IOWebSocketChannel.connect( _webSocketAPIEndpoint, pingInterval: Duration(seconds: 30)); _socketSubscription = _hassioChannel.stream.listen( @@ -199,7 +200,7 @@ class HomeAssistant { } void _completeFetching(error) { - _fetchTimer.cancel(); + _fetchTimer?.cancel(); _completeConnecting(error); if (!_fetchCompleter.isCompleted) { if (error != null) { @@ -213,7 +214,7 @@ class HomeAssistant { } void _completeConnecting(error) { - _connectionTimer.cancel(); + _connectionTimer?.cancel(); if (!_connectionCompleter.isCompleted) { if (error != null) { _connectionCompleter.completeError(error); @@ -241,10 +242,10 @@ class HomeAssistant { _sendSubscribe(); } else if (data["type"] == "auth_invalid") { Logger.d("[Received] <== ${data.toString()}"); - //TODO remove token and login again - _completeConnecting({"errorCode": 6, "errorMessage": "${data["message"]}"}); + _logout(); + _completeConnecting({"errorCode": 62, "errorMessage": "${data["message"]}"}); } else if (data["type"] == "result") { - Logger.d("[Received] <== ${data.toString()}"); + Logger.d("[Received] <== id: ${data['id']}, success: ${data['success']}"); _messageResolver[data["id"]]?.complete(data); _messageResolver.remove(data["id"]); } else if (data["type"] == "event") { @@ -261,6 +262,12 @@ class HomeAssistant { } } + void _logout() { + _token = null; + _tempToken = null; + SharedPreferences.getInstance().then((prefs) => prefs.remove("hassio-token")); + } + void _sendSubscribe() { _incrementMessageId(); _subscriptionMessageId = _currentMessageId; @@ -276,18 +283,19 @@ class HomeAssistant { } Future _getLongLivedToken() async { - await _sendSocketMessage(type: "auth/long_lived_access_token", additionalData: {"client_name": "HA Client 3", "client_icon": null, "lifespan": 365}).then((data) { + await _sendSocketMessage(type: "auth/long_lived_access_token", additionalData: {"client_name": "HA Client app", "lifespan": 365}).then((data) { if (data['success']) { Logger.d("Got long-lived token: ${data['result']}"); _token = data['result']; - //TODO save token + _tempToken = null; + SharedPreferences.getInstance().then((prefs) => prefs.setString("hassio-token", _token)); } else { + _logout(); Logger.e("Error getting long-lived token: ${data['error'].toString()}"); - //TODO DO DO something here } }).catchError((e) { Logger.e("Error getting long-lived token: ${e.toString()}"); - //TODO DO DO something here + _logout(); }); } @@ -337,7 +345,6 @@ class HomeAssistant { Logger.d( "No long leaved token. Need to authenticate."); final flutterWebviewPlugin = new FlutterWebviewPlugin(); flutterWebviewPlugin.onUrlChanged.listen((String url) { - Logger.d("Launched url: $url"); if (url.startsWith("http://ha-client.homemade.systems/service/auth_callback.html")) { String authCode = url.split("=")[1]; Logger.d("We have auth code. Getting temporary access token..."); @@ -354,27 +361,33 @@ class HomeAssistant { Logger.d("Firing event to reload UI"); eventBus.fire(ReloadUIEvent()); }).catchError((e) { - //TODO DO DO something here + _logout(); + disconnect(); + flutterWebviewPlugin.close(); + _completeFetching({"errorCode": 61, "errorMessage": "Error getting temp token"}); Logger.e("Error getting temp token: ${e.toString()}"); }); } }); - disconnect().then((_){ - //TODO create special error code to show "Login" in message - _completeConnecting({"errorCode": 6, "errorMessage": "Not authenticated"}); - }); - String oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent('http://ha-client.homemade.systems/')}&redirect_uri=${Uri.encodeComponent('http://ha-client.homemade.systems/service/auth_callback.html')}"; - Logger.d("OAuth url: $oauthUrl"); - eventBus.fire(StartAuthEvent(oauthUrl)); + disconnect(); + _completeFetching({"errorCode": 60, "errorMessage": "Not authenticated"}); + _requestOAuth(); } else if (_tempToken != null) { Logger.d("We have temp token. Login..."); _hassioChannel.sink.add('{"type": "auth","access_token": "$_tempToken"}'); } else { Logger.e("General login error"); - //TODO DO DO something here + _logout(); + disconnect(); + _completeFetching({"errorCode": 61, "errorMessage": "General login error"}); } } + void _requestOAuth() { + Logger.d("OAuth url: $oauthUrl"); + eventBus.fire(StartAuthEvent(oauthUrl)); + } + Future _sendSocketMessage({String type, Map additionalData, bool noId: false}) { Completer _completer = Completer(); Map dataObject = {"type": "$type"}; diff --git a/lib/main.dart b/lib/main.dart index 479cd66..aa52d9f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -104,8 +104,6 @@ EventBus eventBus = new EventBus(); const String appName = "HA Client"; const appVersion = "0.5.2"; -//String homeAssistantWebHost; - void main() { FlutterError.onError = (errorDetails) { Logger.e( "${errorDetails.exception}"); @@ -158,12 +156,7 @@ class MainPage extends StatefulWidget { } class _MainPageState extends State with WidgetsBindingObserver, TickerProviderStateMixin { - //HomeAssistant _homeAssistant; - //Map _instanceConfig; - //String _webSocketApiEndpoint; - //String _password; - //int _uiViewsCount = 0; - //String _instanceHost; + StreamSubscription _stateSubscription; StreamSubscription _settingsSubscription; StreamSubscription _serviceCallSubscription; @@ -171,11 +164,9 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker StreamSubscription _showErrorSubscription; StreamSubscription _startAuthSubscription; StreamSubscription _reloadUISubscription; - //bool _settingsLoaded = false; bool _accountMenuExpanded = false; - //bool _useLovelaceUI; int _previousViewCount; - final FirebaseMessaging _firebaseMessaging = FirebaseMessaging(); + //final FirebaseMessaging _firebaseMessaging = FirebaseMessaging(); @override void initState() { @@ -213,23 +204,6 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker } } - /*_loadConnectionSettings() async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - String domain = prefs.getString('hassio-domain'); - String port = prefs.getString('hassio-port'); - _instanceHost = "$domain:$port"; - _webSocketApiEndpoint = "${prefs.getString('hassio-protocol')}://$domain:$port/api/websocket"; - homeAssistantWebHost = "${prefs.getString('hassio-res-protocol')}://$domain:$port"; - _password = prefs.getString('hassio-password'); - _useLovelaceUI = prefs.getBool('use-lovelace') ?? true; - if ((domain == null) || (port == null) || (_password == null) || - (domain.length == 0) || (port.length == 0) || (_password.length == 0)) { - throw("Check connection settings"); - } else { - _settingsLoaded = true; - } - }*/ - _subscribe() { if (_stateSubscription == null) { _stateSubscription = eventBus.on().listen((event) { @@ -269,20 +243,12 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker if (_startAuthSubscription == null) { _startAuthSubscription = eventBus.on().listen((event){ - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => WebviewScaffold( - url: "${event.oauthUrl}", - appBar: new AppBar( - title: new Text("Login"), - ), - ), - ) - ); + _showOAuth(); }); } + + /*_firebaseMessaging.getToken().then((String token) { //Logger.d("FCM token: $token"); widget.homeAssistant.sendHTTPPost( @@ -303,6 +269,20 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker );*/ } + void _showOAuth() { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => WebviewScaffold( + url: "${widget.homeAssistant.oauthUrl}", + appBar: new AppBar( + title: new Text("Login"), + ), + ), + ) + ); + } + _refreshData() async { //widget.homeAssistant.updateSettings(_webSocketApiEndpoint, _password, _useLovelaceUI); _hideBottomBar(); @@ -539,12 +519,31 @@ class _MainPageState extends State with WidgetsBindingObserver, Ticker break; } - case 6: { + case 60: { _bottomBarAction = FlatButton( - child: Text("Settings", style: textStyle), + child: Text("Login", style: textStyle), onPressed: () { - //_scaffoldKey?.currentState?.hideCurrentSnackBar(); - Navigator.pushNamed(context, '/connection-settings'); + _refreshData(); + }, + ); + break; + } + + case 61: { + _bottomBarAction = FlatButton( + child: Text("Try again", style: textStyle), + onPressed: () { + _refreshData(); + }, + ); + break; + } + + case 62: { + _bottomBarAction = FlatButton( + child: Text("Login again", style: textStyle), + onPressed: () { + _refreshData(); }, ); break; diff --git a/lib/settings.page.dart b/lib/settings.page.dart index a8179df..044dcf8 100644 --- a/lib/settings.page.dart +++ b/lib/settings.page.dart @@ -14,8 +14,6 @@ class _ConnectionSettingsPageState extends State { String _newHassioDomain = ""; String _hassioPort = ""; String _newHassioPort = ""; - String _hassioPassword = ""; - String _newHassioPassword = ""; String _socketProtocol = "wss"; String _newSocketProtocol = "wss"; bool _useLovelace = true; @@ -36,7 +34,6 @@ class _ConnectionSettingsPageState extends State { setState(() { _hassioDomain = _newHassioDomain = prefs.getString("hassio-domain")?? ""; _hassioPort = _newHassioPort = prefs.getString("hassio-port") ?? ""; - _hassioPassword = _newHassioPassword = prefs.getString("hassio-password") ?? ""; _socketProtocol = _newSocketProtocol = prefs.getString("hassio-protocol") ?? 'wss'; try { _useLovelace = _newUseLovelace = prefs.getBool("use-lovelace") ?? true; @@ -47,7 +44,7 @@ class _ConnectionSettingsPageState extends State { } bool _checkConfigChanged() { - return ((_newHassioPassword != _hassioPassword) || + return ( (_newHassioPort != _hassioPort) || (_newHassioDomain != _hassioDomain) || (_newSocketProtocol != _socketProtocol) || @@ -62,7 +59,6 @@ class _ConnectionSettingsPageState extends State { SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.setString("hassio-domain", _newHassioDomain); prefs.setString("hassio-port", _newHassioPort); - prefs.setString("hassio-password", _newHassioPassword); prefs.setString("hassio-protocol", _newSocketProtocol); prefs.setString("hassio-res-protocol", _newSocketProtocol == "wss" ? "https" : "http"); prefs.setBool("use-lovelace", _newUseLovelace); @@ -152,21 +148,6 @@ class _ConnectionSettingsPageState extends State { "Try ports 80 and 443 if default is not working and you don't know why.", style: TextStyle(color: Colors.grey), ), - new TextField( - decoration: InputDecoration( - labelText: "Access token" - ), - controller: new TextEditingController.fromValue( - new TextEditingValue( - text: _newHassioPassword, - selection: - new TextSelection.collapsed(offset: _newHassioPassword.length) - ) - ), - onChanged: (value) { - _newHassioPassword = value; - } - ), Padding( padding: EdgeInsets.only(top: 20.0), child: Text(