200 lines
7.9 KiB
Dart
200 lines
7.9 KiB
Dart
/// 인증 계약 — 제공자 인터페이스와 결과 타입.
|
|
///
|
|
/// 원칙: **"키 미투입"은 예외가 아니라 상태다.** Firebase 설정 파일이나 Apple
|
|
/// 웹 인증 값처럼 빌드에 들어가야 할 값이 없을 때가 그렇다. "아직 준비가 안 됐다"이지
|
|
/// "지원하지 않는다"가 아니므로 예외를 던지지 않고 [AuthNotConfigured] 로 돌려준다.
|
|
/// 화면은 이걸 안내 다이얼로그로 보여 준다.
|
|
library;
|
|
|
|
/// 로그인 수단 — **별첨 B v1.4 확정값**.
|
|
///
|
|
/// > 1차 로그인은 Google / Apple / Kakao / LINE 4종입니다. 이메일/비밀번호 가입 및
|
|
/// > X 로그인은 제외합니다. 하나의 FANUZU UID 에 복수 소셜 Provider 연결은 허용합니다.
|
|
///
|
|
/// 답변서 v1.1 Q7 은 이메일을 포함한 5종이었으나 별첨 B v1.4 가 이를 뒤집었다.
|
|
/// [email] 값이 남아 있는 것은 **QA 계정(qa-test@fanuzu.co.kr) 내부 경로** 하나
|
|
/// 때문이다 — 사용자에게 노출되는 목록([AuthRepository.providers])에는 들어가지
|
|
/// 않는다. X(트위터)는 확정 목록에 없어 값 자체를 두지 않는다.
|
|
enum AuthProviderKind {
|
|
email('이메일로 시작하기', '이메일'),
|
|
kakao('카카오로 시작하기', '카카오'),
|
|
line('LINE으로 시작하기', 'LINE'),
|
|
apple('Apple로 시작하기', 'Apple'),
|
|
google('Google로 시작하기', 'Google');
|
|
|
|
const AuthProviderKind(this.label, this.shortName);
|
|
|
|
/// 버튼 라벨. 임의로 바꾸지 마라 — 화면 대조 대상이다.
|
|
final String label;
|
|
|
|
/// 제공자 이름만 — 설정 「SNS 연동」·안내 문구용. [label] 에서 「로 시작하기」를
|
|
/// 떼어 만들면 「LINE으로」가 「LINE으」로 잘린다(인계 결함).
|
|
final String shortName;
|
|
|
|
/// 이메일은 소셜이 아니라 입력 폼이 붙는다. QA 내부 경로만 이걸 쓴다.
|
|
bool get needsForm => this == AuthProviderKind.email;
|
|
}
|
|
|
|
/// 이전 이름. 한 번에 다 바꾸지 않아도 되게 남겨 둔다.
|
|
typedef SocialProvider = AuthProviderKind;
|
|
|
|
/// 로그인된 계정.
|
|
class AuthAccount {
|
|
const AuthAccount({
|
|
required this.uid,
|
|
required this.provider,
|
|
this.displayName,
|
|
this.email,
|
|
this.isMock = false,
|
|
});
|
|
|
|
final String uid;
|
|
final AuthProviderKind? provider;
|
|
final String? displayName;
|
|
final String? email;
|
|
|
|
/// Firebase 미설정 상태에서 만든 임시 세션인지. 실 세션과 절대 섞이면 안 된다.
|
|
final bool isMock;
|
|
}
|
|
|
|
/// 로그인 시도 결과.
|
|
sealed class AuthOutcome {
|
|
const AuthOutcome();
|
|
}
|
|
|
|
final class AuthSucceeded extends AuthOutcome {
|
|
const AuthSucceeded(this.account);
|
|
|
|
final AuthAccount account;
|
|
}
|
|
|
|
/// 사용자가 취소함. 에러로 취급하지 않는다.
|
|
final class AuthCancelled extends AuthOutcome {
|
|
const AuthCancelled();
|
|
}
|
|
|
|
/// 실패. [code] 는 에러 카탈로그 ID 다(소셜 실패 = E-AUTH-03).
|
|
final class AuthFailed extends AuthOutcome {
|
|
const AuthFailed(this.code, this.message);
|
|
|
|
final String code;
|
|
final String message;
|
|
}
|
|
|
|
/// 아직 쓸 수 없음 — 키·설정 파일 미투입.
|
|
final class AuthNotConfigured extends AuthOutcome {
|
|
const AuthNotConfigured(this.provider, this.missing);
|
|
|
|
final AuthProviderKind provider;
|
|
|
|
/// 투입해야 하는 것들(파일명·키명). 화면과 README 에 그대로 노출한다.
|
|
final List<String> missing;
|
|
}
|
|
|
|
/// 로그인 제공자 인터페이스. 이메일도 같은 인터페이스를 쓴다.
|
|
abstract interface class SocialAuthProvider {
|
|
AuthProviderKind get provider;
|
|
|
|
/// 실 연동 코드 경로를 가지고 있는지. 스텁이면 false.
|
|
bool get isLive;
|
|
|
|
/// 비어 있지 않으면 지금은 로그인할 수 없다. [signIn] 은 [AuthNotConfigured] 를 낸다.
|
|
List<String> get missingConfig;
|
|
|
|
Future<AuthOutcome> signIn();
|
|
|
|
Future<void> signOut();
|
|
}
|
|
|
|
/// 이메일 제공자 — **QA 계정 전용 내부 경로**.
|
|
///
|
|
/// 별첨 B v1.4 가 이메일/비밀번호 가입을 1차 범위에서 뺐다. 코드를 지우지 않은 것은
|
|
/// 발주사 QA 계정(qa-test@fanuzu.co.kr)으로 로그인 이후 화면을 확인해야 하기
|
|
/// 때문이며, [AuthRepository.qaEmailProvider] 가 **디버그 빌드에서만** 이 구현체를
|
|
/// 돌려준다. 릴리스 빌드에는 부를 수 있는 경로가 없다.
|
|
abstract interface class EmailAuthProvider implements SocialAuthProvider {
|
|
Future<AuthOutcome> signInWithEmail(String email, String password);
|
|
|
|
Future<AuthOutcome> signUpWithEmail(String email, String password);
|
|
|
|
/// 비밀번호 재설정 메일 발송. Figma 「비밀번호 재설정」 화면이 이걸 부른다.
|
|
Future<AuthOutcome> sendPasswordReset(String email);
|
|
}
|
|
|
|
/// 앱 재실행·토큰 만료·다른 기기 로그아웃을 인증 상태에 반영하는 세션 경계.
|
|
///
|
|
/// 저장소는 이 인터페이스만 알며 Firebase SDK 자체는 어댑터 뒤에 둔다. 덕분에
|
|
/// 외부 설정 파일 없이도 세션 복원과 만료 전이를 단위 테스트할 수 있다.
|
|
abstract interface class AuthSession {
|
|
AuthAccount? get currentAccount;
|
|
|
|
Stream<AuthAccount?> get accountChanges;
|
|
|
|
Future<void> signOut();
|
|
}
|
|
|
|
/// 연동 화면에 거는 제공자 — 별첨 B v1.4 확정 4종.
|
|
/// 이메일은 QA 내부 경로라 목록에 없다.
|
|
const List<AuthProviderKind> linkableProviders = [
|
|
AuthProviderKind.google,
|
|
AuthProviderKind.apple,
|
|
AuthProviderKind.kakao,
|
|
AuthProviderKind.line,
|
|
];
|
|
|
|
/// 하나의 FANUZU UID 에 소셜 Provider 를 더 붙이거나 떼는 경계.
|
|
///
|
|
/// 별첨 B v1.4 — *하나의 FANUZU UID 에 복수 소셜 Provider 연결은 허용합니다.*
|
|
/// Firebase 는 `User.linkWithCredential` / `User.unlink` 로 이걸 지원한다.
|
|
///
|
|
/// 연결은 현재 계정의 유효한 인증 상태와 새 Provider 인증 자격을 **서버가** 모두 확인해 붙인다
|
|
/// (Firebase Auth `linkWithCredential` · 카카오는 `fnAuthKakao`). 이메일 일치는 소유 확인이 아니어서
|
|
/// 비교하지 않는다 — 이메일을 주지 않는 카카오 · LINE 계정도 붙는다. 그 Provider 가 다른 FANUZU UID 에
|
|
/// 붙어 있으면 거부하고(E-AUTH-08) 자동 병합하지 않는다.
|
|
/// 저장소와 화면은 이 인터페이스만 알고 SDK 는 어댑터 뒤에 둔다 — 덕분에 설정
|
|
/// 파일 없이도 연결·해제 규칙을 단위 테스트할 수 있다.
|
|
abstract interface class AccountLinker {
|
|
/// 지금 UID 에 붙어 있는 Provider → 그 Provider 가 준 이메일(모르면 null).
|
|
///
|
|
/// 설정 화면의 「SNS 연동」 목록이 이걸 그대로 그린다.
|
|
Map<AuthProviderKind, String?> get linkedAccounts;
|
|
|
|
Future<AuthOutcome> link(AuthProviderKind provider);
|
|
|
|
/// 마지막 하나 남은 Provider 는 뗄 수 없다 — 떼면 로그인 수단이 사라진다.
|
|
Future<AuthOutcome> unlink(AuthProviderKind provider);
|
|
}
|
|
|
|
/// 설정 파일·키가 없어 실제로는 붙일 수 없는 상태의 연동 경계.
|
|
///
|
|
/// 목 모드에서도 「SNS 연동」 화면 자체는 열려야 한다(별첨2 확인 항목의 화면 동선).
|
|
/// 상태를 메모리에만 들고 있고 UID 에는 아무 일도 하지 않는다.
|
|
final class SampleAccountLinker implements AccountLinker {
|
|
SampleAccountLinker({Map<AuthProviderKind, String?>? seed})
|
|
: _linked = {...?seed};
|
|
|
|
final Map<AuthProviderKind, String?> _linked;
|
|
|
|
@override
|
|
Map<AuthProviderKind, String?> get linkedAccounts => {..._linked};
|
|
|
|
@override
|
|
Future<AuthOutcome> link(AuthProviderKind provider) async {
|
|
_linked[provider] = 'fan@fanuzu.co.kr';
|
|
return AuthSucceeded(
|
|
AuthAccount(uid: 'mock-local', provider: provider, isMock: true),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<AuthOutcome> unlink(AuthProviderKind provider) async {
|
|
if (_linked.length <= 1) {
|
|
return const AuthFailed('E-AUTH-13', '마지막 로그인 수단은 연결을 해제할 수 없습니다.');
|
|
}
|
|
_linked.remove(provider);
|
|
return AuthSucceeded(
|
|
AuthAccount(uid: 'mock-local', provider: provider, isMock: true),
|
|
);
|
|
}
|
|
}
|