mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 20:47:12 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file-length / file length <= 250 lines (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (push) Waiting to run
Tests / test (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Also bumps Flutter 3.47.3, flutter_secure_storage 11.1.0, idb_shim 2.9.9, mcp 2.2.0 and ruff 0.16.7 so the freshness gate lets this through. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwtowrowDXx6ccmWVPkCaF
137 lines
5.2 KiB
Dart
137 lines
5.2 KiB
Dart
/// Locally-stored GitHub sync configuration, ported from `~/src/todo`'s
|
|
/// `sync/sync_settings.dart`, including the OAuth device-flow fields: the
|
|
/// "Connect GitHub" button is the primary path, with a pasted PAT kept as a
|
|
/// manual fallback.
|
|
library;
|
|
|
|
import 'package:diet_guard_app/services/token_vault.dart';
|
|
import 'package:diet_guard_app/services/token_vault_factory.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
/// The GitHub token is kept out of `SharedPreferences`, which holds only the
|
|
/// non-secret owner/repo: on Android it lives in the OS keystore, and in the
|
|
/// desktop web build it lives in the wrapper process rather than the browser
|
|
/// at all (see [TokenVault]). Older builds stored the token in plaintext
|
|
/// prefs; [load]/[save] migrate it transparently and never drop the plaintext
|
|
/// copy until a vault write is confirmed (so we degrade to -- never below --
|
|
/// the old behaviour when no secure store is available).
|
|
class SyncSettings {
|
|
/// Creates a [SyncSettings] from its owner/repo/token.
|
|
const SyncSettings({
|
|
required this.owner,
|
|
required this.repo,
|
|
required this.token,
|
|
this.clientId = '',
|
|
});
|
|
|
|
/// The repo owner/org (e.g. `"kuhyx"`).
|
|
final String owner;
|
|
|
|
/// The repo name (e.g. `"syncs"`).
|
|
final String repo;
|
|
|
|
/// A GitHub PAT with contents read/write on [owner]/[repo].
|
|
final String token;
|
|
|
|
/// GitHub OAuth App client id used by the device-flow "Connect" button.
|
|
/// Not a secret (device flow needs no client secret), so it is safe to
|
|
/// ship as a compile-time default and commit to source — see
|
|
/// [defaultClientId].
|
|
final String clientId;
|
|
|
|
/// The app's own GitHub OAuth App (device-flow enabled) client id, baked
|
|
/// in so "Connect GitHub" works with zero setup. Registered 2026-06-23 at
|
|
/// github.com/settings/developers, device flow enabled — distinct from
|
|
/// the sibling notes app's OAuth App, which belongs to a different
|
|
/// product.
|
|
static const defaultClientId = 'Ov23li8wIQBai3qtbsqa';
|
|
|
|
/// True when enough is set to attempt a sync.
|
|
bool get isConfigured =>
|
|
owner.isNotEmpty && repo.isNotEmpty && token.isNotEmpty;
|
|
|
|
/// True when device-flow "Connect GitHub" can be offered.
|
|
bool get canUseDeviceFlow => clientId.isNotEmpty;
|
|
|
|
static const _kOwner = 'sync.owner';
|
|
static const _kRepo = 'sync.repo';
|
|
static const _kClientId = 'sync.clientId';
|
|
// Legacy plaintext location for the token; read-only now and removed once
|
|
// the token has been migrated into secure storage.
|
|
static const _kToken = 'sync.token';
|
|
|
|
static final TokenVault _vault = openTokenVault();
|
|
|
|
/// True when this platform can show the stored token back to the user.
|
|
static bool get exposesTokenValue => _vault.exposesTokenValue;
|
|
|
|
/// Loads settings, defaulting the owner/repo to `kuhyx/syncs`
|
|
/// (matching the PC's `SYNC_REPO_OWNER`/`SYNC_REPO_NAME` constants) and the
|
|
/// client id to the baked-in [defaultClientId], so a fresh install needs
|
|
/// only "Connect GitHub" (once an OAuth App is registered) or a pasted PAT.
|
|
///
|
|
/// A device that already persisted the old standalone `diet-guard-sync`
|
|
/// repo name (before it was folded into the shared `syncs` monorepo) is
|
|
/// migrated to `syncs` here and the corrected value is written straight
|
|
/// back to prefs, so this only ever runs once per device.
|
|
static Future<SyncSettings> load() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
var repo = prefs.getString(_kRepo) ?? 'syncs';
|
|
if (repo == 'diet-guard-sync') {
|
|
repo = 'syncs';
|
|
await prefs.setString(_kRepo, repo);
|
|
}
|
|
return SyncSettings(
|
|
owner: prefs.getString(_kOwner) ?? 'kuhyx',
|
|
repo: repo,
|
|
token: await _loadToken(prefs),
|
|
clientId: prefs.getString(_kClientId) ?? defaultClientId,
|
|
);
|
|
}
|
|
|
|
/// Reads the token, preferring the vault and falling back to the legacy
|
|
/// plaintext value. A legacy value is migrated into the vault on read, but
|
|
/// only dropped from prefs once that write succeeds.
|
|
static Future<String> _loadToken(SharedPreferences prefs) async {
|
|
final stored = await _vault.read();
|
|
if (stored.isNotEmpty) return stored;
|
|
|
|
final legacy = prefs.getString(_kToken) ?? '';
|
|
if (legacy.isNotEmpty && await _vault.write(legacy)) {
|
|
await prefs.remove(_kToken);
|
|
}
|
|
return legacy;
|
|
}
|
|
|
|
/// Persists [owner]/[repo] to prefs and [token] to the vault (or plaintext
|
|
/// prefs as a fallback -- see [_loadToken]).
|
|
Future<void> save() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString(_kOwner, owner);
|
|
await prefs.setString(_kRepo, repo);
|
|
await prefs.setString(_kClientId, clientId);
|
|
// Confirm-before-delete: only remove the plaintext copy once the vault
|
|
// write succeeds; otherwise keep persisting it to prefs as before.
|
|
if (await _vault.write(token)) {
|
|
await prefs.remove(_kToken);
|
|
} else {
|
|
await prefs.setString(_kToken, token);
|
|
}
|
|
}
|
|
|
|
/// Returns a copy of this with only the given fields replaced.
|
|
SyncSettings copyWith({
|
|
String? owner,
|
|
String? repo,
|
|
String? token,
|
|
String? clientId,
|
|
}) {
|
|
return SyncSettings(
|
|
owner: owner ?? this.owner,
|
|
repo: repo ?? this.repo,
|
|
token: token ?? this.token,
|
|
clientId: clientId ?? this.clientId,
|
|
);
|
|
}
|
|
}
|