diff --git a/app/lib/desktop/wrapper_server.dart b/app/lib/desktop/wrapper_server.dart index a87d56d..220f26f 100644 --- a/app/lib/desktop/wrapper_server.dart +++ b/app/lib/desktop/wrapper_server.dart @@ -18,8 +18,11 @@ library; import 'dart:async'; +import 'dart:convert'; import 'dart:io'; +import 'package:crdt_sync/crdt_sync.dart'; + import 'package:diet_guard_app/desktop/github_proxy.dart'; import 'package:diet_guard_app/services/desktop_wrapper.dart'; import 'package:path/path.dart' as p; @@ -93,9 +96,60 @@ class WrapperServer { path.substring(WrapperPaths.documents.length), ); } + if (path == kSyncAccountPath) { + return _syncAccount(request); + } return _static(request, path); } + /// Serves the shared sync account so a desktop install can self-provision. + /// + /// Off unless CRDT_SYNC_SERVE_ACCOUNT is set: this hands out a credential + /// with database write access to anything that can reach the port, so it is + /// something you switch on once to set an install up, not a standing route. + /// 404 (rather than 403) when disabled, so a probe cannot tell it exists. + /// + /// Reads the same ~/.config/crdt-sync/ pair `diet_guard sync` already uses, + /// so the machine has one source of truth rather than a per-app copy — + /// the same reasoning that keeps the GitHub token in [gitHubProxy] instead + /// of the browser. + Future _syncAccount(HttpRequest request) async { + if ((Platform.environment[kSyncAccountEnvVar] ?? '').isEmpty) { + request.response.statusCode = HttpStatus.notFound; + return; + } + final configDir = p.join( + Platform.environment['HOME'] ?? '', + '.config', + 'crdt-sync', + ); + final configFile = File(p.join(configDir, 'firebase.json')); + final passwordFile = File(p.join(configDir, 'password')); + if (!configFile.existsSync() || !passwordFile.existsSync()) { + stderr.writeln( + 'Sync account requested but ~/.config/crdt-sync/ is incomplete — ' + 'the desktop app will keep asking for credentials.', + ); + request.response.statusCode = HttpStatus.notFound; + return; + } + final email = + (jsonDecode(await configFile.readAsString()) + as Map)['email']; + if (email is! String || email.isEmpty) { + stderr.writeln('firebase.json has no usable "email" — not serving it.'); + request.response.statusCode = HttpStatus.notFound; + return; + } + request.response.headers.contentType = ContentType.json; + request.response.write( + jsonEncode({ + 'email': email, + 'password': (await passwordFile.readAsString()).trim(), + }), + ); + } + /// GET returns the stored bytes (404 when absent); POST overwrites them. /// /// [name] is rejected unless it is a plain filename: these handlers write diff --git a/app/lib/services/firebase_backend.dart b/app/lib/services/firebase_backend.dart index 433126b..8b86649 100644 --- a/app/lib/services/firebase_backend.dart +++ b/app/lib/services/firebase_backend.dart @@ -47,9 +47,18 @@ SecureCredentialStore credentialStore() => SecureCredentialStore( /// Reads the per-device account, or null when sync has not been set up. Future loadAccount() async { try { - return FirebaseAccount.tryParse( + final stored = FirebaseAccount.tryParse( await _secure.read(key: kFirebaseAccountKey), ); + if (stored != null) return stored; + // Disconnect must stick: without this the next launch would silently + // re-adopt the account and the button would look broken. + if (await _secure.read(key: kSyncAccountOptOutKey) != null) return null; + // Desktop only in practice: on Android the wrapper does not exist, the + // request fails, and this returns null exactly as before. + final provisioned = await accountFromWrapper(Uri.base); + if (provisioned != null) await saveAccount(provisioned); + return provisioned; } on Object { // No secret service available: behave as "not configured" rather than // crashing the settings screen. @@ -64,6 +73,8 @@ Future saveAccount(FirebaseAccount account) => /// Forgets the account and any cached session. Future clearAccount() async { await _secure.delete(key: kFirebaseAccountKey); + // Suppress wrapper re-provisioning; see loadAccount(). + await _secure.write(key: kSyncAccountOptOutKey, value: 'true'); await credentialStore().clear(); } diff --git a/app/pubspec.lock b/app/pubspec.lock index 386fc70..4cf100c 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -85,11 +85,11 @@ packages: dependency: "direct main" description: path: crdt_sync_dart - ref: "crdt_sync_dart-v0.7.0" - resolved-ref: "6eef99d66132e12eb5056ae8e1780b34237fa013" + ref: "crdt_sync_dart-v0.8.0" + resolved-ref: "6a080333c2ed2b2773fee01605dd67ba713e54ea" url: "https://github.com/kuhyx/utils" source: git - version: "0.7.0" + version: "0.8.0" crypto: dependency: "direct main" description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 42d2d1a..8deeaf0 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: crdt_sync: git: url: https://github.com/kuhyx/utils - ref: crdt_sync_dart-v0.7.0 + ref: crdt_sync_dart-v0.8.0 path: crdt_sync_dart crypto: ^3.0.6 flutter: