SecretGate.passIf: "비밀 값이 있으면 문을 지나야 함" 을 한 곳에서 (관리자 10/24)

- SecretGate.passIf(hasSecret, force): 비밀 값이 없으면 묻지 않고 지나감, 있으면 pass 의 답
- 모은 곳 (같은 꼴의 복사 9곳): 원격 등록 requireSecretGate · 플레이어 (WebDAV 머리) · 그림 AI (API 키) ·
  WebDAV · FTP · SFTP · SMB 설정 · AI 서비스 설정 (저장된 비밀 값이 있는 것을 고치기 전). 오류 종류 · 닫기는 그대로
- 그대로 둔 것 (같은 꼴 아님): 사용자가 직접 누른 일 앞에서 다시 묻는 8곳 (탐색기 · 폴더 고르기 · 다시 시도 · 보관함 · 자막 찾기 등),
  OpenSubtitles (비밀 값 조건 없이 늘 문 - 따로 보고)
- 약속 시험 한 줄 (remote_registry_contract: 묻는지 · 답 · force)

사용자가 보게 될 동작: 바뀌는 것 없음

확인: analyze 0 · 단위 1498 ×2 · 통합 6/6 ×2 · base10 214장 0px

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
modular
RyuJongJin 2026-10-24 15:35:07 +09:00
parent c27eb9b59e
commit e93b0b0be6
10 changed files with 27 additions and 8 deletions

View File

