Move external secrets behind auth broker

This commit is contained in:
Codex
2026-07-20 14:19:55 +09:00
parent ef0d53aa87
commit cb36612b88
14 changed files with 55 additions and 240 deletions
+5
View File
@@ -33,6 +33,11 @@ Thumbs.db
.env .env
.env.* .env.*
!.env.example !.env.example
secrets/
logs/
temp/
.tmp/
backups/
# Local Android/ADB state # Local Android/ADB state
.android-adb/ .android-adb/
@@ -7,8 +7,6 @@ class AppEnvironment {
required this.organizationApiBaseUrl, required this.organizationApiBaseUrl,
required this.orgContextApiBaseUrl, required this.orgContextApiBaseUrl,
required this.orgContextTenantSlug, required this.orgContextTenantSlug,
required this.baronKeyId,
required this.baronKeySecret,
required this.appVersion, required this.appVersion,
required this.buildTimestamp, required this.buildTimestamp,
}); });
@@ -43,15 +41,6 @@ class AppEnvironment {
'TDC114_ORG_CONTEXT_TENANT_SLUG', 'TDC114_ORG_CONTEXT_TENANT_SLUG',
defaultValue: 'hanmac-family', defaultValue: 'hanmac-family',
); );
const baronKeyId = String.fromEnvironment(
'TDC114_BARON_KEY_ID',
defaultValue: '',
);
const baronKeySecret = String.fromEnvironment(
'TDC114_BARON_KEY_SECRET',
defaultValue: '',
);
return AppEnvironment( return AppEnvironment(
ssoBaseUrl: ssoBaseUrl, ssoBaseUrl: ssoBaseUrl,
apiBaseUrl: sharedApiBaseUrl, apiBaseUrl: sharedApiBaseUrl,
@@ -70,8 +59,6 @@ class AppEnvironment {
: organizationApiBaseOverride) : organizationApiBaseOverride)
: orgContextApiBaseOverride, : orgContextApiBaseOverride,
orgContextTenantSlug: orgContextTenantSlug, orgContextTenantSlug: orgContextTenantSlug,
baronKeyId: baronKeyId,
baronKeySecret: baronKeySecret,
appVersion: const String.fromEnvironment( appVersion: const String.fromEnvironment(
'APP_VERSION', 'APP_VERSION',
defaultValue: '0.1.0', defaultValue: '0.1.0',
@@ -90,8 +77,6 @@ class AppEnvironment {
final String organizationApiBaseUrl; final String organizationApiBaseUrl;
final String orgContextApiBaseUrl; final String orgContextApiBaseUrl;
final String orgContextTenantSlug; final String orgContextTenantSlug;
final String baronKeyId;
final String baronKeySecret;
final String appVersion; final String appVersion;
final String buildTimestamp; final String buildTimestamp;
} }
@@ -12,8 +12,6 @@ class AuthSessionStore {
static const _tokenKey = 'tdc114plus.auth.token'; static const _tokenKey = 'tdc114plus.auth.token';
static const _expiresAtKey = 'tdc114plus.auth.expiresAt'; static const _expiresAtKey = 'tdc114plus.auth.expiresAt';
static const _userKey = 'tdc114plus.auth.user'; static const _userKey = 'tdc114plus.auth.user';
static const _orgContextCredentialKey =
'tdc114plus.auth.orgContextCredential';
static const _pendingRefKey = 'tdc114plus.auth.pending.ref'; static const _pendingRefKey = 'tdc114plus.auth.pending.ref';
static const _pendingExpiresAtKey = 'tdc114plus.auth.pending.expiresAt'; static const _pendingExpiresAtKey = 'tdc114plus.auth.pending.expiresAt';
static const _pendingResendAfterAtKey = static const _pendingResendAfterAtKey =
@@ -30,17 +28,8 @@ class AuthSessionStore {
response.expiresAt.toUtc().toIso8601String(), response.expiresAt.toUtc().toIso8601String(),
); );
await prefs.setString(_userKey, jsonEncode(response.user.toJson())); await prefs.setString(_userKey, jsonEncode(response.user.toJson()));
final orgContextCredential = response.orgContextCredential;
if (orgContextCredential == null) {
await prefs.remove(_orgContextCredentialKey);
} else {
await prefs.setString(
_orgContextCredentialKey,
jsonEncode(orgContextCredential.toJson()),
);
}
debugPrint( debugPrint(
'AuthSessionStore.save token=${response.token} tokenLength=${response.token.length} expiresAt=${response.expiresAt.toUtc().toIso8601String()} orgContextCredential=${orgContextCredential != null}', 'AuthSessionStore.save token=${response.token} tokenLength=${response.token.length} expiresAt=${response.expiresAt.toUtc().toIso8601String()}',
); );
} }
@@ -113,7 +102,6 @@ class AuthSessionStore {
final token = prefs.getString(_tokenKey); final token = prefs.getString(_tokenKey);
final expiresAtValue = prefs.getString(_expiresAtKey); final expiresAtValue = prefs.getString(_expiresAtKey);
final userValue = prefs.getString(_userKey); final userValue = prefs.getString(_userKey);
final orgContextCredentialValue = prefs.getString(_orgContextCredentialKey);
if (token == null || expiresAtValue == null || userValue == null) { if (token == null || expiresAtValue == null || userValue == null) {
debugPrint( debugPrint(
'AuthSessionStore.load missing token=${token != null} expiresAt=${expiresAtValue != null} user=${userValue != null}', 'AuthSessionStore.load missing token=${token != null} expiresAt=${expiresAtValue != null} user=${userValue != null}',
@@ -124,12 +112,9 @@ class AuthSessionStore {
token: token, token: token,
expiresAt: DateTime.parse(expiresAtValue), expiresAt: DateTime.parse(expiresAtValue),
user: LoginUser.fromJson(jsonDecode(userValue) as Map<String, dynamic>), user: LoginUser.fromJson(jsonDecode(userValue) as Map<String, dynamic>),
orgContextCredential: _decodeOrgContextCredential(
orgContextCredentialValue,
),
); );
debugPrint( debugPrint(
'AuthSessionStore.load token=${session.token} tokenLength=${session.token.length} expiresAt=${session.expiresAt.toUtc().toIso8601String()} expired=${session.isExpired} orgContextCredential=${session.orgContextCredential != null}', 'AuthSessionStore.load token=${session.token} tokenLength=${session.token.length} expiresAt=${session.expiresAt.toUtc().toIso8601String()} expired=${session.isExpired}',
); );
return session; return session;
} }
@@ -139,22 +124,9 @@ class AuthSessionStore {
await prefs.remove(_tokenKey); await prefs.remove(_tokenKey);
await prefs.remove(_expiresAtKey); await prefs.remove(_expiresAtKey);
await prefs.remove(_userKey); await prefs.remove(_userKey);
await prefs.remove(_orgContextCredentialKey);
await clearPendingLink(); await clearPendingLink();
debugPrint('AuthSessionStore.clear'); debugPrint('AuthSessionStore.clear');
} }
OrgContextCredential? _decodeOrgContextCredential(String? value) {
if (value == null || value.trim().isEmpty) {
return null;
}
try {
return OrgContextCredential.fromJsonOrNull(jsonDecode(value));
} catch (_) {
return null;
}
}
} }
class StoredAuthSession { class StoredAuthSession {
@@ -162,13 +134,11 @@ class StoredAuthSession {
required this.token, required this.token,
required this.expiresAt, required this.expiresAt,
required this.user, required this.user,
this.orgContextCredential,
}); });
final String token; final String token;
final DateTime expiresAt; final DateTime expiresAt;
final LoginUser user; final LoginUser user;
final OrgContextCredential? orgContextCredential;
bool get isExpired => !expiresAt.isAfter(DateTime.now().toUtc()); bool get isExpired => !expiresAt.isAfter(DateTime.now().toUtc());
} }
@@ -101,14 +101,12 @@ class PhoneLoginResponse {
required this.token, required this.token,
required this.expiresAt, required this.expiresAt,
required this.user, required this.user,
this.orgContextCredential,
}); });
final String status; final String status;
final String token; final String token;
final DateTime expiresAt; final DateTime expiresAt;
final LoginUser user; final LoginUser user;
final OrgContextCredential? orgContextCredential;
factory PhoneLoginResponse.fromJson(Map<String, dynamic> json) { factory PhoneLoginResponse.fromJson(Map<String, dynamic> json) {
return PhoneLoginResponse( return PhoneLoginResponse(
@@ -116,9 +114,6 @@ class PhoneLoginResponse {
token: json['token'] as String? ?? json['accessToken'] as String? ?? '', token: json['token'] as String? ?? json['accessToken'] as String? ?? '',
expiresAt: DateTime.parse(json['expiresAt'] as String), expiresAt: DateTime.parse(json['expiresAt'] as String),
user: LoginUser.fromJson(json['user'] as Map<String, dynamic>? ?? {}), user: LoginUser.fromJson(json['user'] as Map<String, dynamic>? ?? {}),
orgContextCredential: OrgContextCredential.fromJsonOrNull(
json['orgContextCredential'] ?? json['org_context'],
),
); );
} }
@@ -128,83 +123,10 @@ class PhoneLoginResponse {
'token': token, 'token': token,
'expiresAt': expiresAt.toUtc().toIso8601String(), 'expiresAt': expiresAt.toUtc().toIso8601String(),
'user': user.toJson(), 'user': user.toJson(),
if (orgContextCredential != null)
'orgContextCredential': orgContextCredential!.toJson(),
}; };
} }
} }
class OrgContextCredential {
const OrgContextCredential({
required this.baseUrl,
required this.tenantSlug,
required this.keyId,
required this.keySecret,
this.expiresAt,
});
final String baseUrl;
final String tenantSlug;
final String keyId;
final String keySecret;
final DateTime? expiresAt;
bool get isUsable {
return baseUrl.trim().isNotEmpty &&
tenantSlug.trim().isNotEmpty &&
keyId.trim().isNotEmpty &&
keySecret.trim().isNotEmpty &&
(expiresAt == null || expiresAt!.isAfter(DateTime.now().toUtc()));
}
factory OrgContextCredential.fromJson(Map<String, dynamic> json) {
return OrgContextCredential(
baseUrl: _readString(json, const ['baseUrl', 'base_url']),
tenantSlug: _readString(json, const ['tenantSlug', 'tenant_slug']),
keyId: _readString(json, const ['keyId', 'key_id']),
keySecret: _readString(json, const ['keySecret', 'key_secret']),
expiresAt: _readDate(json['expiresAt'] ?? json['expires_at']),
);
}
static OrgContextCredential? fromJsonOrNull(Object? value) {
if (value is! Map) {
return null;
}
final credential = OrgContextCredential.fromJson(
Map<String, dynamic>.from(value),
);
return credential.isUsable ? credential : null;
}
Map<String, dynamic> toJson() {
return {
'baseUrl': baseUrl,
'tenantSlug': tenantSlug,
'keyId': keyId,
'keySecret': keySecret,
if (expiresAt != null) 'expiresAt': expiresAt!.toUtc().toIso8601String(),
};
}
static String _readString(Map<String, dynamic> json, List<String> keys) {
for (final key in keys) {
final value = json[key];
if (value is String && value.trim().isNotEmpty) {
return value.trim();
}
}
return '';
}
static DateTime? _readDate(Object? value) {
if (value is! String || value.trim().isEmpty) {
return null;
}
return DateTime.tryParse(value)?.toUtc();
}
}
class PhoneLoginLinkInitRequest { class PhoneLoginLinkInitRequest {
const PhoneLoginLinkInitRequest({ const PhoneLoginLinkInitRequest({
required this.phoneNumber, required this.phoneNumber,
@@ -16,8 +16,6 @@ class OrgContextApiClient {
const OrgContextApiClient({ const OrgContextApiClient({
required this.httpClient, required this.httpClient,
required this.baseUri, required this.baseUri,
required this.keyId,
required this.keySecret,
required this.tenantSlug, required this.tenantSlug,
this.sessionStore, this.sessionStore,
this.timeout = const Duration(seconds: 15), this.timeout = const Duration(seconds: 15),
@@ -28,8 +26,6 @@ class OrgContextApiClient {
final http.Client httpClient; final http.Client httpClient;
final Uri baseUri; final Uri baseUri;
final String keyId;
final String keySecret;
final String tenantSlug; final String tenantSlug;
final AuthSessionStore? sessionStore; final AuthSessionStore? sessionStore;
final Duration timeout; final Duration timeout;
@@ -91,27 +87,13 @@ class OrgContextApiClient {
String? tenantSlug, String? tenantSlug,
}) async { }) async {
final session = await _session(); final session = await _session();
final sessionCredential = session?.orgContextCredential;
final requestedTenantSlug = tenantSlug?.trim(); final requestedTenantSlug = tenantSlug?.trim();
if (sessionCredential != null && sessionCredential.isUsable) {
return _OrgContextCredentialSnapshot(
baseUri: Uri.parse(sessionCredential.baseUrl),
tenantSlug: requestedTenantSlug == null || requestedTenantSlug.isEmpty
? sessionCredential.tenantSlug
: requestedTenantSlug,
keyId: sessionCredential.keyId,
keySecret: sessionCredential.keySecret,
appSessionToken: session?.token ?? '',
);
}
return _OrgContextCredentialSnapshot( return _OrgContextCredentialSnapshot(
baseUri: baseUri, baseUri: baseUri,
tenantSlug: requestedTenantSlug == null || requestedTenantSlug.isEmpty tenantSlug: requestedTenantSlug == null || requestedTenantSlug.isEmpty
? this.tenantSlug ? this.tenantSlug
: requestedTenantSlug, : requestedTenantSlug,
keyId: keyId,
keySecret: keySecret,
appSessionToken: session?.token ?? '', appSessionToken: session?.token ?? '',
); );
} }
@@ -141,10 +123,6 @@ class OrgContextApiClient {
'accept': 'application/json', 'accept': 'application/json',
if (appSessionToken.isNotEmpty) 'Authorization': 'Bearer $appSessionToken', if (appSessionToken.isNotEmpty) 'Authorization': 'Bearer $appSessionToken',
if (appSessionToken.isNotEmpty) 'X-App-Session': appSessionToken, if (appSessionToken.isNotEmpty) 'X-App-Session': appSessionToken,
if (credential.keyId.trim().isNotEmpty)
'X-Baron-Key-ID': credential.keyId.trim(),
if (credential.keySecret.trim().isNotEmpty)
'X-Baron-Key-Secret': credential.keySecret.trim(),
}; };
debugPrint( debugPrint(
'OrgContextApiClient._headers headers=$headers tenantSlug=${credential.tenantSlug} base=${credential.baseUri}', 'OrgContextApiClient._headers headers=$headers tenantSlug=${credential.tenantSlug} base=${credential.baseUri}',
@@ -458,8 +436,6 @@ final orgContextApiClientProvider = Provider<OrgContextApiClient>((ref) {
return OrgContextApiClient( return OrgContextApiClient(
httpClient: ref.watch(httpClientProvider), httpClient: ref.watch(httpClientProvider),
baseUri: Uri.parse(environment.orgContextApiBaseUrl), baseUri: Uri.parse(environment.orgContextApiBaseUrl),
keyId: environment.baronKeyId,
keySecret: environment.baronKeySecret,
tenantSlug: environment.orgContextTenantSlug, tenantSlug: environment.orgContextTenantSlug,
sessionStore: ref.watch(authSessionStoreProvider), sessionStore: ref.watch(authSessionStoreProvider),
); );
@@ -475,22 +451,18 @@ class _OrgContextCredentialSnapshot {
const _OrgContextCredentialSnapshot({ const _OrgContextCredentialSnapshot({
required this.baseUri, required this.baseUri,
required this.tenantSlug, required this.tenantSlug,
required this.keyId,
required this.keySecret,
required this.appSessionToken, required this.appSessionToken,
}); });
final Uri baseUri; final Uri baseUri;
final String tenantSlug; final String tenantSlug;
final String keyId;
final String keySecret;
final String appSessionToken; final String appSessionToken;
String get cacheKey { String get cacheKey {
return [ return [
baseUri.toString(), baseUri.toString(),
tenantSlug, tenantSlug,
appSessionToken.isEmpty ? keyId : appSessionToken, appSessionToken,
].join('|'); ].join('|');
} }
} }
@@ -22,13 +22,6 @@ void main() {
tenantName: 'Hanmac', tenantName: 'Hanmac',
tenantSlug: 'hanmac', tenantSlug: 'hanmac',
), ),
orgContextCredential: OrgContextCredential(
baseUrl: 'https://sadmin.hmac.kr',
tenantSlug: 'hanmac-family',
keyId: 'session-key-id',
keySecret: 'session-key-secret',
expiresAt: DateTime.parse('2099-07-02T13:00:00Z'),
),
); );
await store.save(response); await store.save(response);
@@ -36,8 +29,6 @@ void main() {
expect(loaded?.token, 'session-token'); expect(loaded?.token, 'session-token');
expect(loaded?.user.name, 'User One'); expect(loaded?.user.name, 'User One');
expect(loaded?.orgContextCredential?.keyId, 'session-key-id');
expect(loaded?.orgContextCredential?.keySecret, 'session-key-secret');
await store.clear(); await store.clear();
expect(await store.load(), isNull); expect(await store.load(), isNull);
@@ -22,14 +22,12 @@ void main() {
expect(request.url.queryParameters['tenantSlug'], 'hanmac-family'); expect(request.url.queryParameters['tenantSlug'], 'hanmac-family');
expect(request.url.queryParameters['includeUsers'], 'true'); expect(request.url.queryParameters['includeUsers'], 'true');
expect(request.url.queryParameters['includeUserIds'], 'true'); expect(request.url.queryParameters['includeUserIds'], 'true');
expect(request.headers['X-Baron-Key-ID'], 'key-id'); expect(request.headers.containsKey('X-Baron-Key-ID'), isFalse);
expect(request.headers['X-Baron-Key-Secret'], 'key-secret'); expect(request.headers.containsKey('X-Baron-Key-Secret'), isFalse);
return _jsonResponse(_orgContextResponse()); return _jsonResponse(_orgContextResponse());
}), }),
baseUri: Uri.parse('https://sadmin.hmac.kr'), baseUri: Uri.parse('https://sadmin.hmac.kr'),
keyId: 'key-id',
keySecret: 'key-secret',
tenantSlug: 'hanmac-family', tenantSlug: 'hanmac-family',
); );
@@ -49,7 +47,7 @@ void main() {
expect(is3.totalMemberCount, 2); expect(is3.totalMemberCount, 2);
}); });
test('uses session org-context credential before env fallback', () async { test('does not persist or send session org-context credentials', () async {
const store = AuthSessionStore(); const store = AuthSessionStore();
await store.save( await store.save(
PhoneLoginResponse( PhoneLoginResponse(
@@ -64,27 +62,19 @@ void main() {
tenantName: '', tenantName: '',
tenantSlug: '', tenantSlug: '',
), ),
orgContextCredential: const OrgContextCredential(
baseUrl: 'https://session.example.test',
tenantSlug: 'session-family',
keyId: 'session-key-id',
keySecret: 'session-key-secret',
),
), ),
); );
final client = OrgContextApiClient( final client = OrgContextApiClient(
httpClient: MockClient((request) async { httpClient: MockClient((request) async {
expect(request.url.host, 'session.example.test'); expect(request.url.host, 'env.example.test');
expect(request.url.queryParameters['tenantSlug'], 'session-family'); expect(request.url.queryParameters['tenantSlug'], 'env-family');
expect(request.headers['Authorization'], 'Bearer session-token'); expect(request.headers['Authorization'], 'Bearer session-token');
expect(request.headers['X-Baron-Key-ID'], 'session-key-id'); expect(request.headers.containsKey('X-Baron-Key-ID'), isFalse);
expect(request.headers['X-Baron-Key-Secret'], 'session-key-secret'); expect(request.headers.containsKey('X-Baron-Key-Secret'), isFalse);
return _jsonResponse(_orgContextResponse()); return _jsonResponse(_orgContextResponse());
}), }),
baseUri: Uri.parse('https://env.example.test'), baseUri: Uri.parse('https://env.example.test'),
keyId: 'env-key-id',
keySecret: 'env-key-secret',
tenantSlug: 'env-family', tenantSlug: 'env-family',
sessionStore: store, sessionStore: store,
); );
@@ -99,8 +89,6 @@ void main() {
return _jsonResponse(_orgContextResponse()); return _jsonResponse(_orgContextResponse());
}), }),
baseUri: Uri.parse('https://sadmin.hmac.kr'), baseUri: Uri.parse('https://sadmin.hmac.kr'),
keyId: 'key-id',
keySecret: 'key-secret',
tenantSlug: 'hanmac-family', tenantSlug: 'hanmac-family',
); );
@@ -115,8 +103,6 @@ void main() {
return _jsonResponse(_orgContextResponse()); return _jsonResponse(_orgContextResponse());
}), }),
baseUri: Uri.parse('https://114-auth.hmac.kr'), baseUri: Uri.parse('https://114-auth.hmac.kr'),
keyId: '',
keySecret: '',
tenantSlug: 'hanmac-family', tenantSlug: 'hanmac-family',
); );
@@ -155,8 +141,6 @@ void main() {
return _jsonResponse(_orgContextResponse()); return _jsonResponse(_orgContextResponse());
}), }),
baseUri: Uri.parse('https://114-auth.hmac.kr'), baseUri: Uri.parse('https://114-auth.hmac.kr'),
keyId: '',
keySecret: '',
tenantSlug: 'hanmac-family', tenantSlug: 'hanmac-family',
sessionStore: store, sessionStore: store,
); );
@@ -173,8 +157,6 @@ void main() {
return _jsonResponse(_orgContextResponse()); return _jsonResponse(_orgContextResponse());
}), }),
baseUri: Uri.parse('https://114-auth.hmac.kr'), baseUri: Uri.parse('https://114-auth.hmac.kr'),
keyId: '',
keySecret: '',
tenantSlug: 'hanmac-family', tenantSlug: 'hanmac-family',
); );
@@ -184,27 +166,22 @@ void main() {
expect(requestCount, 1); expect(requestCount, 1);
}); });
test( test('uses configured auth-server org-context endpoint without Baron key headers', () async {
'falls back to env org-context credential without session credential',
() async {
final client = OrgContextApiClient( final client = OrgContextApiClient(
httpClient: MockClient((request) async { httpClient: MockClient((request) async {
expect(request.url.host, 'env.example.test'); expect(request.url.host, 'env.example.test');
expect(request.url.queryParameters['tenantSlug'], 'env-family'); expect(request.url.queryParameters['tenantSlug'], 'env-family');
expect(request.headers['X-Baron-Key-ID'], 'env-key-id'); expect(request.headers.containsKey('X-Baron-Key-ID'), isFalse);
expect(request.headers['X-Baron-Key-Secret'], 'env-key-secret'); expect(request.headers.containsKey('X-Baron-Key-Secret'), isFalse);
return _jsonResponse(_orgContextResponse()); return _jsonResponse(_orgContextResponse());
}), }),
baseUri: Uri.parse('https://env.example.test'), baseUri: Uri.parse('https://env.example.test'),
keyId: 'env-key-id',
keySecret: 'env-key-secret',
tenantSlug: 'env-family', tenantSlug: 'env-family',
sessionStore: const AuthSessionStore(), sessionStore: const AuthSessionStore(),
); );
await client.fetchOrgContext(); await client.fetchOrgContext();
}, });
);
test( test(
'remote directory repository filters org-context employees locally', 'remote directory repository filters org-context employees locally',
@@ -215,8 +192,6 @@ void main() {
return _jsonResponse(_orgContextResponse()); return _jsonResponse(_orgContextResponse());
}), }),
baseUri: Uri.parse('https://sadmin.hmac.kr'), baseUri: Uri.parse('https://sadmin.hmac.kr'),
keyId: 'key-id',
keySecret: 'key-secret',
tenantSlug: 'hanmac-family', tenantSlug: 'hanmac-family',
); );
final repository = RemoteDirectoryRepository(orgContextApiClient: client); final repository = RemoteDirectoryRepository(orgContextApiClient: client);
@@ -238,8 +213,6 @@ void main() {
return _jsonResponse(_orgContextResponse()); return _jsonResponse(_orgContextResponse());
}), }),
baseUri: Uri.parse('https://sadmin.hmac.kr'), baseUri: Uri.parse('https://sadmin.hmac.kr'),
keyId: 'key-id',
keySecret: 'key-secret',
tenantSlug: 'hanmac-family', tenantSlug: 'hanmac-family',
); );
final repository = RemoteDirectoryRepository(orgContextApiClient: client); final repository = RemoteDirectoryRepository(orgContextApiClient: client);
@@ -264,8 +237,6 @@ void main() {
return _jsonResponse(_orgContextResponse()); return _jsonResponse(_orgContextResponse());
}), }),
baseUri: Uri.parse('https://sadmin.hmac.kr'), baseUri: Uri.parse('https://sadmin.hmac.kr'),
keyId: 'key-id',
keySecret: 'key-secret',
tenantSlug: 'hanmac-family', tenantSlug: 'hanmac-family',
); );
final repository = RemoteDirectoryRepository(orgContextApiClient: client); final repository = RemoteDirectoryRepository(orgContextApiClient: client);
@@ -154,6 +154,7 @@
- 공기계 USB 연결 기준으로는 실데이터 화면까지 확인했지만, 현재 실행 모드는 `adb reverse + http://127.0.0.1:5000` 기반 로컬 Baron API 연결이므로 USB 분리 후 독립 동작은 아직 보장하지 않는다 - 공기계 USB 연결 기준으로는 실데이터 화면까지 확인했지만, 현재 실행 모드는 `adb reverse + http://127.0.0.1:5000` 기반 로컬 Baron API 연결이므로 USB 분리 후 독립 동작은 아직 보장하지 않는다
- USB 없이 동작하는 실서버 APK 테스트로 가려면 `TDC114_API_BASE`를 staging 또는 production 공개 URL로 전환하고, headless 승인 로그인과 HTTPS 경로를 그 환경에서 다시 검증해야 한다 - USB 없이 동작하는 실서버 APK 테스트로 가려면 `TDC114_API_BASE`를 staging 또는 production 공개 URL로 전환하고, headless 승인 로그인과 HTTPS 경로를 그 환경에서 다시 검증해야 한다
- 2026-07-08 기준 앱/스크립트는 `TDC114_AUTH_API_BASE`, `TDC114_DIRECTORY_API_BASE`, `TDC114_ORGANIZATION_API_BASE` 분리 주입을 지원하므로 `로그인은 staging`, `직원/조직 데이터는 production` 조합까지 실행 준비가 되어 있다 - 2026-07-08 기준 앱/스크립트는 `TDC114_AUTH_API_BASE`, `TDC114_DIRECTORY_API_BASE`, `TDC114_ORGANIZATION_API_BASE` 분리 주입을 지원하므로 `로그인은 staging`, `직원/조직 데이터는 production` 조합까지 실행 준비가 되어 있다
- 2026-07-20 기준 앱에는 Baron org-context key와 NAVER WORKS secret을 주입하지 않는다. 해당 값은 `tdc114plus-auth` 서버 env에서만 관리하고, 앱은 중계서버 URL과 앱 세션 token만 사용한다
- 2026-07-10 기준 조직/직원 원본 참고 host는 staging `https://sadmin.hmac.kr`로 되돌린다. `admin.brsw.kr` production host는 현재 앱 개발 기준에서 우선 사용하지 않는다 - 2026-07-10 기준 조직/직원 원본 참고 host는 staging `https://sadmin.hmac.kr`로 되돌린다. `admin.brsw.kr` production host는 현재 앱 개발 기준에서 우선 사용하지 않는다
- 2026-07-10 기준 Baron SSO 로그인 성공 시 조직도 API 호출용 ID/Secret을 함께 내려주는 기능은 아직 미개발로 보고, 앱은 해당 값을 받을 준비만 먼저 한다 - 2026-07-10 기준 Baron SSO 로그인 성공 시 조직도 API 호출용 ID/Secret을 함께 내려주는 기능은 아직 미개발로 보고, 앱은 해당 값을 받을 준비만 먼저 한다
- 해당 기능이 완성되기 전까지 조직/직원 데이터는 staging `org-context` endpoint와 로컬 비추적 env/Dart define의 고정 키 fallback으로 검증한다 - 해당 기능이 완성되기 전까지 조직/직원 데이터는 staging `org-context` endpoint와 로컬 비추적 env/Dart define의 고정 키 fallback으로 검증한다
+2 -1
View File
@@ -8,7 +8,7 @@
- `smoke.env.example`: authenticated smoke용 로컬 env 예시 파일이다. `scripts/.env.smoke.local`로 복사해 `TDC114_API_BASE`, `TDC114_SMOKE_PHONE`을 넣어 사용할 수 있다. - `smoke.env.example`: authenticated smoke용 로컬 env 예시 파일이다. `scripts/.env.smoke.local`로 복사해 `TDC114_API_BASE`, `TDC114_SMOKE_PHONE`을 넣어 사용할 수 있다.
- `smoke.staging.env.example`: staging Baron SSO 검증용 env 예시 파일이다. `scripts/.env.staging.local`로 복사해 `TDC114_API_BASE`, `TDC114_SMOKE_PHONE`과 optional expected label 값을 넣어 사용할 수 있다. - `smoke.staging.env.example`: staging Baron SSO 검증용 env 예시 파일이다. `scripts/.env.staging.local`로 복사해 `TDC114_API_BASE`, `TDC114_SMOKE_PHONE`과 optional expected label 값을 넣어 사용할 수 있다.
- `android-device.env.example`: Android 공기계 USB 테스트용 env 예시 파일이다. `scripts/.env.android-device.local`로 복사해 `adb reverse` 기준 `TDC114_API_BASE=http://127.0.0.1:5000`, `TDC114_AUTH_API_BASE=http://127.0.0.1:5001`을 사용한다. - `android-device.env.example`: Android 공기계 USB 테스트용 env 예시 파일이다. `scripts/.env.android-device.local`로 복사해 `adb reverse` 기준 `TDC114_API_BASE=http://127.0.0.1:5000`, `TDC114_AUTH_API_BASE=http://127.0.0.1:5001`을 사용한다.
- `android-device.staging.env.example`: Android 공기계/실폰의 USB 없는 staging 직접 로그인 테스트용 env 예시 파일이다. `scripts/.env.android-device.staging.local`로 복사해 공개 staging HTTPS base URL을 넣어 사용한다. - `android-device.staging.env.example`: Android 공기계/실폰의 USB 없는 staging 직접 로그인 테스트용 env 예시 파일이다. `scripts/.env.android-device.staging.local`로 복사해 공개 staging HTTPS base URL을 넣어 사용한다. Baron org-context key는 앱 env가 아니라 `tdc114plus-auth` 서버 env에만 둔다.
- `android-device.production.env.example`: Android 공기계/실폰의 USB 없는 production 직접 로그인 테스트용 env 예시 파일이다. `scripts/.env.android-device.production.local`로 복사해 공개 production HTTPS base URL을 넣어 사용한다. - `android-device.production.env.example`: Android 공기계/실폰의 USB 없는 production 직접 로그인 테스트용 env 예시 파일이다. `scripts/.env.android-device.production.local`로 복사해 공개 production HTTPS base URL을 넣어 사용한다.
- `bootstrap-baron-api-env.sh`: Baron SSO API worktree의 `.env.sample`을 바탕으로 로컬 smoke용 `.env`를 생성하고 localhost/알림 비활성 override를 덧붙인다. - `bootstrap-baron-api-env.sh`: Baron SSO API worktree의 `.env.sample`을 바탕으로 로컬 smoke용 `.env`를 생성하고 localhost/알림 비활성 override를 덧붙인다.
- `check-baron-api-env.sh`: Baron SSO API worktree의 `.env`, compose, config, Docker runtime 준비 상태를 점검해 실제 API smoke 가능 여부를 빠르게 확인한다. - `check-baron-api-env.sh`: Baron SSO API worktree의 `.env`, compose, config, Docker runtime 준비 상태를 점검해 실제 API smoke 가능 여부를 빠르게 확인한다.
@@ -41,6 +41,7 @@
- ADB 로그 조회, `devices`, `reverse --list`, `screencap` 등 Flutter가 필요 없는 ADB 작업은 `flutter-docker.sh adb` 대신 `ADB_SERVER_SOCKET=tcp:172.21.128.1:5037 ./scripts/host-adb.sh ...`를 우선 사용한다. - ADB 로그 조회, `devices`, `reverse --list`, `screencap` 등 Flutter가 필요 없는 ADB 작업은 `flutter-docker.sh adb` 대신 `ADB_SERVER_SOCKET=tcp:172.21.128.1:5037 ./scripts/host-adb.sh ...`를 우선 사용한다.
- USB 없는 staging 직접 로그인 검증 시에는 `TDC114_SMOKE_ENV_FILE=scripts/.env.android-device.staging.local``TDC114_FLUTTER_DEVICE_ID=<PHYSICAL_DEVICE_ID>`를 명시한다. - USB 없는 staging 직접 로그인 검증 시에는 `TDC114_SMOKE_ENV_FILE=scripts/.env.android-device.staging.local``TDC114_FLUTTER_DEVICE_ID=<PHYSICAL_DEVICE_ID>`를 명시한다.
- 로그인만 staging, 직원/조직 정보만 production으로 분리해야 할 경우 `TDC114_AUTH_API_BASE`, `TDC114_DIRECTORY_API_BASE`, `TDC114_ORGANIZATION_API_BASE`를 추가로 지정한다. 값을 비우면 모두 `TDC114_API_BASE`를 따른다. - 로그인만 staging, 직원/조직 정보만 production으로 분리해야 할 경우 `TDC114_AUTH_API_BASE`, `TDC114_DIRECTORY_API_BASE`, `TDC114_ORGANIZATION_API_BASE`를 추가로 지정한다. 값을 비우면 모두 `TDC114_API_BASE`를 따른다.
- 앱 실행 env에는 `TDC114_BARON_KEY_ID`, `TDC114_BARON_KEY_SECRET`, NAVER WORKS secret을 넣지 않는다. 조직도 key와 네이버웍스 key는 `tdc114plus-auth` 서버 환경변수에서만 관리한다.
- `startup.sh`, `shutdown.sh`는 테스트나 다른 worktree 재사용을 위해 `BARON_SSO_WORKTREE`, `TDC114_LOG_BASE`, `DOCKER_BIN` 같은 환경 변수 override를 지원한다. - `startup.sh`, `shutdown.sh`는 테스트나 다른 worktree 재사용을 위해 `BARON_SSO_WORKTREE`, `TDC114_LOG_BASE`, `DOCKER_BIN` 같은 환경 변수 override를 지원한다.
- 업무 시작 절차에서는 앱 직접 App Link callback 테스트 서버를 더 이상 기동하지 않는다. 현재 로그인은 `tdc114plus-auth``/api/v1/auth/link/init`, `/api/v1/auth/link/poll` 경로를 사용한다. - 업무 시작 절차에서는 앱 직접 App Link callback 테스트 서버를 더 이상 기동하지 않는다. 현재 로그인은 `tdc114plus-auth``/api/v1/auth/link/init`, `/api/v1/auth/link/poll` 경로를 사용한다.
- Windows Android Studio emulator + WSL/Docker Flutter 운영 기준은 `docs/policy_android_studio_wsl_adb_2026-07-03.md`를 따른다. - Windows Android Studio emulator + WSL/Docker Flutter 운영 기준은 `docs/policy_android_studio_wsl_adb_2026-07-03.md`를 따른다.
+5 -7
View File
@@ -3,10 +3,9 @@
# The app talks directly to the public Baron staging API over HTTPS. # The app talks directly to the public Baron staging API over HTTPS.
# No adb reverse is required in this mode. # No adb reverse is required in this mode.
TDC114_API_BASE=https://sso.hmac.kr TDC114_API_BASE=https://sso.hmac.kr
TDC114_ORG_CONTEXT_API_BASE=https://sadmin.hmac.kr TDC114_AUTH_API_BASE=https://114-auth.hmac.kr
TDC114_ORG_CONTEXT_API_BASE=https://114-auth.hmac.kr
TDC114_ORG_CONTEXT_TENANT_SLUG=hanmac-family TDC114_ORG_CONTEXT_TENANT_SLUG=hanmac-family
TDC114_BARON_KEY_ID=replace-with-staging-key-id
TDC114_BARON_KEY_SECRET=replace-with-staging-key-secret
# Skip local phone-login bootstrap and open the normal in-app login flow. # Skip local phone-login bootstrap and open the normal in-app login flow.
TDC114_SKIP_SESSION_BOOTSTRAP=true TDC114_SKIP_SESSION_BOOTSTRAP=true
@@ -18,7 +17,6 @@ TDC114_SMOKE_PHONE=010xxxxxxxx
# TDC114_SMOKE_EXPECTED_TENANT_LABEL=IS3 # TDC114_SMOKE_EXPECTED_TENANT_LABEL=IS3
# Optional split mode: # Optional split mode:
# Login and org-context stay on staging by default. # Login and org-context should normally go through tdc114plus-auth.
# TDC114_AUTH_API_BASE=https://sso.hmac.kr # TDC114_DIRECTORY_API_BASE=https://114-auth.hmac.kr
# TDC114_DIRECTORY_API_BASE=https://sadmin.hmac.kr # TDC114_ORGANIZATION_API_BASE=https://114-auth.hmac.kr
# TDC114_ORGANIZATION_API_BASE=https://sadmin.hmac.kr
-2
View File
@@ -49,8 +49,6 @@ append_optional_define "TDC114_DIRECTORY_API_BASE"
append_optional_define "TDC114_ORGANIZATION_API_BASE" append_optional_define "TDC114_ORGANIZATION_API_BASE"
append_optional_define "TDC114_ORG_CONTEXT_API_BASE" append_optional_define "TDC114_ORG_CONTEXT_API_BASE"
append_optional_define "TDC114_ORG_CONTEXT_TENANT_SLUG" append_optional_define "TDC114_ORG_CONTEXT_TENANT_SLUG"
append_optional_define "TDC114_BARON_KEY_ID"
append_optional_define "TDC114_BARON_KEY_SECRET"
TEST_DEVICE_ID="${TDC114_FLUTTER_DEVICE_ID:-${TDC114_ADB_CONNECT_ADDRESS:-}}" TEST_DEVICE_ID="${TDC114_FLUTTER_DEVICE_ID:-${TDC114_ADB_CONNECT_ADDRESS:-}}"
if [ -n "$TEST_DEVICE_ID" ]; then if [ -n "$TEST_DEVICE_ID" ]; then
-2
View File
@@ -56,8 +56,6 @@ append_optional_define "TDC114_DIRECTORY_API_BASE"
append_optional_define "TDC114_ORGANIZATION_API_BASE" append_optional_define "TDC114_ORGANIZATION_API_BASE"
append_optional_define "TDC114_ORG_CONTEXT_API_BASE" append_optional_define "TDC114_ORG_CONTEXT_API_BASE"
append_optional_define "TDC114_ORG_CONTEXT_TENANT_SLUG" append_optional_define "TDC114_ORG_CONTEXT_TENANT_SLUG"
append_optional_define "TDC114_BARON_KEY_ID"
append_optional_define "TDC114_BARON_KEY_SECRET"
if [ "$SKIP_SESSION_BOOTSTRAP" = "1" ] || [ "$SKIP_SESSION_BOOTSTRAP" = "true" ]; then if [ "$SKIP_SESSION_BOOTSTRAP" = "1" ] || [ "$SKIP_SESSION_BOOTSTRAP" = "true" ]; then
echo "Skipping session bootstrap; app will open its normal login flow." >&2 echo "Skipping session bootstrap; app will open its normal login flow." >&2
+4 -6
View File
@@ -1,8 +1,7 @@
TDC114_API_BASE=https://sso.hmac.kr TDC114_API_BASE=https://sso.hmac.kr
TDC114_ORG_CONTEXT_API_BASE=https://sadmin.hmac.kr TDC114_AUTH_API_BASE=https://114-auth.hmac.kr
TDC114_ORG_CONTEXT_API_BASE=https://114-auth.hmac.kr
TDC114_ORG_CONTEXT_TENANT_SLUG=hanmac-family TDC114_ORG_CONTEXT_TENANT_SLUG=hanmac-family
TDC114_BARON_KEY_ID=replace-with-staging-key-id
TDC114_BARON_KEY_SECRET=replace-with-staging-key-secret
TDC114_SMOKE_PHONE=010xxxxxxxx TDC114_SMOKE_PHONE=010xxxxxxxx
TDC114_SMOKE_AUTH_FLOW=link TDC114_SMOKE_AUTH_FLOW=link
@@ -11,6 +10,5 @@ TDC114_SMOKE_AUTH_FLOW=link
# TDC114_SMOKE_EXPECTED_TENANT_LABEL=IS3 # TDC114_SMOKE_EXPECTED_TENANT_LABEL=IS3
# Optional split mode: # Optional split mode:
# TDC114_AUTH_API_BASE=https://sso.hmac.kr # TDC114_DIRECTORY_API_BASE=https://114-auth.hmac.kr
# TDC114_DIRECTORY_API_BASE=https://sadmin.hmac.kr # TDC114_ORGANIZATION_API_BASE=https://114-auth.hmac.kr
# TDC114_ORGANIZATION_API_BASE=https://sadmin.hmac.kr
+13 -8
View File
@@ -107,14 +107,19 @@ if [ -f "$NAVER_WORKS_ENV_FILE" ]; then
fi fi
fi fi
: "${TDC114_ORG_CONTEXT_API_BASE:?TDC114_ORG_CONTEXT_API_BASE is required in $ENV_FILE}" ORG_CONTEXT_BASE="${BARON_ORG_CONTEXT_BASE_URL:-${TDC114_ORG_CONTEXT_API_BASE:-}}"
: "${TDC114_ORG_CONTEXT_TENANT_SLUG:?TDC114_ORG_CONTEXT_TENANT_SLUG is required in $ENV_FILE}" ORG_CONTEXT_TENANT_SLUG="${BARON_ORG_CONTEXT_TENANT_SLUG:-${TDC114_ORG_CONTEXT_TENANT_SLUG:-}}"
: "${TDC114_BARON_KEY_ID:?TDC114_BARON_KEY_ID is required in $ENV_FILE}" ORG_CONTEXT_KEY_ID="${BARON_ORG_CONTEXT_KEY_ID:-${TDC114_BARON_KEY_ID:-}}"
: "${TDC114_BARON_KEY_SECRET:?TDC114_BARON_KEY_SECRET is required in $ENV_FILE}" ORG_CONTEXT_KEY_SECRET="${BARON_ORG_CONTEXT_KEY_SECRET:-${TDC114_BARON_KEY_SECRET:-}}"
: "${ORG_CONTEXT_BASE:?BARON_ORG_CONTEXT_BASE_URL is required in $ENV_FILE}"
: "${ORG_CONTEXT_TENANT_SLUG:?BARON_ORG_CONTEXT_TENANT_SLUG is required in $ENV_FILE}"
: "${ORG_CONTEXT_KEY_ID:?BARON_ORG_CONTEXT_KEY_ID is required in $ENV_FILE}"
: "${ORG_CONTEXT_KEY_SECRET:?BARON_ORG_CONTEXT_KEY_SECRET is required in $ENV_FILE}"
UPSTREAM_ORG_CONTEXT_BASE="${TDC114_AUTH_UPSTREAM_ORG_CONTEXT_API_BASE:-}" UPSTREAM_ORG_CONTEXT_BASE="${TDC114_AUTH_UPSTREAM_ORG_CONTEXT_API_BASE:-}"
if [ -z "$UPSTREAM_ORG_CONTEXT_BASE" ]; then if [ -z "$UPSTREAM_ORG_CONTEXT_BASE" ]; then
UPSTREAM_ORG_CONTEXT_BASE="$TDC114_ORG_CONTEXT_API_BASE" UPSTREAM_ORG_CONTEXT_BASE="$ORG_CONTEXT_BASE"
fi fi
log "Starting tdc114plus-auth on :$PORT" log "Starting tdc114plus-auth on :$PORT"
@@ -138,9 +143,9 @@ log "Log file: $LOG_FILE"
BARON_JWKS_KID="${BARON_JWKS_KID:-tdc114plus-auth-key-1}" \ BARON_JWKS_KID="${BARON_JWKS_KID:-tdc114plus-auth-key-1}" \
BARON_LINK_RETURN_URI="${BARON_LINK_RETURN_URI:-}" \ BARON_LINK_RETURN_URI="${BARON_LINK_RETURN_URI:-}" \
BARON_ORG_CONTEXT_BASE_URL="$UPSTREAM_ORG_CONTEXT_BASE" \ BARON_ORG_CONTEXT_BASE_URL="$UPSTREAM_ORG_CONTEXT_BASE" \
BARON_ORG_CONTEXT_TENANT_SLUG="$TDC114_ORG_CONTEXT_TENANT_SLUG" \ BARON_ORG_CONTEXT_TENANT_SLUG="$ORG_CONTEXT_TENANT_SLUG" \
BARON_ORG_CONTEXT_KEY_ID="$TDC114_BARON_KEY_ID" \ BARON_ORG_CONTEXT_KEY_ID="$ORG_CONTEXT_KEY_ID" \
BARON_ORG_CONTEXT_KEY_SECRET="$TDC114_BARON_KEY_SECRET" \ BARON_ORG_CONTEXT_KEY_SECRET="$ORG_CONTEXT_KEY_SECRET" \
go run ./cmd/server >>"$LOG_FILE" 2>&1 < /dev/null & go run ./cmd/server >>"$LOG_FILE" 2>&1 < /dev/null &
) )