[Auth] Allow for configuring a custom server (#726)
## Description Users can now tap on the onboarding screen **7 times** to bring up a page where they can configure the endpoint the app should be connecting to. ![self-host](https://github.com/ente-io/ente/assets/1161789/10f61f6d-0fb3-4f5b-889e-806ca7607525) ## Tests - [x] Verified that production flows are working as expected - [x] Verified that configuring the endpoint to a local instance lets you - [x] Connect to that instance - [x] Create an account - [x] Add a key - [x] Modify a key - [x] Logout and log back in
This commit is contained in:
commit
90bbc54bb5
|
@ -6,6 +6,7 @@ import 'dart:typed_data';
|
||||||
import 'package:bip39/bip39.dart' as bip39;
|
import 'package:bip39/bip39.dart' as bip39;
|
||||||
import 'package:ente_auth/core/constants.dart';
|
import 'package:ente_auth/core/constants.dart';
|
||||||
import 'package:ente_auth/core/event_bus.dart';
|
import 'package:ente_auth/core/event_bus.dart';
|
||||||
|
import 'package:ente_auth/events/endpoint_updated_event.dart';
|
||||||
import 'package:ente_auth/events/signed_in_event.dart';
|
import 'package:ente_auth/events/signed_in_event.dart';
|
||||||
import 'package:ente_auth/events/signed_out_event.dart';
|
import 'package:ente_auth/events/signed_out_event.dart';
|
||||||
import 'package:ente_auth/models/key_attributes.dart';
|
import 'package:ente_auth/models/key_attributes.dart';
|
||||||
|
@ -42,6 +43,7 @@ class Configuration {
|
||||||
static const userIDKey = "user_id";
|
static const userIDKey = "user_id";
|
||||||
static const hasMigratedSecureStorageKey = "has_migrated_secure_storage";
|
static const hasMigratedSecureStorageKey = "has_migrated_secure_storage";
|
||||||
static const hasOptedForOfflineModeKey = "has_opted_for_offline_mode";
|
static const hasOptedForOfflineModeKey = "has_opted_for_offline_mode";
|
||||||
|
static const endPointKey = "endpoint";
|
||||||
final List<String> onlineSecureKeys = [
|
final List<String> onlineSecureKeys = [
|
||||||
keyKey,
|
keyKey,
|
||||||
secretKeyKey,
|
secretKeyKey,
|
||||||
|
@ -317,7 +319,12 @@ class Configuration {
|
||||||
}
|
}
|
||||||
|
|
||||||
String getHttpEndpoint() {
|
String getHttpEndpoint() {
|
||||||
return endpoint;
|
return _preferences.getString(endPointKey) ?? endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setHttpEndpoint(String endpoint) async {
|
||||||
|
await _preferences.setString(endPointKey, endpoint);
|
||||||
|
Bus.instance.fire(EndpointUpdatedEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
String? getToken() {
|
String? getToken() {
|
||||||
|
|
|
@ -2,28 +2,24 @@ import 'dart:io';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:ente_auth/core/configuration.dart';
|
import 'package:ente_auth/core/configuration.dart';
|
||||||
import 'package:ente_auth/core/constants.dart';
|
import 'package:ente_auth/core/event_bus.dart';
|
||||||
|
import 'package:ente_auth/events/endpoint_updated_event.dart';
|
||||||
import 'package:fk_user_agent/fk_user_agent.dart';
|
import 'package:fk_user_agent/fk_user_agent.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
int kConnectTimeout = 15000;
|
int kConnectTimeout = 15000;
|
||||||
|
|
||||||
class Network {
|
class Network {
|
||||||
// apiEndpoint points to the Ente server's API endpoint
|
|
||||||
static const apiEndpoint = String.fromEnvironment(
|
|
||||||
"endpoint",
|
|
||||||
defaultValue: kDefaultProductionEndpoint,
|
|
||||||
);
|
|
||||||
late Dio _dio;
|
late Dio _dio;
|
||||||
late Dio _enteDio;
|
late Dio _enteDio;
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
await FkUserAgent.init();
|
await FkUserAgent.init();
|
||||||
final packageInfo = await PackageInfo.fromPlatform();
|
final packageInfo = await PackageInfo.fromPlatform();
|
||||||
final preferences = await SharedPreferences.getInstance();
|
final endpoint = Configuration.instance.getHttpEndpoint();
|
||||||
|
|
||||||
_dio = Dio(
|
_dio = Dio(
|
||||||
BaseOptions(
|
BaseOptions(
|
||||||
connectTimeout: kConnectTimeout,
|
connectTimeout: kConnectTimeout,
|
||||||
|
@ -34,10 +30,10 @@ class Network {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
_dio.interceptors.add(RequestIdInterceptor());
|
|
||||||
_enteDio = Dio(
|
_enteDio = Dio(
|
||||||
BaseOptions(
|
BaseOptions(
|
||||||
baseUrl: apiEndpoint,
|
baseUrl: endpoint,
|
||||||
connectTimeout: kConnectTimeout,
|
connectTimeout: kConnectTimeout,
|
||||||
headers: {
|
headers: {
|
||||||
HttpHeaders.userAgentHeader: FkUserAgent.userAgent,
|
HttpHeaders.userAgentHeader: FkUserAgent.userAgent,
|
||||||
|
@ -46,7 +42,13 @@ class Network {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
_enteDio.interceptors.add(EnteRequestInterceptor(preferences, apiEndpoint));
|
_setupInterceptors(endpoint);
|
||||||
|
|
||||||
|
Bus.instance.on<EndpointUpdatedEvent>().listen((event) {
|
||||||
|
final endpoint = Configuration.instance.getHttpEndpoint();
|
||||||
|
_enteDio.options.baseUrl = endpoint;
|
||||||
|
_setupInterceptors(endpoint);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Network._privateConstructor();
|
Network._privateConstructor();
|
||||||
|
@ -55,34 +57,41 @@ class Network {
|
||||||
|
|
||||||
Dio getDio() => _dio;
|
Dio getDio() => _dio;
|
||||||
Dio get enteDio => _enteDio;
|
Dio get enteDio => _enteDio;
|
||||||
|
|
||||||
|
void _setupInterceptors(String endpoint) {
|
||||||
|
_dio.interceptors.clear();
|
||||||
|
_dio.interceptors.add(RequestIdInterceptor());
|
||||||
|
|
||||||
|
_enteDio.interceptors.clear();
|
||||||
|
_enteDio.interceptors.add(EnteRequestInterceptor(endpoint));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RequestIdInterceptor extends Interceptor {
|
class RequestIdInterceptor extends Interceptor {
|
||||||
@override
|
@override
|
||||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||||
// ignore: prefer_const_constructors
|
options.headers
|
||||||
options.headers.putIfAbsent("x-request-id", () => Uuid().v4().toString());
|
.putIfAbsent("x-request-id", () => const Uuid().v4().toString());
|
||||||
return super.onRequest(options, handler);
|
return super.onRequest(options, handler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class EnteRequestInterceptor extends Interceptor {
|
class EnteRequestInterceptor extends Interceptor {
|
||||||
final SharedPreferences _preferences;
|
final String endpoint;
|
||||||
final String enteEndpoint;
|
|
||||||
|
|
||||||
EnteRequestInterceptor(this._preferences, this.enteEndpoint);
|
EnteRequestInterceptor(this.endpoint);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
assert(
|
assert(
|
||||||
options.baseUrl == enteEndpoint,
|
options.baseUrl == endpoint,
|
||||||
"interceptor should only be used for API endpoint",
|
"interceptor should only be used for API endpoint",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// ignore: prefer_const_constructors
|
options.headers
|
||||||
options.headers.putIfAbsent("x-request-id", () => Uuid().v4().toString());
|
.putIfAbsent("x-request-id", () => const Uuid().v4().toString());
|
||||||
final String? tokenValue = _preferences.getString(Configuration.tokenKey);
|
final String? tokenValue = Configuration.instance.getToken();
|
||||||
if (tokenValue != null) {
|
if (tokenValue != null) {
|
||||||
options.headers.putIfAbsent("X-Auth-Token", () => tokenValue);
|
options.headers.putIfAbsent("X-Auth-Token", () => tokenValue);
|
||||||
}
|
}
|
||||||
|
|
3
auth/lib/events/endpoint_updated_event.dart
Normal file
3
auth/lib/events/endpoint_updated_event.dart
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import 'package:ente_auth/events/event.dart';
|
||||||
|
|
||||||
|
class EndpointUpdatedEvent extends Event {}
|
|
@ -1,43 +1,29 @@
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:ente_auth/core/configuration.dart';
|
|
||||||
import 'package:ente_auth/core/errors.dart';
|
import 'package:ente_auth/core/errors.dart';
|
||||||
|
import 'package:ente_auth/core/network.dart';
|
||||||
import 'package:ente_auth/models/authenticator/auth_entity.dart';
|
import 'package:ente_auth/models/authenticator/auth_entity.dart';
|
||||||
import 'package:ente_auth/models/authenticator/auth_key.dart';
|
import 'package:ente_auth/models/authenticator/auth_key.dart';
|
||||||
|
|
||||||
class AuthenticatorGateway {
|
class AuthenticatorGateway {
|
||||||
final Dio _dio;
|
late Dio _enteDio;
|
||||||
final Configuration _config;
|
|
||||||
late String _basedEndpoint;
|
|
||||||
|
|
||||||
AuthenticatorGateway(this._dio, this._config) {
|
AuthenticatorGateway() {
|
||||||
_basedEndpoint = _config.getHttpEndpoint() + "/authenticator";
|
_enteDio = Network.instance.enteDio;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> createKey(String encKey, String header) async {
|
Future<void> createKey(String encKey, String header) async {
|
||||||
await _dio.post(
|
await _enteDio.post(
|
||||||
_basedEndpoint + "/key",
|
"/authenticator/key",
|
||||||
data: {
|
data: {
|
||||||
"encryptedKey": encKey,
|
"encryptedKey": encKey,
|
||||||
"header": header,
|
"header": header,
|
||||||
},
|
},
|
||||||
options: Options(
|
|
||||||
headers: {
|
|
||||||
"X-Auth-Token": _config.getToken(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AuthKey> getKey() async {
|
Future<AuthKey> getKey() async {
|
||||||
try {
|
try {
|
||||||
final response = await _dio.get(
|
final response = await _enteDio.get("/authenticator/key");
|
||||||
_basedEndpoint + "/key",
|
|
||||||
options: Options(
|
|
||||||
headers: {
|
|
||||||
"X-Auth-Token": _config.getToken(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return AuthKey.fromMap(response.data);
|
return AuthKey.fromMap(response.data);
|
||||||
} on DioError catch (e) {
|
} on DioError catch (e) {
|
||||||
if (e.response != null && (e.response!.statusCode ?? 0) == 404) {
|
if (e.response != null && (e.response!.statusCode ?? 0) == 404) {
|
||||||
|
@ -51,17 +37,12 @@ class AuthenticatorGateway {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AuthEntity> createEntity(String encryptedData, String header) async {
|
Future<AuthEntity> createEntity(String encryptedData, String header) async {
|
||||||
final response = await _dio.post(
|
final response = await _enteDio.post(
|
||||||
_basedEndpoint + "/entity",
|
"/authenticator/entity",
|
||||||
data: {
|
data: {
|
||||||
"encryptedData": encryptedData,
|
"encryptedData": encryptedData,
|
||||||
"header": header,
|
"header": header,
|
||||||
},
|
},
|
||||||
options: Options(
|
|
||||||
headers: {
|
|
||||||
"X-Auth-Token": _config.getToken(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return AuthEntity.fromMap(response.data);
|
return AuthEntity.fromMap(response.data);
|
||||||
}
|
}
|
||||||
|
@ -71,50 +52,35 @@ class AuthenticatorGateway {
|
||||||
String encryptedData,
|
String encryptedData,
|
||||||
String header,
|
String header,
|
||||||
) async {
|
) async {
|
||||||
await _dio.put(
|
await _enteDio.put(
|
||||||
_basedEndpoint + "/entity",
|
"/authenticator/entity",
|
||||||
data: {
|
data: {
|
||||||
"id": id,
|
"id": id,
|
||||||
"encryptedData": encryptedData,
|
"encryptedData": encryptedData,
|
||||||
"header": header,
|
"header": header,
|
||||||
},
|
},
|
||||||
options: Options(
|
|
||||||
headers: {
|
|
||||||
"X-Auth-Token": _config.getToken(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteEntity(
|
Future<void> deleteEntity(
|
||||||
String id,
|
String id,
|
||||||
) async {
|
) async {
|
||||||
await _dio.delete(
|
await _enteDio.delete(
|
||||||
_basedEndpoint + "/entity",
|
"/authenticator/entity",
|
||||||
queryParameters: {
|
queryParameters: {
|
||||||
"id": id,
|
"id": id,
|
||||||
},
|
},
|
||||||
options: Options(
|
|
||||||
headers: {
|
|
||||||
"X-Auth-Token": _config.getToken(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<AuthEntity>> getDiff(int sinceTime, {int limit = 500}) async {
|
Future<List<AuthEntity>> getDiff(int sinceTime, {int limit = 500}) async {
|
||||||
try {
|
try {
|
||||||
final response = await _dio.get(
|
final response = await _enteDio.get(
|
||||||
_basedEndpoint + "/entity/diff",
|
"/authenticator/entity/diff",
|
||||||
queryParameters: {
|
queryParameters: {
|
||||||
"sinceTime": sinceTime,
|
"sinceTime": sinceTime,
|
||||||
"limit": limit,
|
"limit": limit,
|
||||||
},
|
},
|
||||||
options: Options(
|
|
||||||
headers: {
|
|
||||||
"X-Auth-Token": _config.getToken(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
final List<AuthEntity> authEntities = <AuthEntity>[];
|
final List<AuthEntity> authEntities = <AuthEntity>[];
|
||||||
final diff = response.data["diff"] as List;
|
final diff = response.data["diff"] as List;
|
||||||
|
|
|
@ -408,5 +408,12 @@
|
||||||
"hearUsExplanation": "We don't track app installs. It'd help if you told us where you found us!",
|
"hearUsExplanation": "We don't track app installs. It'd help if you told us where you found us!",
|
||||||
"waitingForBrowserRequest": "Waiting for browser request...",
|
"waitingForBrowserRequest": "Waiting for browser request...",
|
||||||
"launchPasskeyUrlAgain": "Launch passkey URL again",
|
"launchPasskeyUrlAgain": "Launch passkey URL again",
|
||||||
"passkey": "Passkey"
|
"passkey": "Passkey",
|
||||||
|
"developerSettingsWarning":"Are you sure that you want to modify Developer settings?",
|
||||||
|
"developerSettings": "Developer settings",
|
||||||
|
"serverEndpoint": "Server endpoint",
|
||||||
|
"invalidEndpoint": "Invalid endpoint",
|
||||||
|
"invalidEndpointMessage": "Sorry, the endpoint you entered is invalid. Please enter a valid endpoint and try again.",
|
||||||
|
"endpointUpdatedMessage": "Endpoint updated successfully",
|
||||||
|
"customEndpoint": "Connected to {endpoint}"
|
||||||
}
|
}
|
|
@ -17,6 +17,8 @@ import 'package:ente_auth/ui/common/gradient_button.dart';
|
||||||
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||||
import 'package:ente_auth/ui/components/models/button_result.dart';
|
import 'package:ente_auth/ui/components/models/button_result.dart';
|
||||||
import 'package:ente_auth/ui/home_page.dart';
|
import 'package:ente_auth/ui/home_page.dart';
|
||||||
|
import 'package:ente_auth/ui/settings/developer_settings_page.dart';
|
||||||
|
import 'package:ente_auth/ui/settings/developer_settings_widget.dart';
|
||||||
import 'package:ente_auth/ui/settings/language_picker.dart';
|
import 'package:ente_auth/ui/settings/language_picker.dart';
|
||||||
import 'package:ente_auth/utils/dialog_util.dart';
|
import 'package:ente_auth/utils/dialog_util.dart';
|
||||||
import 'package:ente_auth/utils/navigation_util.dart';
|
import 'package:ente_auth/utils/navigation_util.dart';
|
||||||
|
@ -33,8 +35,12 @@ class OnboardingPage extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _OnboardingPageState extends State<OnboardingPage> {
|
class _OnboardingPageState extends State<OnboardingPage> {
|
||||||
|
static const kDeveloperModeTapCountThreshold = 7;
|
||||||
|
|
||||||
late StreamSubscription<TriggerLogoutEvent> _triggerLogoutEvent;
|
late StreamSubscription<TriggerLogoutEvent> _triggerLogoutEvent;
|
||||||
|
|
||||||
|
int _developerModeTapCount = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_triggerLogoutEvent =
|
_triggerLogoutEvent =
|
||||||
|
@ -56,114 +62,142 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||||
final l10n = context.l10n;
|
final l10n = context.l10n;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Center(
|
child: GestureDetector(
|
||||||
child: SingleChildScrollView(
|
onTap: () async {
|
||||||
child: Padding(
|
_developerModeTapCount++;
|
||||||
padding:
|
if (_developerModeTapCount >= kDeveloperModeTapCountThreshold) {
|
||||||
const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40),
|
_developerModeTapCount = 0;
|
||||||
child: Column(
|
final result = await showChoiceDialog(
|
||||||
children: [
|
context,
|
||||||
Column(
|
title: l10n.developerSettings,
|
||||||
children: [
|
firstButtonLabel: l10n.yes,
|
||||||
kDebugMode
|
body: l10n.developerSettingsWarning,
|
||||||
? GestureDetector(
|
isDismissible: false,
|
||||||
child: const Align(
|
);
|
||||||
alignment: Alignment.topRight,
|
if (result?.action == ButtonAction.first) {
|
||||||
child: Text("Lang"),
|
await Navigator.of(context).push(
|
||||||
),
|
MaterialPageRoute(
|
||||||
onTap: () async {
|
builder: (BuildContext context) {
|
||||||
final locale = await getLocale();
|
return const DeveloperSettingsPage();
|
||||||
routeToPage(
|
},
|
||||||
context,
|
),
|
||||||
LanguageSelectorPage(
|
);
|
||||||
appSupportedLocales,
|
setState(() {});
|
||||||
(locale) async {
|
}
|
||||||
await setLocale(locale);
|
}
|
||||||
App.setLocale(context, locale);
|
},
|
||||||
},
|
child: Center(
|
||||||
locale,
|
child: SingleChildScrollView(
|
||||||
|
child: Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
kDebugMode
|
||||||
|
? GestureDetector(
|
||||||
|
child: const Align(
|
||||||
|
alignment: Alignment.topRight,
|
||||||
|
child: Text("Lang"),
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
final locale = await getLocale();
|
||||||
|
routeToPage(
|
||||||
|
context,
|
||||||
|
LanguageSelectorPage(
|
||||||
|
appSupportedLocales,
|
||||||
|
(locale) async {
|
||||||
|
await setLocale(locale);
|
||||||
|
App.setLocale(context, locale);
|
||||||
|
},
|
||||||
|
locale,
|
||||||
|
),
|
||||||
|
).then((value) {
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: const SizedBox(),
|
||||||
|
Image.asset(
|
||||||
|
"assets/sheild-front-gradient.png",
|
||||||
|
width: 200,
|
||||||
|
height: 200,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text(
|
||||||
|
"ente",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontFamily: 'Montserrat',
|
||||||
|
fontSize: 42,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
"Authenticator",
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
Text(
|
||||||
|
l10n.onBoardingBody,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style:
|
||||||
|
Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||||
|
color: Colors.white38,
|
||||||
),
|
),
|
||||||
).then((value) {
|
|
||||||
setState(() {});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: const SizedBox(),
|
|
||||||
Image.asset(
|
|
||||||
"assets/sheild-front-gradient.png",
|
|
||||||
width: 200,
|
|
||||||
height: 200,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
const Text(
|
|
||||||
"ente",
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontFamily: 'Montserrat',
|
|
||||||
fontSize: 42,
|
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 100),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: GradientButton(
|
||||||
|
onTap: _navigateToSignUpPage,
|
||||||
|
text: l10n.newUser,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
),
|
||||||
Text(
|
const SizedBox(height: 4),
|
||||||
"Authenticator",
|
Container(
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
width: double.infinity,
|
||||||
),
|
padding: const EdgeInsets.fromLTRB(20, 12, 20, 0),
|
||||||
const SizedBox(height: 32),
|
child: Hero(
|
||||||
Text(
|
tag: "log_in",
|
||||||
l10n.onBoardingBody,
|
child: ElevatedButton(
|
||||||
textAlign: TextAlign.center,
|
style: Theme.of(context)
|
||||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
.colorScheme
|
||||||
color: Colors.white38,
|
.optionalActionButtonStyle,
|
||||||
|
onPressed: _navigateToSignInPage,
|
||||||
|
child: Text(
|
||||||
|
l10n.existingUser,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.black, // same for both themes
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 100),
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
||||||
child: GradientButton(
|
|
||||||
onTap: _navigateToSignUpPage,
|
|
||||||
text: l10n.newUser,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 0),
|
|
||||||
child: Hero(
|
|
||||||
tag: "log_in",
|
|
||||||
child: ElevatedButton(
|
|
||||||
style: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.optionalActionButtonStyle,
|
|
||||||
onPressed: _navigateToSignInPage,
|
|
||||||
child: Text(
|
|
||||||
l10n.existingUser,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.black, // same for both themes
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 4),
|
||||||
const SizedBox(height: 4),
|
Container(
|
||||||
Container(
|
width: double.infinity,
|
||||||
width: double.infinity,
|
padding: const EdgeInsets.only(top: 20, bottom: 20),
|
||||||
padding: const EdgeInsets.only(top: 20, bottom: 20),
|
child: GestureDetector(
|
||||||
child: GestureDetector(
|
onTap: _optForOfflineMode,
|
||||||
onTap: _optForOfflineMode,
|
child: Center(
|
||||||
child: Center(
|
child: Text(
|
||||||
child: Text(
|
l10n.useOffline,
|
||||||
l10n.useOffline,
|
style: body.copyWith(
|
||||||
style: body.copyWith(
|
color:
|
||||||
color: Theme.of(context).colorScheme.mutedTextColor,
|
Theme.of(context).colorScheme.mutedTextColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const DeveloperSettingsWidget(),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -5,7 +5,6 @@ import 'dart:math';
|
||||||
import 'package:ente_auth/core/configuration.dart';
|
import 'package:ente_auth/core/configuration.dart';
|
||||||
import 'package:ente_auth/core/errors.dart';
|
import 'package:ente_auth/core/errors.dart';
|
||||||
import 'package:ente_auth/core/event_bus.dart';
|
import 'package:ente_auth/core/event_bus.dart';
|
||||||
import 'package:ente_auth/core/network.dart';
|
|
||||||
import 'package:ente_auth/events/codes_updated_event.dart';
|
import 'package:ente_auth/events/codes_updated_event.dart';
|
||||||
import 'package:ente_auth/events/signed_in_event.dart';
|
import 'package:ente_auth/events/signed_in_event.dart';
|
||||||
import 'package:ente_auth/events/trigger_logout_event.dart';
|
import 'package:ente_auth/events/trigger_logout_event.dart';
|
||||||
|
@ -26,6 +25,7 @@ enum AccountMode {
|
||||||
online,
|
online,
|
||||||
offline,
|
offline,
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on AccountMode {
|
extension on AccountMode {
|
||||||
bool get isOnline => this == AccountMode.online;
|
bool get isOnline => this == AccountMode.online;
|
||||||
bool get isOffline => this == AccountMode.offline;
|
bool get isOffline => this == AccountMode.offline;
|
||||||
|
@ -56,7 +56,7 @@ class AuthenticatorService {
|
||||||
_prefs = await SharedPreferences.getInstance();
|
_prefs = await SharedPreferences.getInstance();
|
||||||
_db = AuthenticatorDB.instance;
|
_db = AuthenticatorDB.instance;
|
||||||
_offlineDb = OfflineAuthenticatorDB.instance;
|
_offlineDb = OfflineAuthenticatorDB.instance;
|
||||||
_gateway = AuthenticatorGateway(Network.instance.getDio(), _config);
|
_gateway = AuthenticatorGateway();
|
||||||
if (Configuration.instance.hasConfiguredAccount()) {
|
if (Configuration.instance.hasConfiguredAccount()) {
|
||||||
unawaited(onlineSync());
|
unawaited(onlineSync());
|
||||||
}
|
}
|
||||||
|
@ -154,7 +154,7 @@ class AuthenticatorService {
|
||||||
} else {
|
} else {
|
||||||
debugPrint("Skipping delete since account mode is offline");
|
debugPrint("Skipping delete since account mode is offline");
|
||||||
}
|
}
|
||||||
if(accountMode.isOnline) {
|
if (accountMode.isOnline) {
|
||||||
await _db.deleteByIDs(generatedIDs: [genID]);
|
await _db.deleteByIDs(generatedIDs: [genID]);
|
||||||
} else {
|
} else {
|
||||||
await _offlineDb.deleteByIDs(generatedIDs: [genID]);
|
await _offlineDb.deleteByIDs(generatedIDs: [genID]);
|
||||||
|
@ -163,7 +163,7 @@ class AuthenticatorService {
|
||||||
|
|
||||||
Future<bool> onlineSync() async {
|
Future<bool> onlineSync() async {
|
||||||
try {
|
try {
|
||||||
if(getAccountMode().isOffline) {
|
if (getAccountMode().isOffline) {
|
||||||
debugPrint("Skipping sync since account mode is offline");
|
debugPrint("Skipping sync since account mode is offline");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -253,7 +253,7 @@ class AuthenticatorService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uint8List> getOrCreateAuthDataKey(AccountMode mode) async {
|
Future<Uint8List> getOrCreateAuthDataKey(AccountMode mode) async {
|
||||||
if(mode.isOffline) {
|
if (mode.isOffline) {
|
||||||
return _config.getOfflineSecretKey()!;
|
return _config.getOfflineSecretKey()!;
|
||||||
}
|
}
|
||||||
if (_config.getAuthSecretKey() != null) {
|
if (_config.getAuthSecretKey() != null) {
|
||||||
|
|
|
@ -7,10 +7,6 @@ class UserStore {
|
||||||
late SharedPreferences _preferences;
|
late SharedPreferences _preferences;
|
||||||
|
|
||||||
static final UserStore instance = UserStore._privateConstructor();
|
static final UserStore instance = UserStore._privateConstructor();
|
||||||
static const endpoint = String.fromEnvironment(
|
|
||||||
"endpoint",
|
|
||||||
defaultValue: "https://api.ente.io",
|
|
||||||
);
|
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
_preferences = await SharedPreferences.getInstance();
|
_preferences = await SharedPreferences.getInstance();
|
||||||
|
|
89
auth/lib/ui/settings/developer_settings_page.dart
Normal file
89
auth/lib/ui/settings/developer_settings_page.dart
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:ente_auth/core/configuration.dart';
|
||||||
|
import 'package:ente_auth/l10n/l10n.dart';
|
||||||
|
import 'package:ente_auth/ui/common/gradient_button.dart';
|
||||||
|
import 'package:ente_auth/utils/dialog_util.dart';
|
||||||
|
import 'package:ente_auth/utils/toast_util.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
class DeveloperSettingsPage extends StatefulWidget {
|
||||||
|
const DeveloperSettingsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
_DeveloperSettingsPageState createState() => _DeveloperSettingsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DeveloperSettingsPageState extends State<DeveloperSettingsPage> {
|
||||||
|
final _logger = Logger('DeveloperSettingsPage');
|
||||||
|
final _urlController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_urlController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
_logger.info(
|
||||||
|
"Current endpoint is: " + Configuration.instance.getHttpEndpoint(),
|
||||||
|
);
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(context.l10n.developerSettings),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _urlController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: context.l10n.serverEndpoint,
|
||||||
|
hintText: Configuration.instance.getHttpEndpoint(),
|
||||||
|
),
|
||||||
|
autofocus: true,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
GradientButton(
|
||||||
|
onTap: () async {
|
||||||
|
String url = _urlController.text;
|
||||||
|
_logger.info("Entered endpoint: " + url);
|
||||||
|
try {
|
||||||
|
final uri = Uri.parse(url);
|
||||||
|
if ((uri.scheme == "http" || uri.scheme == "https")) {
|
||||||
|
await _ping(url);
|
||||||
|
await Configuration.instance.setHttpEndpoint(url);
|
||||||
|
showToast(context, context.l10n.endpointUpdatedMessage);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
} else {
|
||||||
|
throw const FormatException();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showErrorDialog(
|
||||||
|
context,
|
||||||
|
context.l10n.invalidEndpoint,
|
||||||
|
context.l10n.invalidEndpointMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text: context.l10n.saveAction,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _ping(String endpoint) async {
|
||||||
|
try {
|
||||||
|
final response = await Dio().get(endpoint + '/ping');
|
||||||
|
if (response.data['message'] != 'pong') {
|
||||||
|
throw Exception('Invalid response');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Error occurred: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
auth/lib/ui/settings/developer_settings_widget.dart
Normal file
27
auth/lib/ui/settings/developer_settings_widget.dart
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import 'package:ente_auth/core/configuration.dart';
|
||||||
|
import 'package:ente_auth/core/constants.dart';
|
||||||
|
import 'package:ente_auth/l10n/l10n.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class DeveloperSettingsWidget extends StatelessWidget {
|
||||||
|
const DeveloperSettingsWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final endpoint = Configuration.instance.getHttpEndpoint();
|
||||||
|
if (endpoint != kDefaultProductionEndpoint) {
|
||||||
|
final endpointURI = Uri.parse(endpoint);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 20),
|
||||||
|
child: Text(
|
||||||
|
context.l10n.customEndpoint(
|
||||||
|
endpointURI.host + ":" + endpointURI.port.toString(),
|
||||||
|
),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ import 'package:ente_auth/ui/settings/account_section_widget.dart';
|
||||||
import 'package:ente_auth/ui/settings/app_version_widget.dart';
|
import 'package:ente_auth/ui/settings/app_version_widget.dart';
|
||||||
import 'package:ente_auth/ui/settings/data/data_section_widget.dart';
|
import 'package:ente_auth/ui/settings/data/data_section_widget.dart';
|
||||||
import 'package:ente_auth/ui/settings/data/export_widget.dart';
|
import 'package:ente_auth/ui/settings/data/export_widget.dart';
|
||||||
|
import 'package:ente_auth/ui/settings/developer_settings_widget.dart';
|
||||||
import 'package:ente_auth/ui/settings/general_section_widget.dart';
|
import 'package:ente_auth/ui/settings/general_section_widget.dart';
|
||||||
import 'package:ente_auth/ui/settings/security_section_widget.dart';
|
import 'package:ente_auth/ui/settings/security_section_widget.dart';
|
||||||
import 'package:ente_auth/ui/settings/social_section_widget.dart';
|
import 'package:ente_auth/ui/settings/social_section_widget.dart';
|
||||||
|
@ -149,6 +150,7 @@ class SettingsPage extends StatelessWidget {
|
||||||
sectionSpacing,
|
sectionSpacing,
|
||||||
const AboutSectionWidget(),
|
const AboutSectionWidget(),
|
||||||
const AppVersionWidget(),
|
const AppVersionWidget(),
|
||||||
|
const DeveloperSettingsWidget(),
|
||||||
const SupportDevWidget(),
|
const SupportDevWidget(),
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.only(bottom: 60),
|
padding: EdgeInsets.only(bottom: 60),
|
||||||
|
|
|
@ -203,7 +203,7 @@
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
LastSwiftUpdateCheck = 0920;
|
LastSwiftUpdateCheck = 0920;
|
||||||
LastUpgradeCheck = 1300;
|
LastUpgradeCheck = 1430;
|
||||||
ORGANIZATIONNAME = "";
|
ORGANIZATIONNAME = "";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
33CC10EC2044A3C60003C045 = {
|
33CC10EC2044A3C60003C045 = {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1300"
|
LastUpgradeVersion = "1430"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|
Loading…
Reference in a new issue