Resolves #338 OAuth with Home Assistant

This commit is contained in:
estevez-dev 2019-03-20 23:05:25 +02:00
parent 6a03105d01
commit 67d7bb45f5
3 changed files with 84 additions and 91 deletions

View File

@ -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"};

View File

@ -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<MainPage> 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<MainPage> 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<MainPage> 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<StateChangedEvent>().listen((event) {
@ -269,20 +243,12 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
if (_startAuthSubscription == null) {
_startAuthSubscription = eventBus.on<StartAuthEvent>().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<MainPage> 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<MainPage> 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;

View File

@ -14,8 +14,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
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<ConnectionSettingsPage> {
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<ConnectionSettingsPage> {
}
bool _checkConfigChanged() {
return ((_newHassioPassword != _hassioPassword) ||
return (
(_newHassioPort != _hassioPort) ||
(_newHassioDomain != _hassioDomain) ||
(_newSocketProtocol != _socketProtocol) ||
@ -62,7 +59,6 @@ class _ConnectionSettingsPageState extends State<ConnectionSettingsPage> {
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<ConnectionSettingsPage> {
"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(