@ -75,5 +75,5 @@ class RemoteRegistry<S, C extends RemoteClient> {
/// 문: 비밀 값이 있으면 마스터 확인을 지나야 한다 (못 지나면 연결 전에 [DavLockedException]). 비밀 값이 없으면 묻지 않는다
Future<void> requireSecretGate(bool hasSecret) async {
if (hasSecret && !await SecretGate.pass()) throw DavLockedException(tr(secretGateMessage));
if (!await SecretGate.passIf(hasSecret)) throw DavLockedException(tr(secretGateMessage));
}

View File

@ -6,6 +6,10 @@ class SecretGate {
/// 써도 되면 true
static Future<bool> pass({bool force = false}) => check?.call(force) ?? Future.value(true);
/// 비밀 값이 있을 때만 문을 지나야 한다 ([hasSecret] 가 false 면 묻지 않고 true). 저장된 비밀 값을 쓰기 · 보이기 · 고치기 전에 부른다.
/// 원격 등록 · 플레이어 · 그림 AI · 서버 · 서비스 설정이 함께 쓴다 (관리자 10/24 - 같은 꼴의 복사를 한 곳으로)
static Future<bool> passIf(bool hasSecret, {bool force = false}) => hasSecret ? pass(force: force) : Future.value(true);
}
/// 마스터 비밀번호를 넣지 않아 막혔을 때의 안내

View File

@ -148,7 +148,7 @@ class MediaKitFullPlayer implements MediaPlayer {
if (dav == null) return mk.Media(f);
// 124: 저장된 비밀번호로 여는 것이면 (마스터 비밀번호를 정해 두었으면 묻는다)
// 사용자가 직접 튼 것이니 한 번 취소했어도 다시 묻는다
if (dav.headers.isNotEmpty && !await SecretGate.pass(force: true)) throw Exception(tr(secretGateMessage));
if (!await SecretGate.passIf(dav.headers.isNotEmpty, force: true)) throw Exception(tr(secretGateMessage));
return mk.Media(dav.url, httpHeaders: dav.headers);
}

View File

@ -140,7 +140,7 @@ class RemoteImageEngine implements ImageAiEngine {
Future<List<GeneratedImage>> generate(ImageGenRequest r, {required String outDir, required String baseName, AiProgress? onProgress}) async {
_cancel = false;
// 124: 저장된 API 키를 쓰기 전에 (마스터 비밀번호를 정해 두었으면)
if (service.apiKey.isNotEmpty && !await SecretGate.pass(force: true)) throw ImageAiException(tr(secretGateMessage));
if (!await SecretGate.passIf(service.apiKey.isNotEmpty, force: true)) throw ImageAiException(tr(secretGateMessage));
final seed = resolveSeed(r.seed);
onProgress?.call(null, trf('{0} 에 보내는 중', [service.label]));
final List<List<int>> images = switch (service.kind) {

View File

@ -468,7 +468,7 @@ String _kindName(String kind) => switch (kind) {
/// 서비스 추가 · 고치기. 저장된 API 키가 칸에 채워지므로 고칠 때는 먼저 마스터 비밀번호 (127 과 같음)
Future<void> editAiService(BuildContext context, AppController c, {AiService? old}) async {
if (old != null && old.apiKey.isNotEmpty && !await SecretGate.pass(force: true)) return;
if (old != null && !await SecretGate.passIf(old.apiKey.isNotEmpty, force: true)) return;
if (!context.mounted) return;
final name = TextEditingController(text: old?.name ?? '');
final url = TextEditingController(text: old?.url ?? 'http://');

View File

@ -128,7 +128,7 @@ Future<void> trustFtpCert(AppController c, String id, FtpCertException e) =>
/// FTP 서버 추가 · 고치기 창. 저장한 서버를 돌려준다 (취소면 null)
Future<FtpServer?> editFtpServer(BuildContext context, AppController c, {FtpServer? old}) async {
if (old != null && old.password.isNotEmpty && !await SecretGate.pass(force: true)) return null;
if (old != null && !await SecretGate.passIf(old.password.isNotEmpty, force: true)) return null;
if (!context.mounted) return null;
final name = TextEditingController(text: old?.name ?? '');
final host = TextEditingController(text: old?.host ?? '');

View File

@ -144,7 +144,7 @@ Future<void> trustSftpHostKey(AppController c, String id, SftpHostKeyException e
/// SFTP 서버 추가 · 고치기 창. 저장한 서버를 돌려준다 (취소면 null)
Future<SftpServer?> editSftpServer(BuildContext context, AppController c, {SftpServer? old}) async {
// 저장된 비밀번호 · 키가 칸에 채워지므로 먼저 마스터 비밀번호 (정해 두었으면)
if (old != null && (old.password.isNotEmpty || old.privateKey.isNotEmpty) && !await SecretGate.pass(force: true)) return null;
if (old != null && !await SecretGate.passIf(old.password.isNotEmpty || old.privateKey.isNotEmpty, force: true)) return null;
if (!context.mounted) return null;
final name = TextEditingController(text: old?.name ?? '');
final host = TextEditingController(text: old?.host ?? '');

View File

@ -113,7 +113,7 @@ Future<bool> deleteSmbServer(BuildContext context, AppController c, SmbServer s)
/// SMB 서버 추가 · 고치기 창. 저장한 서버를 돌려준다 (취소면 null). [from]: 경로 입력에 넣은 smb:// · \\서버\공유 (채워 둠)
Future<SmbServer?> editSmbServer(BuildContext context, AppController c, {SmbServer? old, String? from}) async {
if (old != null && old.password.isNotEmpty && !await SecretGate.pass(force: true)) return null;
if (old != null && !await SecretGate.passIf(old.password.isNotEmpty, force: true)) return null;
if (!context.mounted) return null;
final pre = from == null ? null : parseSmbAddress(from);
final name = TextEditingController(text: old?.name ?? '');

View File

@ -99,7 +99,7 @@ List<String> davServerUsers(AppSettings s, String id) {
/// WebDAV 서버 추가 · 고치기 창. 저장한 서버를 돌려준다 (취소면 null).
Future<DavServer?> editDavServer(BuildContext context, AppController c, {DavServer? old}) async {
// 127: 저장된 비밀번호가 칸에 채워지므로 (눈 버튼으로 보임) 먼저 마스터 비밀번호 (정해 두었으면)
if (old != null && old.password.isNotEmpty && !await SecretGate.pass(force: true)) return null;
if (old != null && !await SecretGate.passIf(old.password.isNotEmpty, force: true)) return null;
if (!context.mounted) return null;
final name = TextEditingController(text: old?.name ?? '');
final url = TextEditingController(text: old?.url ?? 'https://');

View File

@ -96,4 +96,19 @@ void main() {
}
expect(asked, 0);
});
test('SecretGate.passIf: 비밀 값이 있을 때만 묻고 그 답을 따름 · 없으면 묻지 않고 지나감 · force 를 넘김', () async {
final asked = <bool>[];
var answer = false;
SecretGate.check = (force) async {
asked.add(force);
return answer;
};
expect(await SecretGate.passIf(false), isTrue);
expect(asked, isEmpty);
expect(await SecretGate.passIf(true), isFalse);
answer = true;
expect(await SecretGate.passIf(true, force: true), isTrue);
expect(asked, [false, true]);
});
}