Merge branch 'master' into bg_sync
This commit is contained in:
commit
3f9137481f
7 changed files with 485 additions and 79 deletions
|
@ -124,7 +124,7 @@ class Configuration {
|
|||
if (SyncService.instance.isSyncInProgress()) {
|
||||
SyncService.instance.stopSync();
|
||||
try {
|
||||
await SyncService.instance.existingSync();
|
||||
await SyncService.instance.existingSync().timeout(Duration(seconds: 5));
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
|
|
140
lib/models/sessions.dart
Normal file
140
lib/models/sessions.dart
Normal file
|
@ -0,0 +1,140 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class Sessions {
|
||||
final List<Session> sessions;
|
||||
|
||||
Sessions(
|
||||
this.sessions,
|
||||
);
|
||||
|
||||
Sessions copyWith({
|
||||
List<Session> sessions,
|
||||
}) {
|
||||
return Sessions(
|
||||
sessions ?? this.sessions,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'sessions': sessions?.map((x) => x.toMap())?.toList(),
|
||||
};
|
||||
}
|
||||
|
||||
factory Sessions.fromMap(Map<String, dynamic> map) {
|
||||
return Sessions(
|
||||
List<Session>.from(map['sessions']?.map((x) => Session.fromMap(x))),
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory Sessions.fromJson(String source) =>
|
||||
Sessions.fromMap(json.decode(source));
|
||||
|
||||
@override
|
||||
String toString() => 'Sessions(sessions: $sessions)';
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is Sessions && listEquals(other.sessions, sessions);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => sessions.hashCode;
|
||||
}
|
||||
|
||||
class Session {
|
||||
final String token;
|
||||
final int creationTime;
|
||||
final String ip;
|
||||
final String ua;
|
||||
final String prettyUA;
|
||||
final int lastUsedTime;
|
||||
|
||||
Session(
|
||||
this.token,
|
||||
this.creationTime,
|
||||
this.ip,
|
||||
this.ua,
|
||||
this.prettyUA,
|
||||
this.lastUsedTime,
|
||||
);
|
||||
|
||||
Session copyWith({
|
||||
String token,
|
||||
int creationTime,
|
||||
String ip,
|
||||
String ua,
|
||||
String prettyUA,
|
||||
int lastUsedTime,
|
||||
}) {
|
||||
return Session(
|
||||
token ?? this.token,
|
||||
creationTime ?? this.creationTime,
|
||||
ip ?? this.ip,
|
||||
ua ?? this.ua,
|
||||
prettyUA ?? this.prettyUA,
|
||||
lastUsedTime ?? this.lastUsedTime,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'token': token,
|
||||
'creationTime': creationTime,
|
||||
'ip': ip,
|
||||
'ua': ua,
|
||||
'prettyUA': prettyUA,
|
||||
'lastUsedTime': lastUsedTime,
|
||||
};
|
||||
}
|
||||
|
||||
factory Session.fromMap(Map<String, dynamic> map) {
|
||||
return Session(
|
||||
map['token'],
|
||||
map['creationTime'],
|
||||
map['ip'],
|
||||
map['ua'],
|
||||
map['prettyUA'],
|
||||
map['lastUsedTime'],
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory Session.fromJson(String source) =>
|
||||
Session.fromMap(json.decode(source));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Session(token: $token, creationTime: $creationTime, ip: $ip, ua: $ua, prettyUA: $prettyUA, lastUsedTime: $lastUsedTime)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is Session &&
|
||||
other.token == token &&
|
||||
other.creationTime == creationTime &&
|
||||
other.ip == ip &&
|
||||
other.ua == ua &&
|
||||
other.prettyUA == prettyUA &&
|
||||
other.lastUsedTime == lastUsedTime;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return token.hashCode ^
|
||||
creationTime.hashCode ^
|
||||
ip.hashCode ^
|
||||
ua.hashCode ^
|
||||
prettyUA.hashCode ^
|
||||
lastUsedTime.hashCode;
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ import 'package:photos/events/user_details_changed_event.dart';
|
|||
import 'package:photos/models/key_attributes.dart';
|
||||
import 'package:photos/models/key_gen_result.dart';
|
||||
import 'package:photos/models/public_key.dart';
|
||||
import 'package:photos/models/sessions.dart';
|
||||
import 'package:photos/models/set_keys_request.dart';
|
||||
import 'package:photos/models/set_recovery_key_request.dart';
|
||||
import 'package:photos/models/user_details.dart';
|
||||
|
@ -121,6 +122,40 @@ class UserService {
|
|||
}
|
||||
}
|
||||
|
||||
Future<Sessions> getActiveSessions() async {
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
_config.getHttpEndpoint() + "/users/sessions",
|
||||
options: Options(
|
||||
headers: {
|
||||
"X-Auth-Token": _config.getToken(),
|
||||
},
|
||||
),
|
||||
);
|
||||
return Sessions.fromMap(response.data);
|
||||
} on DioError catch (e) {
|
||||
_logger.info(e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> terminateSession(String token) async {
|
||||
try {
|
||||
await _dio.delete(_config.getHttpEndpoint() + "/users/session",
|
||||
options: Options(
|
||||
headers: {
|
||||
"X-Auth-Token": _config.getToken(),
|
||||
},
|
||||
),
|
||||
queryParameters: {
|
||||
"token": token,
|
||||
});
|
||||
} on DioError catch (e) {
|
||||
_logger.info(e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> logout(BuildContext context) async {
|
||||
final dialog = createProgressDialog(context, "logging out...");
|
||||
await dialog.show();
|
||||
|
|
|
@ -182,9 +182,12 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
|
|||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
file.getDisplayName(),
|
||||
Flexible(
|
||||
child: Text(
|
||||
file.getDisplayName(),
|
||||
),
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(8)),
|
||||
Icon(
|
||||
Icons.edit,
|
||||
color: Colors.white.withOpacity(0.85),
|
||||
|
|
199
lib/ui/sessions_page.dart
Normal file
199
lib/ui/sessions_page.dart
Normal file
|
@ -0,0 +1,199 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/models/sessions.dart';
|
||||
import 'package:photos/services/user_service.dart';
|
||||
import 'package:photos/ui/loading_widget.dart';
|
||||
import 'package:photos/utils/date_time_util.dart';
|
||||
import 'package:photos/utils/dialog_util.dart';
|
||||
|
||||
class SessionsPage extends StatefulWidget {
|
||||
SessionsPage({Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_SessionsPageState createState() => _SessionsPageState();
|
||||
}
|
||||
|
||||
class _SessionsPageState extends State<SessionsPage> {
|
||||
Sessions _sessions;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_fetchActiveSessions();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("active sessions"),
|
||||
),
|
||||
body: _getBody(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getBody() {
|
||||
if (_sessions == null) {
|
||||
return Center(child: loadWidget);
|
||||
}
|
||||
List<Widget> rows = [];
|
||||
for (final session in _sessions.sessions) {
|
||||
rows.add(_getSessionWidget(session));
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: rows,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getSessionWidget(Session session) {
|
||||
final lastUsedTime =
|
||||
DateTime.fromMicrosecondsSinceEpoch(session.lastUsedTime);
|
||||
return Column(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
_showSessionTerminationDialog(session);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_getUAWidget(session),
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
session.ip,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(8)),
|
||||
Flexible(
|
||||
child: Text(
|
||||
getFormattedTime(lastUsedTime),
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Divider(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _terminateSession(Session session) async {
|
||||
final dialog = createProgressDialog(context, "please wait...");
|
||||
await dialog.show();
|
||||
await UserService.instance.terminateSession(session.token);
|
||||
await _fetchActiveSessions();
|
||||
await dialog.hide();
|
||||
}
|
||||
|
||||
Future<void> _fetchActiveSessions() async {
|
||||
_sessions = await UserService.instance.getActiveSessions();
|
||||
_sessions.sessions.sort((first, second) {
|
||||
return second.lastUsedTime.compareTo(first.lastUsedTime);
|
||||
});
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _showSessionTerminationDialog(Session session) {
|
||||
final isLoggingOutFromThisDevice =
|
||||
session.token == Configuration.instance.getToken();
|
||||
Widget text;
|
||||
if (isLoggingOutFromThisDevice) {
|
||||
text = Text(
|
||||
"this will log you out of this device!",
|
||||
);
|
||||
} else {
|
||||
text = SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
"this will log you out of the following device:",
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(8)),
|
||||
Text(
|
||||
session.ua,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
AlertDialog alert = AlertDialog(
|
||||
title: Text("terminate session?"),
|
||||
content: text,
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text(
|
||||
"terminate",
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
if (isLoggingOutFromThisDevice) {
|
||||
await UserService.instance.logout(context);
|
||||
} else {
|
||||
_terminateSession(session);
|
||||
}
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
"cancel",
|
||||
style: TextStyle(
|
||||
color: isLoggingOutFromThisDevice
|
||||
? Theme.of(context).buttonColor
|
||||
: Colors.white,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return alert;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getUAWidget(Session session) {
|
||||
if (session.token == Configuration.instance.getToken()) {
|
||||
return Text(
|
||||
"this device",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).buttonColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Text(session.prettyUA);
|
||||
}
|
||||
}
|
|
@ -9,7 +9,9 @@ import 'package:photos/events/two_factor_status_change_event.dart';
|
|||
import 'package:photos/services/user_service.dart';
|
||||
import 'package:photos/ui/app_lock.dart';
|
||||
import 'package:photos/ui/loading_widget.dart';
|
||||
import 'package:photos/ui/sessions_page.dart';
|
||||
import 'package:photos/ui/settings/settings_section_title.dart';
|
||||
import 'package:photos/ui/settings/settings_text_item.dart';
|
||||
import 'package:photos/utils/auth_util.dart';
|
||||
import 'package:photos/utils/toast_util.dart';
|
||||
|
||||
|
@ -128,86 +130,113 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
|
|||
),
|
||||
]);
|
||||
if (Platform.isAndroid) {
|
||||
children.addAll([
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
Divider(height: 4),
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
SizedBox(
|
||||
height: 36,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("hide from recents"),
|
||||
Switch(
|
||||
value: _config.shouldHideFromRecents(),
|
||||
onChanged: (value) async {
|
||||
if (value) {
|
||||
AlertDialog alert = AlertDialog(
|
||||
title: Text("hide from recents?"),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
Text(
|
||||
"hiding from the task switcher will prevent you from taking screenshots in this app.",
|
||||
style: TextStyle(
|
||||
height: 1.5,
|
||||
children.addAll(
|
||||
[
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
Divider(height: 4),
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
SizedBox(
|
||||
height: 36,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("hide from recents"),
|
||||
Switch(
|
||||
value: _config.shouldHideFromRecents(),
|
||||
onChanged: (value) async {
|
||||
if (value) {
|
||||
AlertDialog alert = AlertDialog(
|
||||
title: Text("hide from recents?"),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
Text(
|
||||
"hiding from the task switcher will prevent you from taking screenshots in this app.",
|
||||
style: TextStyle(
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(padding: EdgeInsets.all(8)),
|
||||
Text(
|
||||
"are you sure?",
|
||||
style: TextStyle(
|
||||
height: 1.5,
|
||||
Padding(padding: EdgeInsets.all(8)),
|
||||
Text(
|
||||
"are you sure?",
|
||||
style: TextStyle(
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child:
|
||||
Text("no", style: TextStyle(color: Colors.white)),
|
||||
onPressed: () {
|
||||
Navigator.of(context, rootNavigator: true)
|
||||
.pop('dialog');
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text("yes",
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.8))),
|
||||
onPressed: () async {
|
||||
Navigator.of(context, rootNavigator: true)
|
||||
.pop('dialog');
|
||||
await _config.setShouldHideFromRecents(true);
|
||||
await FlutterWindowManager.addFlags(
|
||||
FlutterWindowManager.FLAG_SECURE);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text("no",
|
||||
style: TextStyle(color: Colors.white)),
|
||||
onPressed: () {
|
||||
Navigator.of(context, rootNavigator: true)
|
||||
.pop('dialog');
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text("yes",
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.8))),
|
||||
onPressed: () async {
|
||||
Navigator.of(context, rootNavigator: true)
|
||||
.pop('dialog');
|
||||
await _config.setShouldHideFromRecents(true);
|
||||
await FlutterWindowManager.addFlags(
|
||||
FlutterWindowManager.FLAG_SECURE);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return alert;
|
||||
},
|
||||
);
|
||||
} else {
|
||||
await _config.setShouldHideFromRecents(false);
|
||||
await FlutterWindowManager.clearFlags(
|
||||
FlutterWindowManager.FLAG_SECURE);
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return alert;
|
||||
},
|
||||
);
|
||||
} else {
|
||||
await _config.setShouldHideFromRecents(false);
|
||||
await FlutterWindowManager.clearFlags(
|
||||
FlutterWindowManager.FLAG_SECURE);
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
Padding(padding: EdgeInsets.all(4)),
|
||||
Divider(height: 4),
|
||||
Padding(padding: EdgeInsets.all(2)),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () async {
|
||||
AppLock.of(context).setEnabled(false);
|
||||
final result = await requestAuthentication();
|
||||
AppLock.of(context)
|
||||
.setEnabled(Configuration.instance.shouldShowLockScreen());
|
||||
if (!result) {
|
||||
showToast("please authenticate to view your active sessions");
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return SessionsPage();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: SettingsTextItem(
|
||||
text: "active sessions", icon: Icons.navigate_next),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
children: children,
|
||||
|
|
|
@ -11,7 +11,7 @@ description: ente photos application
|
|||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 0.3.48+258
|
||||
version: 0.4.1+261
|
||||
|
||||
environment:
|
||||
sdk: ">=2.10.0 <3.0.0"
|
||||
|
|
Loading…
Add table
Reference in a new issue