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;
|
||||
}
|
||||
|
||||
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 {
|
||||
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);
|
||||
|
||||
ApiOcs ocs() => ApiOcs(this);
|
||||
|
||||
ApiSystemtags systemtags() => ApiSystemtags(this);
|
||||
|
||||
ApiSystemtagsRelations systemtagsRelations() => ApiSystemtagsRelations(this);
|
||||
|
||||
static String getAuthorizationHeaderValue(Account account) {
|
||||
final auth =
|
||||
base64.encode(utf8.encode("${account.username2}:${account.password}"));
|
||||
return "Basic $auth";
|
||||
return BasicAuth.fromAccount(account).toString();
|
||||
}
|
||||
|
||||
Future<Response> request(
|
||||
|
@ -52,10 +76,12 @@ class Api {
|
|||
bool isResponseString = true,
|
||||
}) async {
|
||||
final url = _makeUri(endpoint, queryParameters: queryParameters);
|
||||
final req = http.Request(method, url)
|
||||
..headers.addAll({
|
||||
"authorization": getAuthorizationHeaderValue(_account),
|
||||
final req = http.Request(method, url);
|
||||
if (_auth != null) {
|
||||
req.headers.addAll({
|
||||
"authorization": _auth.toString(),
|
||||
});
|
||||
}
|
||||
if (header != null) {
|
||||
// turn all to lower case, since HTTP headers are case-insensitive, this
|
||||
// smooths our processing (if any)
|
||||
|
@ -89,19 +115,16 @@ class Api {
|
|||
String endpoint, {
|
||||
Map<String, String>? queryParameters,
|
||||
}) {
|
||||
final splits = _account.address.split("/");
|
||||
final authority = splits[0];
|
||||
final path = splits.length > 1
|
||||
? splits.sublist(1).join("/") + "/$endpoint"
|
||||
: endpoint;
|
||||
if (_account.scheme == "http") {
|
||||
return Uri.http(authority, path, queryParameters);
|
||||
final path = _baseUrl.path + "/$endpoint";
|
||||
if (_baseUrl.scheme == "http") {
|
||||
return Uri.http(_baseUrl.authority, path, queryParameters);
|
||||
} 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");
|
||||
}
|
||||
|
@ -451,7 +474,9 @@ class ApiOcs {
|
|||
ApiOcs(this._api);
|
||||
|
||||
ApiOcsDav dav() => ApiOcsDav(this);
|
||||
|
||||
ApiOcsFacerecognition facerecognition() => ApiOcsFacerecognition(this);
|
||||
|
||||
ApiOcsFilesSharing filesSharing() => ApiOcsFilesSharing(this);
|
||||
|
||||
final Api _api;
|
||||
|
@ -499,6 +524,7 @@ class ApiOcsFacerecognition {
|
|||
ApiOcsFacerecognition(this._ocs);
|
||||
|
||||
ApiOcsFacerecognitionPersons persons() => ApiOcsFacerecognitionPersons(this);
|
||||
|
||||
ApiOcsFacerecognitionPerson person(String name) =>
|
||||
ApiOcsFacerecognitionPerson(this, name);
|
||||
|
||||
|
@ -571,8 +597,10 @@ class ApiOcsFilesSharing {
|
|||
ApiOcsFilesSharing(this._ocs);
|
||||
|
||||
ApiOcsFilesSharingShares shares() => ApiOcsFilesSharingShares(this);
|
||||
|
||||
ApiOcsFilesSharingShare share(String shareId) =>
|
||||
ApiOcsFilesSharingShare(this, shareId);
|
||||
|
||||
ApiOcsFilesSharingSharees sharees() => ApiOcsFilesSharingSharees(this);
|
||||
|
||||
final ApiOcs _ocs;
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
/// Helper functions working with remote Nextcloud server
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api.dart';
|
||||
|
@ -104,37 +107,130 @@ String getFacePreviewUrlRelative(
|
|||
return "index.php/apps/facerecognition/face/$faceId/thumb/$size";
|
||||
}
|
||||
|
||||
/// Query the app password for [account]
|
||||
Future<String> exchangePassword(Account account) async {
|
||||
final response = await Api(account).request(
|
||||
"GET",
|
||||
"ocs/v2.php/core/getapppassword",
|
||||
/// 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<InitiateLoginResponse> initiateLogin(Uri uri) async {
|
||||
final response = await Api.fromBaseUrl(uri).request(
|
||||
"POST",
|
||||
"index.php/login/v2",
|
||||
header: {
|
||||
"OCS-APIRequest": "true",
|
||||
"User-Agent": "nc-photos",
|
||||
},
|
||||
);
|
||||
if (response.isGood) {
|
||||
try {
|
||||
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;
|
||||
return InitiateLoginResponse.fromJsonString(response.body);
|
||||
} else {
|
||||
_log.severe(
|
||||
"[exchangePassword] Failed while requesting app password: $response");
|
||||
"[initiateLogin] Failed while requesting app password: $response");
|
||||
throw ApiException(
|
||||
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");
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:flutter/foundation.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/exception.dart';
|
||||
|
||||
|
@ -11,17 +11,58 @@ abstract class AppPasswordExchangeBlocEvent {
|
|||
const AppPasswordExchangeBlocEvent();
|
||||
}
|
||||
|
||||
class AppPasswordExchangeBlocConnect extends AppPasswordExchangeBlocEvent {
|
||||
const AppPasswordExchangeBlocConnect(this.account);
|
||||
class AppPasswordExchangeBlocInitiateLogin
|
||||
extends AppPasswordExchangeBlocEvent {
|
||||
const AppPasswordExchangeBlocInitiateLogin(this.uri);
|
||||
|
||||
@override
|
||||
toString() {
|
||||
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 {
|
||||
|
@ -32,17 +73,31 @@ class AppPasswordExchangeBlocInit extends AppPasswordExchangeBlocState {
|
|||
const AppPasswordExchangeBlocInit();
|
||||
}
|
||||
|
||||
class AppPasswordExchangeBlocSuccess extends AppPasswordExchangeBlocState {
|
||||
const AppPasswordExchangeBlocSuccess(this.password);
|
||||
class AppPasswordExchangeBlocInitiateLoginSuccess
|
||||
extends AppPasswordExchangeBlocState {
|
||||
const AppPasswordExchangeBlocInitiateLoginSuccess(this.result);
|
||||
|
||||
@override
|
||||
toString() {
|
||||
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 {
|
||||
|
@ -58,6 +113,20 @@ class AppPasswordExchangeBlocFailure extends AppPasswordExchangeBlocState {
|
|||
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
|
||||
extends Bloc<AppPasswordExchangeBlocEvent, AppPasswordExchangeBlocState> {
|
||||
AppPasswordExchangeBloc() : super(const AppPasswordExchangeBlocInit()) {
|
||||
|
@ -67,36 +136,96 @@ class AppPasswordExchangeBloc
|
|||
Future<void> _onEvent(AppPasswordExchangeBlocEvent event,
|
||||
Emitter<AppPasswordExchangeBlocState> emit) async {
|
||||
_log.info("[_onEvent] $event");
|
||||
if (event is AppPasswordExchangeBlocConnect) {
|
||||
await _onEventConnect(event, emit);
|
||||
if (event is AppPasswordExchangeBlocInitiateLogin) {
|
||||
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 {
|
||||
final account = ev.account;
|
||||
final uri = ev.uri;
|
||||
try {
|
||||
final appPwd = await api_util.exchangePassword(account);
|
||||
emit(AppPasswordExchangeBlocSuccess(appPwd));
|
||||
final initiateLoginResponse = await api_util.initiateLogin(uri);
|
||||
emit(AppPasswordExchangeBlocInitiateLoginSuccess(initiateLoginResponse));
|
||||
} on InvalidBaseUrlException catch (e) {
|
||||
_log.warning("[_exchangePassword] Invalid base url");
|
||||
_log.warning("[_onEventInitiateLogin] Invalid base url");
|
||||
emit(AppPasswordExchangeBlocFailure(e));
|
||||
} on HandshakeException catch (e) {
|
||||
_log.info("[_exchangePassword] Self-signed cert");
|
||||
_log.info("[_onEventInitiateLogin] Self-signed cert");
|
||||
emit(AppPasswordExchangeBlocFailure(e));
|
||||
} catch (e, stacktrace) {
|
||||
if (e is ApiException && e.response.statusCode == 401) {
|
||||
// wrong password, normal
|
||||
_log.warning(
|
||||
"[_exchangePassword] Server response 401, wrong password?");
|
||||
} else {
|
||||
_log.shout("[_exchangePassword] Failed while exchanging password", e,
|
||||
_log.shout("[_onEventInitiateLogin] Failed while exchanging password", e,
|
||||
stacktrace);
|
||||
}
|
||||
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 =
|
||||
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';
|
||||
|
||||
class ConnectArguments {
|
||||
ConnectArguments(this.account);
|
||||
ConnectArguments(this.uri);
|
||||
|
||||
final Account account;
|
||||
final Uri uri;
|
||||
}
|
||||
|
||||
class Connect extends StatefulWidget {
|
||||
|
@ -36,19 +36,19 @@ class Connect extends StatefulWidget {
|
|||
|
||||
const Connect({
|
||||
Key? key,
|
||||
required this.account,
|
||||
required this.uri,
|
||||
}) : super(key: key);
|
||||
|
||||
Connect.fromArgs(ConnectArguments args, {Key? key})
|
||||
: this(
|
||||
key: key,
|
||||
account: args.account,
|
||||
uri: args.uri,
|
||||
);
|
||||
|
||||
@override
|
||||
createState() => _ConnectState();
|
||||
|
||||
final Account account;
|
||||
final Uri uri;
|
||||
}
|
||||
|
||||
class _ConnectState extends State<Connect> {
|
||||
|
@ -58,6 +58,12 @@ class _ConnectState extends State<Connect> {
|
|||
_initBloc();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_bloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
@ -71,7 +77,7 @@ class _ConnectState extends State<Connect> {
|
|||
|
||||
void _initBloc() {
|
||||
_log.info("[_initBloc] Initialize bloc");
|
||||
_connect();
|
||||
_initiateLogin();
|
||||
}
|
||||
|
||||
Widget _buildContent(BuildContext context) {
|
||||
|
@ -88,7 +94,7 @@ class _ConnectState extends State<Connect> {
|
|||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
Text(
|
||||
L10n.global().connectingToServer(widget.account.url),
|
||||
L10n.global().connectingToServer(widget.uri),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
)
|
||||
|
@ -100,9 +106,19 @@ class _ConnectState extends State<Connect> {
|
|||
|
||||
void _onStateChange(
|
||||
BuildContext context, AppPasswordExchangeBlocState state) {
|
||||
if (state is AppPasswordExchangeBlocSuccess) {
|
||||
final newAccount = widget.account.copyWith(password: state.password);
|
||||
_log.info("[_onStateChange] Password exchanged: $newAccount");
|
||||
if (state is AppPasswordExchangeBlocInitiateLoginSuccess) {
|
||||
// launch the login url
|
||||
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);
|
||||
} else if (state is AppPasswordExchangeBlocFailure) {
|
||||
if (features.isSupportSelfSignedCert &&
|
||||
|
@ -185,8 +201,8 @@ class _ConnectState extends State<Connect> {
|
|||
});
|
||||
}
|
||||
|
||||
void _connect() {
|
||||
_bloc.add(AppPasswordExchangeBlocConnect(widget.account));
|
||||
void _initiateLogin() {
|
||||
_bloc.add(AppPasswordExchangeBlocInitiateLogin(widget.uri));
|
||||
}
|
||||
|
||||
Future<void> _onCheckWebDavUrlFailed(
|
||||
|
|
|
@ -5,17 +5,14 @@ import 'package:kiwi/kiwi.dart';
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:nc_photos/account.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/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/platform/k.dart' as platform_k;
|
||||
import 'package:nc_photos/pref.dart';
|
||||
import 'package:nc_photos/pref_util.dart' as pref_util;
|
||||
import 'package:nc_photos/string_extension.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/home.dart';
|
||||
import 'package:nc_photos/widget/root_picker.dart';
|
||||
|
@ -90,35 +87,6 @@ class _SignInState extends State<SignIn> {
|
|||
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()),
|
||||
Container(
|
||||
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 {
|
||||
_formKey.currentState!.save();
|
||||
Account? account = Account(
|
||||
Account.newId(),
|
||||
_formValue.scheme,
|
||||
_formValue.address,
|
||||
_formValue.username.toCi(),
|
||||
_formValue.username,
|
||||
_formValue.password,
|
||||
[""],
|
||||
);
|
||||
_log.info("[_connect] Try connecting with account: $account");
|
||||
account = await Navigator.pushNamed<Account>(context, Connect.routeName,
|
||||
arguments: ConnectArguments(account));
|
||||
Uri url = Uri.parse("${_formValue.scheme}://${_formValue.address}");
|
||||
_log.info("[_connect] Try connecting with url: $url");
|
||||
Account? account = await Navigator.pushNamed<Account>(
|
||||
context, Connect.routeName,
|
||||
arguments: ConnectArguments(url));
|
||||
if (account == null) {
|
||||
// connection failed
|
||||
return;
|
||||
|
@ -351,6 +281,4 @@ extension on _Scheme {
|
|||
class _FormValue {
|
||||
late String scheme;
|
||||
late String address;
|
||||
late String username;
|
||||
late String password;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue