mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-10 19:07:02 +01:00
Let the desktop provision its sync account from the wrapper
The desktop app is a Flutter web build in a Chrome --app window, so
FlutterSecureStorage resolves to the web backend: AES-encrypted localStorage
keyed inside the browser profile. That left the sync account settable only by
typing it into the settings screen — nothing outside the page can write that
store (verified: `secret-tool` targets libsecret, which only a Linux build
would read, and a Dart CLI cannot even import flutter_secure_storage since it
is a Flutter plugin that fails to resolve on the VM).
So the account now arrives the same way the GitHub token already does: from
the wrapper, which owns the filesystem and serves the page. loadAccount()
falls back to GET /sync-account when the store is empty and persists the
result, so the route is consulted once per install rather than being
load-bearing forever.
The route reads ~/.config/crdt-sync/{firebase.json,password} — the same pair
the Python daemons already use — so there is one source of truth per machine
instead of a per-app copy.
Off unless CRDT_SYNC_SERVE_ACCOUNT is set, because it hands a credential with
database write access to anything that can reach the port. Verified both
ways against the running wrapper: 404 without the env var, 200 with it, and
the app serves normally either way. It answers 404 rather than 403 when
disabled, so a probe cannot tell the route exists.
clearAccount() now writes an opt-out marker that the fallback checks —
without it, Disconnect would delete the account and the next launch would
silently re-adopt it, making the button look broken.
252 tests, analyzer clean. Pinned to crdt_sync_dart-v0.8.0 for the shared
path/env-var constants and accountFromWrapper().
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177xwzcpfpWG2qca8Mr39q9
This commit is contained in:
parent
91c7691fbf
commit
3c6c071513
@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:crdt_sync/crdt_sync.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// Local HTTP server backing the desktop app.
|
||||
@ -79,9 +80,56 @@ class WrapperServer {
|
||||
if (path == '/backup/log') {
|
||||
return _file(request, logPath);
|
||||
}
|
||||
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 the route
|
||||
/// exists.
|
||||
///
|
||||
/// The account is read from the same ~/.config/crdt-sync/ pair the Python
|
||||
/// daemons already use, so there is one source of truth per machine rather
|
||||
/// than a per-app copy.
|
||||
Future<void> _syncAccount(HttpRequest request) async {
|
||||
if ((Platform.environment[kSyncAccountEnvVar] ?? '').isEmpty) {
|
||||
request.response.statusCode = HttpStatus.notFound;
|
||||
return;
|
||||
}
|
||||
final home = Platform.environment['HOME'] ?? '';
|
||||
final configDir = p.join(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<String, dynamic>)['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 file's contents (404 when absent); POST overwrites it.
|
||||
Future<void> _file(HttpRequest request, String filePath) async {
|
||||
final file = File(filePath);
|
||||
|
||||
@ -45,11 +45,24 @@ SecureCredentialStore credentialStore() => SecureCredentialStore(
|
||||
);
|
||||
|
||||
/// Reads the per-device account, or null when sync has not been set up.
|
||||
///
|
||||
/// Falls back to the local wrapper when the store is empty, so a desktop
|
||||
/// install can provision itself from `~/.config/crdt-sync/` instead of the
|
||||
/// account being retyped per machine. The fetched account is written to the
|
||||
/// store on first success, so the route is consulted once rather than being
|
||||
/// load-bearing forever.
|
||||
Future<FirebaseAccount?> 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;
|
||||
final provisioned = await accountFromWrapper(Uri.base);
|
||||
if (provisioned != null) await saveAccount(provisioned);
|
||||
return provisioned;
|
||||
} on Exception {
|
||||
// No secret service available: behave as "not configured" rather than
|
||||
// crashing the settings screen.
|
||||
@ -64,6 +77,8 @@ Future<void> saveAccount(FirebaseAccount account) =>
|
||||
/// Forgets the account and any cached session.
|
||||
Future<void> clearAccount() async {
|
||||
await _secure.delete(key: kFirebaseAccountKey);
|
||||
// Suppress wrapper re-provisioning; see loadAccount().
|
||||
await _secure.write(key: kSyncAccountOptOutKey, value: 'true');
|
||||
await credentialStore().clear();
|
||||
}
|
||||
|
||||
|
||||
@ -101,11 +101,11 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: crdt_sync_dart
|
||||
ref: "crdt_sync_dart-v0.6.0"
|
||||
resolved-ref: fc99eff00599a4c3195e4c1573ba08f15909a10d
|
||||
ref: "crdt_sync_dart-v0.8.0"
|
||||
resolved-ref: "6a080333c2ed2b2773fee01605dd67ba713e54ea"
|
||||
url: "https://github.com/kuhyx/utils"
|
||||
source: git
|
||||
version: "0.6.0"
|
||||
version: "0.8.0"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@ -33,7 +33,7 @@ dependencies:
|
||||
crdt_sync:
|
||||
git:
|
||||
url: https://github.com/kuhyx/utils
|
||||
ref: crdt_sync_dart-v0.6.0
|
||||
ref: crdt_sync_dart-v0.8.0
|
||||
path: crdt_sync_dart
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user