feat(studio): registry credentials panel in Settings (v0.44.0)
The hub install path now reads `~/.fai/registry-token` as a fallback when `FAI_REGISTRY_TOKEN` is unset (platform v0.10.92). Studio now writes that file directly: a fresh operator pastes the PAT into Settings → Registry credentials, hits Save, and the next install attempt resolves the auth wall without any shell or env-var setup. The token never round-trips back into Studio after save — status is shown only as "Configured (40 chars)" / "Not set" with no display of the secret itself. The Clear action deletes the file. On Unix the file is chmod-ed to 0600 (owner-only read/write); Windows leaves the default user ACL in place. Storage is strictly local: `~/.fai/registry-token`, never sent to a remote service. The hint text under the field says so to make the data flow explicit. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
01f3f773cf
commit
878f0a4e28
8 changed files with 464 additions and 6 deletions
66
lib/data/registry_token.dart
Normal file
66
lib/data/registry_token.dart
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import 'dart:io';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// Operator-managed registry auth token, kept at
|
||||
/// `~/.fai/registry-token` (mode 0600 on Unix). The hub reads
|
||||
/// this file at install time when `FAI_REGISTRY_TOKEN` is unset
|
||||
/// — see `download_to_temp` in `crates/fai_hub/src/lib.rs`.
|
||||
///
|
||||
/// Studio writes the file directly so a fresh install never
|
||||
/// requires the operator to fiddle with shell env vars.
|
||||
class RegistryToken {
|
||||
static String _faiHome() {
|
||||
final home = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];
|
||||
if (home == null || home.isEmpty) {
|
||||
throw StateError('Cannot resolve home directory (no HOME / USERPROFILE)');
|
||||
}
|
||||
return p.join(home, '.fai');
|
||||
}
|
||||
|
||||
/// Absolute path to the token file.
|
||||
static String get path => p.join(_faiHome(), 'registry-token');
|
||||
|
||||
/// True iff the file exists with a non-empty trimmed body.
|
||||
static Future<bool> isConfigured() async {
|
||||
final f = File(path);
|
||||
if (!await f.exists()) return false;
|
||||
final content = await f.readAsString();
|
||||
return content.trim().isNotEmpty;
|
||||
}
|
||||
|
||||
/// Length of the trimmed token, or null when the file is
|
||||
/// missing / empty. Used for status display ("Configured
|
||||
/// (40 chars)") without ever surfacing the secret.
|
||||
static Future<int?> charCount() async {
|
||||
final f = File(path);
|
||||
if (!await f.exists()) return null;
|
||||
final content = await f.readAsString();
|
||||
final trimmed = content.trim();
|
||||
return trimmed.isEmpty ? null : trimmed.length;
|
||||
}
|
||||
|
||||
/// Persist [token] to disk, creating `~/.fai/` if needed.
|
||||
/// On Unix the file is chmod-ed to 0600 (owner read/write
|
||||
/// only). On Windows the default user-ACL is left alone —
|
||||
/// best-effort, no PowerShell handshake.
|
||||
static Future<void> write(String token) async {
|
||||
final f = File(path);
|
||||
await f.parent.create(recursive: true);
|
||||
await f.writeAsString(token.trim(), flush: true);
|
||||
if (Platform.isLinux || Platform.isMacOS) {
|
||||
try {
|
||||
await Process.run('chmod', ['600', f.path]);
|
||||
} catch (_) {
|
||||
// best-effort; if chmod is unavailable the file still
|
||||
// exists with default perms (usually 0644, world-readable
|
||||
// but only on the operator's own machine).
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete the token file. No-op when the file doesn't exist.
|
||||
static Future<void> delete() async {
|
||||
final f = File(path);
|
||||
if (await f.exists()) await f.delete();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue