Line data Source code
1 : // Package auth は、認証ユースケース(Usecase)の実装を提供します。
2 : // 本ファイルでは「Firebase IDトークン検証済みのユーザー情報を元に、
3 : // users / auth_identities を upsert し、レスポンスDTOを返す」ユースケースを実装します。
4 : package auth
5 :
6 : import (
7 : "context"
8 : "fmt"
9 :
10 : "resume/internal/shared/apperr"
11 : "resume/internal/shared/util"
12 : )
13 :
14 : // UpsertUserFromFirebase は、VerifyIDToken 済みの入力(UID / プロバイダ配列 等)を受け取り、
15 : // - users テーブルの作成 or 更新(LastLoginAt をサーバ時刻で更新)
16 : // - auth_identities テーブルの upsert(provider, provider_user_id で一意)
17 : // を **1トランザクション** で実行します。
18 : // 成功時は、最新のユーザー情報(紐付くプロバイダ一覧を含む)とトークンを返します。
19 : //
20 : // エラーポリシー:
21 : // - 入力不足(IDToken/UID 無し)は ErrInvalidInput を返す
22 : // - リポジトリ層のDBエラーは mapInfraToUCError で UC語彙(ErrConflict/ErrNotFound 等)に正規化しつつ伝播
23 : // - トランザクション内のいずれかで失敗したらその場で中断・ロールバック
24 0 : func (u *interactor) UpsertUserFromFirebase(ctx context.Context, in UpsertUserFromFirebaseInput) (AuthResultOutput, error) {
25 0 : // --- 0) 入力チェック -------------------------------------------------------
26 0 : // ここでは必須だけを軽く確認(詳細バリデーションは上位レイヤで実施しても良い)。
27 0 : if in.IDToken == "" || in.UID == "" {
28 0 : return AuthResultOutput{}, apperr.New(
29 0 : apperr.CodeUnprocessable,
30 0 : "validation failed",
31 0 : map[string]any{"fields": map[string]string{
32 0 : "idToken": "required",
33 0 : "uid": "required",
34 0 : }},
35 0 : )
36 0 : }
37 :
38 : // UCに注入された Clock を用いて現在時刻(UTC想定)を取得。
39 : // これを LastLoginAt / CreatedAt / UpdatedAt に用いる。
40 0 : now := u.clock.Now()
41 0 :
42 0 : // 以降で使う変数を先に宣言。
43 0 : var (
44 0 : isNew bool // 今回作成された新規ユーザーかどうか
45 0 : userEnt *User // 処理の最終的な対象ユーザー(作成 or 更新後)
46 0 : userID uint64 // auth_identities 側のFKに合わせた int64 のID
47 0 : )
48 0 :
49 0 : // --- 1) トランザクション境界 ------------------------------------------------
50 0 : // TxRunner.Do は ctx に *gorm.db(tx) を差し込み、repo が FromCtxOrDB(...) で
51 0 : // その tx を取得して同一トランザクションに乗れるようにする。
52 0 : if err := u.tx.Do(ctx, func(txCtx context.Context) error {
53 0 : // --- 2) users の upsert ------------------------------------------------
54 0 : // UID は Firebase プロジェクト内で一意。まず既存のユーザーを探す。
55 0 : found, err := u.users.FindByUID(txCtx, in.UID)
56 0 : if err != nil {
57 0 : // DBエラーはそのまま返し、Tx もロールバック。
58 0 : return fmt.Errorf("find user by uid: %w", err)
59 0 : }
60 :
61 0 : if found == nil {
62 0 : // 2-a) 見つからなければ新規作成
63 0 : isNew = true
64 0 : userEnt = &User{
65 0 : UID: in.UID,
66 0 : // util.Clone は *T をシャローコピー。nil 安全に上書きしたい時に使う。
67 0 : Email: util.Clone(in.Email),
68 0 : EmailVerified: in.EmailVerified,
69 0 : DisplayName: util.Clone(in.DisplayName),
70 0 : PhotoURL: util.Clone(in.PhotoURL),
71 0 : LastLoginAt: &now,
72 0 : CreatedAt: now,
73 0 : UpdatedAt: now,
74 0 : }
75 0 : newID, err := u.users.Create(txCtx, userEnt)
76 0 : if err != nil {
77 0 : // DB一意制約などは repo 側で整形 → ここで UC 語彙に正規化
78 0 : return mapInfraToUCError(fmt.Errorf("create user: %w", err))
79 0 : }
80 : // domain/entity.User は ID が uint64 の想定。FK用に int64 も保持。
81 0 : userEnt.ID = newID
82 0 : userID = newID
83 0 : } else {
84 0 : // 2-b) 見つかったのでプロフィール同期+最終ログイン更新
85 0 : found.Email = util.Clone(in.Email)
86 0 : found.EmailVerified = in.EmailVerified
87 0 : found.DisplayName = util.Clone(in.DisplayName)
88 0 : found.PhotoURL = util.Clone(in.PhotoURL)
89 0 : found.LastLoginAt = &now
90 0 : found.UpdatedAt = now
91 0 :
92 0 : // プロフィールのログイン時更新はリポジトリ側でカラム限定 Updates。
93 0 : if err := u.users.UpdateProfileOnLogin(txCtx, found); err != nil {
94 0 : return mapInfraToUCError(fmt.Errorf("update user on login: %w", err))
95 0 : }
96 0 : userEnt = found
97 0 : userID = found.ID
98 : }
99 :
100 : // --- 3) auth_identities の upsert -------------------------------------
101 : // Providers は 0..n を許容。password/anonymous なども来うる。
102 : // provider, provider_user_id の組で一意にし、ON CONFLICT DO UPDATE の想定。
103 0 : for _, p := range in.Providers {
104 0 : ident := &Identity{
105 0 : UserID: userID,
106 0 : Provider: p.Provider,
107 0 : ProviderUserID: p.ProviderUserID,
108 0 : ProviderDisplayName: p.ProviderDisplayName, // NOT NULL 前提。ハンドラでFallback済みを想定。
109 0 : EmailAtSignup: util.Clone(p.EmailAtSignup),
110 0 : CreatedAt: now, // 新規時採用。既存更新時は repo 実装が保持。
111 0 : }
112 0 : if _, err := u.idents.Upsert(txCtx, ident); err != nil {
113 0 : // (provider, provider_user_id) の重複やFK不整合などをUC語彙へマップ
114 0 : return mapInfraToUCError(fmt.Errorf("upsert identity (%s/%s): %w", p.Provider, p.ProviderUserID, err))
115 0 : }
116 : }
117 0 : return nil
118 0 : }); err != nil {
119 0 : // Tx 内で発生したエラーはここに伝播。HTTPハンドラは標準のエラーフォーマットで返す。
120 0 : return AuthResultOutput{}, err
121 0 : }
122 :
123 : // --- 4) 返却用のプロバイダ一覧を再読込 --------------------------------------
124 : // Tx外で読み出してよい(副作用なし)。一覧を DTO に詰め替えて返す。
125 0 : idents, err := u.idents.ListByUserID(ctx, userID)
126 0 : if err != nil {
127 0 : return AuthResultOutput{}, mapInfraToUCError(fmt.Errorf("list providers: %w", err))
128 0 : }
129 :
130 : // --- 5) 出力DTOを返却 ------------------------------------------------------
131 : // Token は今回は受け取った IDToken をそのまま返す想定。
132 0 : return AuthResultOutput{
133 0 : Token: in.IDToken,
134 0 : User: toUserOutput(userEnt, idents),
135 0 : IsNewUser: isNew,
136 0 : }, nil
137 : }
|