[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:
Vishnu Mohandas 2024-03-07 13:24:30 +05:30 committed by GitHub
commit 90bbc54bb5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 318 additions and 178 deletions

View file

@ -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() {

View file

@ -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);
} }

View file

@ -0,0 +1,3 @@
import 'package:ente_auth/events/event.dart';
class EndpointUpdatedEvent extends Event {}

View file

@ -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;

View file

@ -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}"
} }

View file

@ -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(),
], ],
),
), ),
), ),
), ),

View file

@ -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) {

View file

@ -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();

View 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');
}
}
}

View 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();
}
}
}

View file

@ -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),

View file

@ -203,7 +203,7 @@
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastSwiftUpdateCheck = 0920; LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1300; LastUpgradeCheck = 1430;
ORGANIZATIONNAME = ""; ORGANIZATIONNAME = "";
TargetAttributes = { TargetAttributes = {
33CC10EC2044A3C60003C045 = { 33CC10EC2044A3C60003C045 = {

View file

@ -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"