mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-02 06:46:22 +01:00
[123] change login form to nextcloud login flow v2 (#1)
This commit is contained in:
parent
c19003e5b9
commit
34066215c0
5 changed files with 350 additions and 153 deletions
|
@ -28,18 +28,42 @@ class Response {
|
||||||
final dynamic body;
|
final dynamic body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BasicAuth {
|
||||||
|
BasicAuth(this.username, this.password);
|
||||||
|
|
||||||
|
BasicAuth.fromAccount(Account account)
|
||||||
|
: this(
|
||||||
|
account.username2,
|
||||||
|
account.password,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() {
|
||||||
|
final authString = base64.encode(utf8.encode("$username:$password"));
|
||||||
|
return "Basic $authString";
|
||||||
|
}
|
||||||
|
|
||||||
|
final String username;
|
||||||
|
final String password;
|
||||||
|
}
|
||||||
|
|
||||||
class Api {
|
class Api {
|
||||||
Api(this._account);
|
Api(Account account)
|
||||||
|
: _baseUrl = Uri.parse(account.url),
|
||||||
|
_auth = BasicAuth.fromAccount(account);
|
||||||
|
|
||||||
|
Api.fromBaseUrl(Uri baseUrl) : _baseUrl = baseUrl;
|
||||||
|
|
||||||
ApiFiles files() => ApiFiles(this);
|
ApiFiles files() => ApiFiles(this);
|
||||||
|
|
||||||
ApiOcs ocs() => ApiOcs(this);
|
ApiOcs ocs() => ApiOcs(this);
|
||||||
|
|
||||||
ApiSystemtags systemtags() => ApiSystemtags(this);
|
ApiSystemtags systemtags() => ApiSystemtags(this);
|
||||||
|
|
||||||
ApiSystemtagsRelations systemtagsRelations() => ApiSystemtagsRelations(this);
|
ApiSystemtagsRelations systemtagsRelations() => ApiSystemtagsRelations(this);
|
||||||
|
|
||||||
static String getAuthorizationHeaderValue(Account account) {
|
static String getAuthorizationHeaderValue(Account account) {
|
||||||
final auth =
|
return BasicAuth.fromAccount(account).toString();
|
||||||
base64.encode(utf8.encode("${account.username2}:${account.password}"));
|
|
||||||
return "Basic $auth";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> request(
|
Future<Response> request(
|
||||||
|
@ -52,10 +76,12 @@ class Api {
|
||||||
bool isResponseString = true,
|
bool isResponseString = true,
|
||||||
}) async {
|
}) async {
|
||||||
final url = _makeUri(endpoint, queryParameters: queryParameters);
|
final url = _makeUri(endpoint, queryParameters: queryParameters);
|
||||||
final req = http.Request(method, url)
|
final req = http.Request(method, url);
|
||||||
..headers.addAll({
|
if (_auth != null) {
|
||||||
"authorization": getAuthorizationHeaderValue(_account),
|
req.headers.addAll({
|
||||||
|
"authorization": _auth.toString(),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
if (header != null) {
|
if (header != null) {
|
||||||
// turn all to lower case, since HTTP headers are case-insensitive, this
|
// turn all to lower case, since HTTP headers are case-insensitive, this
|
||||||
// smooths our processing (if any)
|
// smooths our processing (if any)
|
||||||
|
@ -89,19 +115,16 @@ class Api {
|
||||||
String endpoint, {
|
String endpoint, {
|
||||||
Map<String, String>? queryParameters,
|
Map<String, String>? queryParameters,
|
||||||
}) {
|
}) {
|
||||||
final splits = _account.address.split("/");
|
final path = _baseUrl.path + "/$endpoint";
|
||||||
final authority = splits[0];
|
if (_baseUrl.scheme == "http") {
|
||||||
final path = splits.length > 1
|
return Uri.http(_baseUrl.authority, path, queryParameters);
|
||||||
? splits.sublist(1).join("/") + "/$endpoint"
|
|
||||||
: endpoint;
|
|
||||||
if (_account.scheme == "http") {
|
|
||||||
return Uri.http(authority, path, queryParameters);
|
|
||||||
} else {
|
} else {
|
||||||
return Uri.https(authority, path, queryParameters);
|
return Uri.https(_baseUrl.authority, path, queryParameters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final Account _account;
|
final Uri _baseUrl;
|
||||||
|
BasicAuth? _auth;
|
||||||
|
|
||||||
static final _log = Logger("api.api.Api");
|
static final _log = Logger("api.api.Api");
|
||||||
}
|
}
|
||||||
|
@ -451,7 +474,9 @@ class ApiOcs {
|
||||||
ApiOcs(this._api);
|
ApiOcs(this._api);
|
||||||
|
|
||||||
ApiOcsDav dav() => ApiOcsDav(this);
|
ApiOcsDav dav() => ApiOcsDav(this);
|
||||||
|
|
||||||
ApiOcsFacerecognition facerecognition() => ApiOcsFacerecognition(this);
|
ApiOcsFacerecognition facerecognition() => ApiOcsFacerecognition(this);
|
||||||
|
|
||||||
ApiOcsFilesSharing filesSharing() => ApiOcsFilesSharing(this);
|
ApiOcsFilesSharing filesSharing() => ApiOcsFilesSharing(this);
|
||||||
|
|
||||||
final Api _api;
|
final Api _api;
|
||||||
|
@ -499,6 +524,7 @@ class ApiOcsFacerecognition {
|
||||||
ApiOcsFacerecognition(this._ocs);
|
ApiOcsFacerecognition(this._ocs);
|
||||||
|
|
||||||
ApiOcsFacerecognitionPersons persons() => ApiOcsFacerecognitionPersons(this);
|
ApiOcsFacerecognitionPersons persons() => ApiOcsFacerecognitionPersons(this);
|
||||||
|
|
||||||
ApiOcsFacerecognitionPerson person(String name) =>
|
ApiOcsFacerecognitionPerson person(String name) =>
|
||||||
ApiOcsFacerecognitionPerson(this, name);
|
ApiOcsFacerecognitionPerson(this, name);
|
||||||
|
|
||||||
|
@ -571,8 +597,10 @@ class ApiOcsFilesSharing {
|
||||||
ApiOcsFilesSharing(this._ocs);
|
ApiOcsFilesSharing(this._ocs);
|
||||||
|
|
||||||
ApiOcsFilesSharingShares shares() => ApiOcsFilesSharingShares(this);
|
ApiOcsFilesSharingShares shares() => ApiOcsFilesSharingShares(this);
|
||||||
|
|
||||||
ApiOcsFilesSharingShare share(String shareId) =>
|
ApiOcsFilesSharingShare share(String shareId) =>
|
||||||
ApiOcsFilesSharingShare(this, shareId);
|
ApiOcsFilesSharingShare(this, shareId);
|
||||||
|
|
||||||
ApiOcsFilesSharingSharees sharees() => ApiOcsFilesSharingSharees(this);
|
ApiOcsFilesSharingSharees sharees() => ApiOcsFilesSharingSharees(this);
|
||||||
|
|
||||||
final ApiOcs _ocs;
|
final ApiOcs _ocs;
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
/// Helper functions working with remote Nextcloud server
|
/// Helper functions working with remote Nextcloud server
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/api/api.dart';
|
import 'package:nc_photos/api/api.dart';
|
||||||
|
@ -104,37 +107,130 @@ String getFacePreviewUrlRelative(
|
||||||
return "index.php/apps/facerecognition/face/$faceId/thumb/$size";
|
return "index.php/apps/facerecognition/face/$faceId/thumb/$size";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Query the app password for [account]
|
/// Initiate a login with Nextclouds login flow v2: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2
|
||||||
Future<String> exchangePassword(Account account) async {
|
Future<InitiateLoginResponse> initiateLogin(Uri uri) async {
|
||||||
final response = await Api(account).request(
|
final response = await Api.fromBaseUrl(uri).request(
|
||||||
"GET",
|
"POST",
|
||||||
"ocs/v2.php/core/getapppassword",
|
"index.php/login/v2",
|
||||||
header: {
|
header: {
|
||||||
"OCS-APIRequest": "true",
|
"User-Agent": "nc-photos",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (response.isGood) {
|
if (response.isGood) {
|
||||||
try {
|
return InitiateLoginResponse.fromJsonString(response.body);
|
||||||
final appPwdRegex = RegExp(r"<apppassword>(.*)</apppassword>");
|
|
||||||
final appPwdMatch = appPwdRegex.firstMatch(response.body);
|
|
||||||
return appPwdMatch!.group(1)!;
|
|
||||||
} catch (_) {
|
|
||||||
// this happens when the address is not the base URL and so Nextcloud
|
|
||||||
// returned the login page
|
|
||||||
throw InvalidBaseUrlException();
|
|
||||||
}
|
|
||||||
} else if (response.statusCode == 403) {
|
|
||||||
// If the client is authenticated with an app password a 403 will be
|
|
||||||
// returned
|
|
||||||
_log.info("[exchangePassword] Already an app password");
|
|
||||||
return account.password;
|
|
||||||
} else {
|
} else {
|
||||||
_log.severe(
|
_log.severe(
|
||||||
"[exchangePassword] Failed while requesting app password: $response");
|
"[initiateLogin] Failed while requesting app password: $response");
|
||||||
throw ApiException(
|
throw ApiException(
|
||||||
response: response,
|
response: response,
|
||||||
message: "Server responed with an error: HTTP ${response.statusCode}");
|
message: "Server responded with an error: HTTP ${response.statusCode}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieve App Password after successful initiation of login with Nextclouds login flow v2: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2
|
||||||
|
Future<AppPasswordResponse> _getAppPassword(
|
||||||
|
InitiateLoginPollOptions options) async {
|
||||||
|
Uri baseUrl;
|
||||||
|
if (options.endpoint.scheme == "http") {
|
||||||
|
baseUrl = Uri.http(options.endpoint.authority);
|
||||||
|
} else {
|
||||||
|
baseUrl = Uri.https(options.endpoint.authority);
|
||||||
|
}
|
||||||
|
final response = await Api.fromBaseUrl(baseUrl).request(
|
||||||
|
"POST", options.endpoint.pathSegments.join("/"),
|
||||||
|
header: {
|
||||||
|
"User-Agent": "nc-photos",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
},
|
||||||
|
body: "token=${options.token}");
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return AppPasswordSuccess.fromJsonString(response.body);
|
||||||
|
} else if (response.statusCode == 404) {
|
||||||
|
return AppPasswordPending();
|
||||||
|
} else {
|
||||||
|
_log.severe(
|
||||||
|
"[getAppPassword] Failed while requesting app password: $response");
|
||||||
|
throw ApiException(
|
||||||
|
response: response,
|
||||||
|
message: "Server responded with an error: HTTP ${response.statusCode}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Polls the app password endpoint every 5 seconds as lang as the token is valid (currently fixed to 20 min)
|
||||||
|
/// See https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2
|
||||||
|
Stream<Future<AppPasswordResponse>> pollAppPassword(
|
||||||
|
InitiateLoginPollOptions options) {
|
||||||
|
return Stream.periodic(
|
||||||
|
const Duration(seconds: 5), (_) => _getAppPassword(options))
|
||||||
|
.takeWhile((_) => options.isTokenValid());
|
||||||
|
}
|
||||||
|
|
||||||
|
class InitiateLoginResponse {
|
||||||
|
InitiateLoginResponse.fromJsonString(String jsonString) {
|
||||||
|
final json = jsonDecode(jsonString);
|
||||||
|
poll = InitiateLoginPollOptions(
|
||||||
|
json['poll']['token'], json['poll']['endpoint']);
|
||||||
|
login = json['login'];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() {
|
||||||
|
return "$runtimeType {"
|
||||||
|
"poll: $poll, "
|
||||||
|
"login: $login, "
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
late InitiateLoginPollOptions poll;
|
||||||
|
late String login;
|
||||||
|
}
|
||||||
|
|
||||||
|
class InitiateLoginPollOptions {
|
||||||
|
InitiateLoginPollOptions(this.token, String endpoint)
|
||||||
|
: endpoint = Uri.parse(endpoint),
|
||||||
|
_validUntil = DateTime.now().add(const Duration(minutes: 20));
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() {
|
||||||
|
return "$runtimeType {"
|
||||||
|
"token: ${kDebugMode ? token : '***'}, "
|
||||||
|
"endpoint: $endpoint, "
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isTokenValid() {
|
||||||
|
return DateTime.now().isBefore(_validUntil);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String token;
|
||||||
|
final Uri endpoint;
|
||||||
|
final DateTime _validUntil;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class AppPasswordResponse {}
|
||||||
|
|
||||||
|
class AppPasswordSuccess implements AppPasswordResponse {
|
||||||
|
AppPasswordSuccess.fromJsonString(String jsonString) {
|
||||||
|
final json = jsonDecode(jsonString);
|
||||||
|
server = Uri.parse(json['server']);
|
||||||
|
loginName = json['loginName'];
|
||||||
|
appPassword = json['appPassword'];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() {
|
||||||
|
return "$runtimeType {"
|
||||||
|
"server: $server, "
|
||||||
|
"loginName: $loginName, "
|
||||||
|
"appPassword: ${kDebugMode ? appPassword : '***'}, "
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
late Uri server;
|
||||||
|
late String loginName;
|
||||||
|
late String appPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppPasswordPending implements AppPasswordResponse {}
|
||||||
|
|
||||||
final _log = Logger("api.api_util");
|
final _log = Logger("api.api_util");
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
|
||||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||||
import 'package:nc_photos/exception.dart';
|
import 'package:nc_photos/exception.dart';
|
||||||
|
|
||||||
|
@ -11,17 +11,58 @@ abstract class AppPasswordExchangeBlocEvent {
|
||||||
const AppPasswordExchangeBlocEvent();
|
const AppPasswordExchangeBlocEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppPasswordExchangeBlocConnect extends AppPasswordExchangeBlocEvent {
|
class AppPasswordExchangeBlocInitiateLogin
|
||||||
const AppPasswordExchangeBlocConnect(this.account);
|
extends AppPasswordExchangeBlocEvent {
|
||||||
|
const AppPasswordExchangeBlocInitiateLogin(this.uri);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
toString() {
|
toString() {
|
||||||
return "$runtimeType {"
|
return "$runtimeType {"
|
||||||
"account: $account, "
|
"uri: $uri, "
|
||||||
"}";
|
"}";
|
||||||
}
|
}
|
||||||
|
|
||||||
final Account account;
|
final Uri uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppPasswordExchangeBlocPoll extends AppPasswordExchangeBlocEvent {
|
||||||
|
const AppPasswordExchangeBlocPoll(this.pollOptions);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() {
|
||||||
|
return "$runtimeType {"
|
||||||
|
"pollOptions: $pollOptions, "
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
final api_util.InitiateLoginPollOptions pollOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppPasswordExchangeBlocAppPwReceived
|
||||||
|
extends AppPasswordExchangeBlocEvent {
|
||||||
|
const _AppPasswordExchangeBlocAppPwReceived(this.appPasswordResponse);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() {
|
||||||
|
return "$runtimeType {"
|
||||||
|
"appPasswordResponse: $appPasswordResponse, "
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
final api_util.AppPasswordSuccess appPasswordResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppPasswordExchangeBlocAppPwFailed extends AppPasswordExchangeBlocEvent {
|
||||||
|
const _AppPasswordExchangeBlocAppPwFailed(this.exception);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() {
|
||||||
|
return "$runtimeType {"
|
||||||
|
"exception: $exception, "
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
final dynamic exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class AppPasswordExchangeBlocState {
|
abstract class AppPasswordExchangeBlocState {
|
||||||
|
@ -32,17 +73,31 @@ class AppPasswordExchangeBlocInit extends AppPasswordExchangeBlocState {
|
||||||
const AppPasswordExchangeBlocInit();
|
const AppPasswordExchangeBlocInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppPasswordExchangeBlocSuccess extends AppPasswordExchangeBlocState {
|
class AppPasswordExchangeBlocInitiateLoginSuccess
|
||||||
const AppPasswordExchangeBlocSuccess(this.password);
|
extends AppPasswordExchangeBlocState {
|
||||||
|
const AppPasswordExchangeBlocInitiateLoginSuccess(this.result);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
toString() {
|
toString() {
|
||||||
return "$runtimeType {"
|
return "$runtimeType {"
|
||||||
"password: ${kDebugMode ? password : '***'}, "
|
"result: $result, "
|
||||||
"}";
|
"}";
|
||||||
}
|
}
|
||||||
|
|
||||||
final String password;
|
final api_util.InitiateLoginResponse result;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppPasswordExchangeBlocAppPwSuccess extends AppPasswordExchangeBlocState {
|
||||||
|
const AppPasswordExchangeBlocAppPwSuccess(this.result);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() {
|
||||||
|
return "$runtimeType {"
|
||||||
|
"result: $result, "
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
final api_util.AppPasswordSuccess result;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppPasswordExchangeBlocFailure extends AppPasswordExchangeBlocState {
|
class AppPasswordExchangeBlocFailure extends AppPasswordExchangeBlocState {
|
||||||
|
@ -58,6 +113,20 @@ class AppPasswordExchangeBlocFailure extends AppPasswordExchangeBlocState {
|
||||||
final dynamic exception;
|
final dynamic exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Business Logic Component (BLoC) which handles the App password exchange.
|
||||||
|
///
|
||||||
|
/// The flow followed by this component is described in the Nextcloud documentation under
|
||||||
|
/// https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// Event [AppPasswordExchangeBlocInitiateLogin] -> State [AppPasswordExchangeBlocInitiateLoginSuccess]
|
||||||
|
/// -> State [AppPasswordExchangeBlocFailure]
|
||||||
|
/// Event [AppPasswordExchangeBlocPoll] -> Event [AppPasswordExchangeBlocAppPwReceived]
|
||||||
|
/// -> Event [AppPasswordExchangeBlocAppPwFailed]
|
||||||
|
/// Event [AppPasswordExchangeBlocAppPwReceived] -> State [AppPasswordExchangeBlocAppPwSuccess]
|
||||||
|
/// -> State [AppPasswordExchangeBlocFailure]
|
||||||
|
/// Event [AppPasswordExchangeBlocAppPwFailed] -> State [AppPasswordExchangeBlocFailure]
|
||||||
|
/// ```
|
||||||
class AppPasswordExchangeBloc
|
class AppPasswordExchangeBloc
|
||||||
extends Bloc<AppPasswordExchangeBlocEvent, AppPasswordExchangeBlocState> {
|
extends Bloc<AppPasswordExchangeBlocEvent, AppPasswordExchangeBlocState> {
|
||||||
AppPasswordExchangeBloc() : super(const AppPasswordExchangeBlocInit()) {
|
AppPasswordExchangeBloc() : super(const AppPasswordExchangeBlocInit()) {
|
||||||
|
@ -67,36 +136,96 @@ class AppPasswordExchangeBloc
|
||||||
Future<void> _onEvent(AppPasswordExchangeBlocEvent event,
|
Future<void> _onEvent(AppPasswordExchangeBlocEvent event,
|
||||||
Emitter<AppPasswordExchangeBlocState> emit) async {
|
Emitter<AppPasswordExchangeBlocState> emit) async {
|
||||||
_log.info("[_onEvent] $event");
|
_log.info("[_onEvent] $event");
|
||||||
if (event is AppPasswordExchangeBlocConnect) {
|
if (event is AppPasswordExchangeBlocInitiateLogin) {
|
||||||
await _onEventConnect(event, emit);
|
await _onEventInitiateLogin(event, emit);
|
||||||
|
} else if (event is AppPasswordExchangeBlocPoll) {
|
||||||
|
await _onEventPoll(event, emit);
|
||||||
|
} else if (event is _AppPasswordExchangeBlocAppPwReceived) {
|
||||||
|
await _onEventAppPasswordReceived(event, emit);
|
||||||
|
} else if (event is _AppPasswordExchangeBlocAppPwFailed) {
|
||||||
|
await _onEventAppPasswordFailure(event, emit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onEventConnect(AppPasswordExchangeBlocConnect ev,
|
Future<void> _onEventInitiateLogin(AppPasswordExchangeBlocInitiateLogin ev,
|
||||||
Emitter<AppPasswordExchangeBlocState> emit) async {
|
Emitter<AppPasswordExchangeBlocState> emit) async {
|
||||||
final account = ev.account;
|
final uri = ev.uri;
|
||||||
try {
|
try {
|
||||||
final appPwd = await api_util.exchangePassword(account);
|
final initiateLoginResponse = await api_util.initiateLogin(uri);
|
||||||
emit(AppPasswordExchangeBlocSuccess(appPwd));
|
emit(AppPasswordExchangeBlocInitiateLoginSuccess(initiateLoginResponse));
|
||||||
} on InvalidBaseUrlException catch (e) {
|
} on InvalidBaseUrlException catch (e) {
|
||||||
_log.warning("[_exchangePassword] Invalid base url");
|
_log.warning("[_onEventInitiateLogin] Invalid base url");
|
||||||
emit(AppPasswordExchangeBlocFailure(e));
|
emit(AppPasswordExchangeBlocFailure(e));
|
||||||
} on HandshakeException catch (e) {
|
} on HandshakeException catch (e) {
|
||||||
_log.info("[_exchangePassword] Self-signed cert");
|
_log.info("[_onEventInitiateLogin] Self-signed cert");
|
||||||
emit(AppPasswordExchangeBlocFailure(e));
|
emit(AppPasswordExchangeBlocFailure(e));
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
if (e is ApiException && e.response.statusCode == 401) {
|
_log.shout("[_onEventInitiateLogin] Failed while exchanging password", e,
|
||||||
// wrong password, normal
|
stacktrace);
|
||||||
_log.warning(
|
|
||||||
"[_exchangePassword] Server response 401, wrong password?");
|
|
||||||
} else {
|
|
||||||
_log.shout("[_exchangePassword] Failed while exchanging password", e,
|
|
||||||
stacktrace);
|
|
||||||
}
|
|
||||||
emit(AppPasswordExchangeBlocFailure(e));
|
emit(AppPasswordExchangeBlocFailure(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _onEventPoll(AppPasswordExchangeBlocPoll ev,
|
||||||
|
Emitter<AppPasswordExchangeBlocState> emit) async {
|
||||||
|
final pollOptions = ev.pollOptions;
|
||||||
|
try {
|
||||||
|
await _pollPasswordSubscription?.cancel();
|
||||||
|
_pollPasswordSubscription = api_util
|
||||||
|
.pollAppPassword(pollOptions)
|
||||||
|
.listen(_pollAppPasswordStreamListener);
|
||||||
|
} catch (e, stacktrace) {
|
||||||
|
await _pollPasswordSubscription?.cancel();
|
||||||
|
_log.shout(
|
||||||
|
"[_onEventPoll] Failed while polling for password", e, stacktrace);
|
||||||
|
emit(AppPasswordExchangeBlocFailure(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pollAppPasswordStreamListener(event) async {
|
||||||
|
try {
|
||||||
|
final appPasswordResponse = await event;
|
||||||
|
if (appPasswordResponse is api_util.AppPasswordSuccess) {
|
||||||
|
await _pollPasswordSubscription?.cancel();
|
||||||
|
add(_AppPasswordExchangeBlocAppPwReceived(appPasswordResponse));
|
||||||
|
}
|
||||||
|
} catch (e, stacktrace) {
|
||||||
|
_log.shout(
|
||||||
|
"[_pollAppPasswordStreamListener] Failed while polling for password", e, stacktrace);
|
||||||
|
add(_AppPasswordExchangeBlocAppPwFailed(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onEventAppPasswordReceived(
|
||||||
|
_AppPasswordExchangeBlocAppPwReceived ev,
|
||||||
|
Emitter<AppPasswordExchangeBlocState> emit) async {
|
||||||
|
try {
|
||||||
|
emit(AppPasswordExchangeBlocAppPwSuccess(ev.appPasswordResponse));
|
||||||
|
} catch (e, stacktrace) {
|
||||||
|
_log.shout(
|
||||||
|
"[_onEventAppPasswordReceived] Failed while exchanging password",
|
||||||
|
e,
|
||||||
|
stacktrace);
|
||||||
|
emit(AppPasswordExchangeBlocFailure(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onEventAppPasswordFailure(
|
||||||
|
_AppPasswordExchangeBlocAppPwFailed ev,
|
||||||
|
Emitter<AppPasswordExchangeBlocState> emit) async {
|
||||||
|
await _pollPasswordSubscription?.cancel();
|
||||||
|
emit(AppPasswordExchangeBlocFailure(ev.exception));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
_pollPasswordSubscription?.cancel();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
|
||||||
static final _log =
|
static final _log =
|
||||||
Logger("bloc.app_password_exchange.AppPasswordExchangeBloc");
|
Logger("bloc.app_password_exchange.AppPasswordExchangeBloc");
|
||||||
|
|
||||||
|
StreamSubscription<Future<api_util.AppPasswordResponse>>?
|
||||||
|
_pollPasswordSubscription;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,9 +22,9 @@ import 'package:nc_photos/url_launcher_util.dart';
|
||||||
import 'package:nc_photos/use_case/ls_single_file.dart';
|
import 'package:nc_photos/use_case/ls_single_file.dart';
|
||||||
|
|
||||||
class ConnectArguments {
|
class ConnectArguments {
|
||||||
ConnectArguments(this.account);
|
ConnectArguments(this.uri);
|
||||||
|
|
||||||
final Account account;
|
final Uri uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Connect extends StatefulWidget {
|
class Connect extends StatefulWidget {
|
||||||
|
@ -36,19 +36,19 @@ class Connect extends StatefulWidget {
|
||||||
|
|
||||||
const Connect({
|
const Connect({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.account,
|
required this.uri,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
Connect.fromArgs(ConnectArguments args, {Key? key})
|
Connect.fromArgs(ConnectArguments args, {Key? key})
|
||||||
: this(
|
: this(
|
||||||
key: key,
|
key: key,
|
||||||
account: args.account,
|
uri: args.uri,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
createState() => _ConnectState();
|
createState() => _ConnectState();
|
||||||
|
|
||||||
final Account account;
|
final Uri uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ConnectState extends State<Connect> {
|
class _ConnectState extends State<Connect> {
|
||||||
|
@ -58,6 +58,12 @@ class _ConnectState extends State<Connect> {
|
||||||
_initBloc();
|
_initBloc();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_bloc.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
build(BuildContext context) {
|
build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
@ -71,7 +77,7 @@ class _ConnectState extends State<Connect> {
|
||||||
|
|
||||||
void _initBloc() {
|
void _initBloc() {
|
||||||
_log.info("[_initBloc] Initialize bloc");
|
_log.info("[_initBloc] Initialize bloc");
|
||||||
_connect();
|
_initiateLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildContent(BuildContext context) {
|
Widget _buildContent(BuildContext context) {
|
||||||
|
@ -88,7 +94,7 @@ class _ConnectState extends State<Connect> {
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
L10n.global().connectingToServer(widget.account.url),
|
L10n.global().connectingToServer(widget.uri),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.headline6,
|
style: Theme.of(context).textTheme.headline6,
|
||||||
)
|
)
|
||||||
|
@ -100,9 +106,19 @@ class _ConnectState extends State<Connect> {
|
||||||
|
|
||||||
void _onStateChange(
|
void _onStateChange(
|
||||||
BuildContext context, AppPasswordExchangeBlocState state) {
|
BuildContext context, AppPasswordExchangeBlocState state) {
|
||||||
if (state is AppPasswordExchangeBlocSuccess) {
|
if (state is AppPasswordExchangeBlocInitiateLoginSuccess) {
|
||||||
final newAccount = widget.account.copyWith(password: state.password);
|
// launch the login url
|
||||||
_log.info("[_onStateChange] Password exchanged: $newAccount");
|
launch(state.result.login);
|
||||||
|
// and start polling the API for login credentials
|
||||||
|
_bloc.add(AppPasswordExchangeBlocPoll(state.result.poll));
|
||||||
|
} else if (state is AppPasswordExchangeBlocAppPwSuccess) {
|
||||||
|
final newAccount = Account(
|
||||||
|
Account.newId(),
|
||||||
|
state.result.server.scheme,
|
||||||
|
state.result.server.authority,
|
||||||
|
state.result.loginName.toCi(),
|
||||||
|
state.result.loginName,
|
||||||
|
state.result.appPassword, []);
|
||||||
_checkWebDavUrl(context, newAccount);
|
_checkWebDavUrl(context, newAccount);
|
||||||
} else if (state is AppPasswordExchangeBlocFailure) {
|
} else if (state is AppPasswordExchangeBlocFailure) {
|
||||||
if (features.isSupportSelfSignedCert &&
|
if (features.isSupportSelfSignedCert &&
|
||||||
|
@ -185,8 +201,8 @@ class _ConnectState extends State<Connect> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _connect() {
|
void _initiateLogin() {
|
||||||
_bloc.add(AppPasswordExchangeBlocConnect(widget.account));
|
_bloc.add(AppPasswordExchangeBlocInitiateLogin(widget.uri));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onCheckWebDavUrlFailed(
|
Future<void> _onCheckWebDavUrlFailed(
|
||||||
|
|
|
@ -5,17 +5,14 @@ import 'package:kiwi/kiwi.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/app_localizations.dart';
|
import 'package:nc_photos/app_localizations.dart';
|
||||||
import 'package:nc_photos/ci_string.dart';
|
|
||||||
import 'package:nc_photos/di_container.dart';
|
import 'package:nc_photos/di_container.dart';
|
||||||
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
|
||||||
import 'package:nc_photos/help_utils.dart' as help_utils;
|
|
||||||
import 'package:nc_photos/iterable_extension.dart';
|
import 'package:nc_photos/iterable_extension.dart';
|
||||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||||
import 'package:nc_photos/pref.dart';
|
import 'package:nc_photos/pref.dart';
|
||||||
import 'package:nc_photos/pref_util.dart' as pref_util;
|
import 'package:nc_photos/pref_util.dart' as pref_util;
|
||||||
import 'package:nc_photos/string_extension.dart';
|
import 'package:nc_photos/string_extension.dart';
|
||||||
import 'package:nc_photos/theme.dart';
|
import 'package:nc_photos/theme.dart';
|
||||||
import 'package:nc_photos/url_launcher_util.dart';
|
|
||||||
import 'package:nc_photos/widget/connect.dart';
|
import 'package:nc_photos/widget/connect.dart';
|
||||||
import 'package:nc_photos/widget/home.dart';
|
import 'package:nc_photos/widget/home.dart';
|
||||||
import 'package:nc_photos/widget/root_picker.dart';
|
import 'package:nc_photos/widget/root_picker.dart';
|
||||||
|
@ -90,35 +87,6 @@ class _SignInState extends State<SignIn> {
|
||||||
child: _buildForm(context),
|
child: _buildForm(context),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(
|
|
||||||
alignment: AlignmentDirectional.centerStart,
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxWidth:
|
|
||||||
Theme.of(context).widthLimitedContentMaxWidth,
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () {
|
|
||||||
launch(help_utils.twoFactorAuthUrl);
|
|
||||||
},
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16, vertical: 16),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.help_outline, size: 16),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Expanded(
|
|
||||||
child:
|
|
||||||
Text(L10n.global().signIn2faHintText),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (!platform_k.isWeb) Expanded(child: Container()),
|
if (!platform_k.isWeb) Expanded(child: Container()),
|
||||||
Container(
|
Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
|
@ -223,55 +191,17 @@ class _SignInState extends State<SignIn> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
|
||||||
TextFormField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: L10n.global().usernameInputHint,
|
|
||||||
),
|
|
||||||
validator: (value) {
|
|
||||||
if (value!.trim().isEmpty) {
|
|
||||||
return L10n.global().usernameInputInvalidEmpty;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
onSaved: (value) {
|
|
||||||
_formValue.username = value!;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
TextFormField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: L10n.global().passwordInputHint,
|
|
||||||
),
|
|
||||||
obscureText: true,
|
|
||||||
validator: (value) {
|
|
||||||
if (value!.trim().isEmpty) {
|
|
||||||
return L10n.global().passwordInputInvalidEmpty;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
onSaved: (value) {
|
|
||||||
_formValue.password = value!;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _connect() async {
|
Future<void> _connect() async {
|
||||||
_formKey.currentState!.save();
|
_formKey.currentState!.save();
|
||||||
Account? account = Account(
|
Uri url = Uri.parse("${_formValue.scheme}://${_formValue.address}");
|
||||||
Account.newId(),
|
_log.info("[_connect] Try connecting with url: $url");
|
||||||
_formValue.scheme,
|
Account? account = await Navigator.pushNamed<Account>(
|
||||||
_formValue.address,
|
context, Connect.routeName,
|
||||||
_formValue.username.toCi(),
|
arguments: ConnectArguments(url));
|
||||||
_formValue.username,
|
|
||||||
_formValue.password,
|
|
||||||
[""],
|
|
||||||
);
|
|
||||||
_log.info("[_connect] Try connecting with account: $account");
|
|
||||||
account = await Navigator.pushNamed<Account>(context, Connect.routeName,
|
|
||||||
arguments: ConnectArguments(account));
|
|
||||||
if (account == null) {
|
if (account == null) {
|
||||||
// connection failed
|
// connection failed
|
||||||
return;
|
return;
|
||||||
|
@ -351,6 +281,4 @@ extension on _Scheme {
|
||||||
class _FormValue {
|
class _FormValue {
|
||||||
late String scheme;
|
late String scheme;
|
||||||
late String address;
|
late String address;
|
||||||
late String username;
|
|
||||||
late String password;
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue