From 7e1c083e688a35a63c029046d802cd1e237c2591 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Thu, 13 Jul 2023 17:49:58 +0200 Subject: [PATCH] neon: make LoginQrcode and Account a Credentials --- packages/neon/neon/lib/models.dart | 2 +- .../neon/neon/lib/src/models/account.dart | 123 +++++++++++++++++- .../neon/neon/lib/src/pages/login_qrcode.dart | 6 +- packages/neon/neon/lib/src/router.dart | 5 +- .../neon/neon/lib/src/utils/login_qrcode.dart | 47 ------- packages/neon/neon/test/account_test.dart | 25 ++++ 6 files changed, 152 insertions(+), 56 deletions(-) delete mode 100644 packages/neon/neon/lib/src/utils/login_qrcode.dart create mode 100644 packages/neon/neon/test/account_test.dart diff --git a/packages/neon/neon/lib/models.dart b/packages/neon/neon/lib/models.dart index 3c441f6e..ce1f317b 100644 --- a/packages/neon/neon/lib/models.dart +++ b/packages/neon/neon/lib/models.dart @@ -1,3 +1,3 @@ -export 'package:neon/src/models/account.dart'; +export 'package:neon/src/models/account.dart' hide Credentials, LoginQrcode; export 'package:neon/src/models/app_implementation.dart'; export 'package:neon/src/models/notifications_interface.dart'; diff --git a/packages/neon/neon/lib/src/models/account.dart b/packages/neon/neon/lib/src/models/account.dart index 5dbaca84..93ce1391 100644 --- a/packages/neon/neon/lib/src/models/account.dart +++ b/packages/neon/neon/lib/src/models/account.dart @@ -2,15 +2,29 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:crypto/crypto.dart'; -import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; import 'package:nextcloud/nextcloud.dart'; part 'account.g.dart'; +/// Credentials interface +@internal +@immutable +abstract interface class Credentials { + /// Url of the server + abstract final String serverURL; + + /// Username + abstract final String username; + + /// Password + abstract final String? password; +} + @JsonSerializable() @immutable -class Account { +class Account implements Credentials { Account({ required this.serverURL, required this.loginName, @@ -29,9 +43,12 @@ class Account { factory Account.fromJson(final Map json) => _$AccountFromJson(json); Map toJson() => _$AccountToJson(this); + @override final String serverURL; final String loginName; + @override final String username; + @override final String? password; final String? userAgent; @@ -76,3 +93,105 @@ extension AccountFind on Iterable { Account? tryFind(final String? accountID) => firstWhereOrNull((final account) => account.id == accountID); Account find(final String accountID) => firstWhere((final account) => account.id == accountID); } + +/// Qrcode Login credentials. +/// +/// The Credentials as provided by the server when manually creating an app +/// password. +@internal +@immutable +class LoginQrcode implements Credentials { + /// Creates a new LoginQrcode object. + @visibleForTesting + const LoginQrcode({ + required this.serverURL, + required this.username, + required this.password, + }); + + @override + final String serverURL; + @override + final String username; + @override + final String password; + + /// Pattern matching the full Qrcode content. + static final _loginQrcodeUrlRegex = RegExp(r'^nc://login/user:(.*)&password:(.*)&server:(.*)$'); + + /// Pattern matching the path part of the Qrcode. + /// + /// This is used when launching the app through an intent. + static final _loginQrcodePathRegex = RegExp(r'^/user:(.*)&password:(.*)&server:(.*)$'); + + /// Creates a new `LoginQrcode` object by parsing a url string. + /// + /// If the [url] string is not valid as a LoginQrcode a [FormatException] is + /// thrown. + /// + /// Example: + /// ```dart + /// final loginQrcode = + /// LoginQrcode.parse('nc://login/user:JohnDoe&password:super_secret&server:example.com'); + /// print(loginQrcode.serverURL); // JohnDoe + /// print(loginQrcode.username); // super_secret + /// print(loginQrcode.password); // example.com + /// + /// LoginQrcode.parse('::Not valid LoginQrcode::'); // Throws FormatException. + /// ``` + static LoginQrcode parse(final String url) { + for (final regex in [_loginQrcodeUrlRegex, _loginQrcodePathRegex]) { + final matches = regex.allMatches(url); + if (matches.isEmpty) { + continue; + } + + final match = matches.single; + if (match.groupCount != 3) { + continue; + } + + return LoginQrcode( + serverURL: match.group(3)!, + username: match.group(1)!, + password: match.group(2)!, + ); + } + + throw const FormatException(); + } + + /// Creates a new `LoginQrcode` object by parsing a url string. + /// + /// Returns `null` if the [url] string is not valid as a LoginQrcode. + /// + /// Example: + /// ```dart + /// final loginQrcode = + /// LoginQrcode.parse('nc://login/user:JohnDoe&password:super_secret&server:example.com'); + /// print(loginQrcode.serverURL); // JohnDoe + /// print(loginQrcode.username); // super_secret + /// print(loginQrcode.password); // example.com + /// + /// final notLoginQrcode = LoginQrcode.tryParse('::Not valid LoginQrcode::'); + /// print(notLoginQrcode); // null + /// ``` + static LoginQrcode? tryParse(final String url) { + try { + return parse(url); + } on FormatException { + return null; + } + } + + @override + bool operator ==(final Object other) => + other is LoginQrcode && other.serverURL == serverURL && other.username == username && other.password == password; + + @override + int get hashCode => Object.hashAll([ + serverURL, + username, + password, + ]); +} diff --git a/packages/neon/neon/lib/src/pages/login_qrcode.dart b/packages/neon/neon/lib/src/pages/login_qrcode.dart index 5c1086f2..c92edb0b 100644 --- a/packages/neon/neon/lib/src/pages/login_qrcode.dart +++ b/packages/neon/neon/lib/src/pages/login_qrcode.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_zxing/flutter_zxing.dart'; +import 'package:neon/src/models/account.dart'; import 'package:neon/src/router.dart'; import 'package:neon/src/utils/exceptions.dart'; -import 'package:neon/src/utils/login_qrcode.dart'; import 'package:neon/src/widgets/exception.dart'; class LoginQrcodePage extends StatefulWidget { @@ -41,8 +41,8 @@ class _LoginQrcodePageState extends State { } LoginCheckServerStatusRoute.withCredentials( - serverUrl: match.server, - loginName: match.user, + serverUrl: match.serverURL, + loginName: match.username, password: match.password, ).pushReplacement(context); } catch (e, s) { diff --git a/packages/neon/neon/lib/src/router.dart b/packages/neon/neon/lib/src/router.dart index 708e5b46..72b6ca43 100644 --- a/packages/neon/neon/lib/src/router.dart +++ b/packages/neon/neon/lib/src/router.dart @@ -17,7 +17,6 @@ import 'package:neon/src/pages/login_flow.dart'; import 'package:neon/src/pages/login_qrcode.dart'; import 'package:neon/src/pages/nextcloud_app_settings.dart'; import 'package:neon/src/pages/settings.dart'; -import 'package:neon/src/utils/login_qrcode.dart'; import 'package:neon/src/utils/stream_listenable.dart'; import 'package:provider/provider.dart'; @@ -36,8 +35,8 @@ class AppRouter extends GoRouter { final loginQrcode = LoginQrcode.tryParse(state.location); if (loginQrcode != null) { return LoginCheckServerStatusRoute.withCredentials( - serverUrl: loginQrcode.server, - loginName: loginQrcode.user, + serverUrl: loginQrcode.serverURL, + loginName: loginQrcode.username, password: loginQrcode.password, ).location; } diff --git a/packages/neon/neon/lib/src/utils/login_qrcode.dart b/packages/neon/neon/lib/src/utils/login_qrcode.dart deleted file mode 100644 index 4e2081e2..00000000 --- a/packages/neon/neon/lib/src/utils/login_qrcode.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:meta/meta.dart'; - -@internal -class LoginQrcode { - LoginQrcode({ - required this.server, - required this.user, - required this.password, - }); - - static final _loginQrcodeUrlRegex = RegExp(r'^nc://login/user:(.*)&password:(.*)&server:(.*)$'); - static final _loginQrcodePathRegex = RegExp(r'^/user:(.*)&password:(.*)&server:(.*)$'); - - static LoginQrcode parse(final String url) { - for (final regex in [_loginQrcodeUrlRegex, _loginQrcodePathRegex]) { - final matches = regex.allMatches(url); - if (matches.isEmpty) { - continue; - } - - final match = matches.single; - if (match.groupCount != 3) { - continue; - } - - return LoginQrcode( - server: match.group(3)!, - user: match.group(1)!, - password: match.group(2)!, - ); - } - - throw const FormatException(); - } - - static LoginQrcode? tryParse(final String url) { - try { - return parse(url); - } on FormatException { - return null; - } - } - - final String server; - final String user; - final String password; -} diff --git a/packages/neon/neon/test/account_test.dart b/packages/neon/neon/test/account_test.dart new file mode 100644 index 00000000..57b7c527 --- /dev/null +++ b/packages/neon/neon/test/account_test.dart @@ -0,0 +1,25 @@ +import 'package:neon/src/models/account.dart'; +import 'package:test/test.dart'; + +void main() { + const qrCodePath = '/user:JohnDoe&password:super_secret&server:example.com'; + const qrCode = 'nc://login$qrCodePath'; + const invalidUrl = '::Not valid LoginQrcode::'; + const credentials = LoginQrcode( + serverURL: 'example.com', + username: 'JohnDoe', + password: 'super_secret', + ); + + group('LoginQrcode', () { + test('parse', () { + expect(LoginQrcode.tryParse(qrCode), equals(credentials)); + expect(LoginQrcode.tryParse(qrCodePath), equals(credentials)); + expect(LoginQrcode.tryParse(invalidUrl), null); + }); + + test('equality', () { + expect(credentials, equals(credentials)); + }); + }); +}