Line data Source code
1 : // Package controller は フリーランサー情報のエンドポイント処理を担当します。
2 : package controller
3 :
4 : import (
5 : "errors"
6 : "fmt"
7 : "log/slog"
8 : "net/http"
9 : "strconv"
10 :
11 : "github.com/gin-gonic/gin"
12 : "github.com/go-playground/validator/v10"
13 :
14 : fba "resume/internal/adapter/gateway/firebase"
15 : "resume/internal/adapter/http/dto/request"
16 : "resume/internal/adapter/http/handlerutil"
17 : "resume/internal/adapter/http/presenter"
18 : "resume/internal/shared/apperr"
19 : "resume/internal/shared/ctx/auth"
20 : "resume/internal/usecase/profile"
21 : )
22 :
23 : // ProfileHandler は フリーランサー情報のハンドラです
24 : type ProfileHandler struct {
25 : fb fba.Auth
26 : uc profile.Usecase
27 : }
28 :
29 : // NewProfileHandler は ProfileHandler の新しいインスタンスを生成して返す
30 : // フリーランサー情報の HTTP リクエストを処理するためのハンドラを初期化します
31 : func NewProfileHandler(
32 : fb fba.Auth,
33 : uc profile.Usecase,
34 0 : ) *ProfileHandler {
35 0 : return &ProfileHandler{
36 0 : fb: fb,
37 0 : uc: uc,
38 0 : }
39 0 : }
40 :
41 : // Fba は認証済みユーザーのFirebaseから取得できる情報を返します。
42 : // コンテキストから認証クレームを取得し、メールアドレスとUID、UserIdをjson形式で返します
43 : // 多分表立っては使いません
44 0 : func (h *ProfileHandler) Fba(c *gin.Context) {
45 0 : cl, ok := auth.From(c.Request.Context())
46 0 : if !ok {
47 0 : handlerutil.WriteError(c,
48 0 : apperr.New(apperr.CodeUnauthorized, "unauthorized", nil))
49 0 : return
50 0 : }
51 0 : userID, ok := auth.UserID(c)
52 0 : if !ok || userID == 0 {
53 0 : handlerutil.WriteError(c, apperr.New(apperr.CodeUnauthorized, "unauthorized", nil))
54 0 : }
55 0 : c.JSON(http.StatusOK, gin.H{"uid": cl.UID, "email": cl.Email, "id": userID})
56 : }
57 :
58 : // Me は 認証済みユーザーの情報を返します
59 : // コンテキストから認証クレームを取得し、UIDとメールアドレスをJSON形式で応答します。
60 0 : func (h *ProfileHandler) Me(c *gin.Context) {
61 0 : userID, ok := auth.UserID(c)
62 0 : if !ok || userID == 0 {
63 0 : handlerutil.WriteError(c, apperr.New(apperr.CodeUnauthorized, "unauthorized", nil))
64 0 : }
65 0 : in := profile.UserInput{
66 0 : UserID: userID,
67 0 : }
68 0 : out, err := h.uc.GetUser(c.Request.Context(), in)
69 0 : if err != nil {
70 0 : handlerutil.WriteError(c, err)
71 0 : }
72 0 : c.JSON(http.StatusOK, out)
73 : }
74 :
75 : // Identities は認証ユーザに紐づく外部ID一覧をページングして返します。
76 : // クエリ: page(>=1), per_page(-1|1..100), sort(created_at|provider|uid|email_at_signup), order(asc|desc), q(任意)
77 0 : func (h *ProfileHandler) Identities(c *gin.Context) {
78 0 : userID, ok := auth.UserID(c)
79 0 : if !ok || userID == 0 {
80 0 : handlerutil.WriteError(c, apperr.New(apperr.CodeUnauthorized, "unauthorized", nil))
81 0 : return
82 0 : }
83 0 : var q request.ListIdentitiesQuery
84 0 : if err := c.ShouldBindQuery(&q); err != nil {
85 0 : // ▼ ここで必ず validation 用のユーティリティに流す
86 0 : var verrs validator.ValidationErrors
87 0 : if errors.As(err, &verrs) {
88 0 : slog.Info("validator.ValidationErrors hit", "errs", verrs)
89 0 : // scope は他と揃えて domain.user_identity にしておく
90 0 : env := handlerutil.BuildValidationEnvelope(c, verrs, "meta")
91 0 : handlerutil.WriteValidationError(c, env) // 422 + 新フォーマット
92 0 : return
93 0 : }
94 :
95 : // バリデーション以外の Bind エラーだけ通常の WriteError
96 0 : handlerutil.WriteError(c, err)
97 0 : return
98 : }
99 0 : q.Normalize()
100 0 :
101 0 : in := profile.UserIdentityInput{
102 0 : UserID: userID,
103 0 : Page: q.Page,
104 0 : PerPage: q.PerPage,
105 0 : Sort: q.Sort,
106 0 : SortOrder: q.Order,
107 0 : Query: q.Q,
108 0 : }
109 0 : //slog.Debug("uid=%d page=%d per=%d sort=%s order=%s q=%v", userID, page, perPage, col, order, q)
110 0 : out, err := h.uc.ListUserIdentity(c.Request.Context(), in)
111 0 : if err != nil {
112 0 : c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch identities"})
113 0 : }
114 0 : c.JSON(http.StatusOK, out)
115 : }
116 :
117 : // GetPersonalInfo は 現在ログイン中のユーザーに紐づくプロフィール情報を取得します
118 : // 成功時には HTTP 200 でプロフィール情報を JSON 形式で返します
119 : // 認証エラー時は HTTP 401、内部エラー時は HTTP 500 を返します
120 0 : func (h *ProfileHandler) GetPersonalInfo(c *gin.Context) {
121 0 : userID, ok := auth.UserID(c)
122 0 : if !ok || userID == 0 {
123 0 : handlerutil.WriteError(c, apperr.New(apperr.CodeUnauthorized, "unauthorized", nil))
124 0 : return
125 0 : }
126 0 : in := profile.UserInput{
127 0 : UserID: userID,
128 0 : }
129 0 : out, err := h.uc.GetUserProfile(c.Request.Context(), in)
130 0 : if err != nil {
131 0 : c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch user_profile"})
132 0 : }
133 0 : res := presenter.PresentUserProfile(out)
134 0 : c.JSON(http.StatusOK, res)
135 : }
136 :
137 : // PatchPersonalInfo は 現在ログイン中のユーザーに紐づくプロフィール情報を登録・更新します
138 : // 成功時には HTTP 200 で返します
139 0 : func (h *ProfileHandler) PatchPersonalInfo(c *gin.Context) {
140 0 : var req request.PatchProfileRequest
141 0 : if err := c.ShouldBindJSON(&req); err != nil {
142 0 : var verrs validator.ValidationErrors
143 0 : if errors.As(err, &verrs) {
144 0 : env := handlerutil.BuildValidationEnvelope(c, verrs, "domain.user_profile")
145 0 : handlerutil.WriteValidationError(c, env) // 422 + {"error": {...}}
146 0 : return
147 0 : }
148 0 : handlerutil.WriteError(c, err)
149 0 : return
150 : }
151 0 : req.Normalize()
152 0 : userID, ok := auth.UserID(c)
153 0 : if !ok || userID == 0 {
154 0 : handlerutil.WriteError(c, apperr.New(apperr.CodeUnauthorized, "unauthorized", nil))
155 0 : return
156 0 : }
157 0 : in := profile.PatchUserProfileInput{
158 0 : UserID: userID,
159 0 : FamilyName: req.FamilyName,
160 0 : GivenName: req.GivenName,
161 0 : FamilyNameKana: req.FamilyNameKana,
162 0 : GivenNameKana: req.GivenNameKana,
163 0 : BirthDate: req.BirthDate,
164 0 : GenderID: req.GenderID,
165 0 : Initial: req.Initial,
166 0 : }
167 0 : if err := h.uc.PatchUserProfile(c.Request.Context(), in); err != nil {
168 0 : handlerutil.WriteError(c, err)
169 0 : return
170 0 : }
171 0 : c.JSON(http.StatusOK, gin.H{"status": "ok"})
172 : }
173 :
174 : // HasPersonalInfo は 現在ログイン中のユーザーに紐づくプロフィール情報の有無を取得します
175 0 : func (h *ProfileHandler) HasPersonalInfo(c *gin.Context) {
176 0 : userID, ok := auth.UserID(c)
177 0 : if !ok || userID == 0 {
178 0 : handlerutil.WriteError(c, apperr.New(apperr.CodeUnauthorized, "unauthorized", nil))
179 0 : return
180 0 : }
181 0 : in := profile.UserInput{
182 0 : UserID: userID,
183 0 : }
184 0 : out, err := h.uc.HasUserProfile(c.Request.Context(), in)
185 0 : if err != nil {
186 0 : handlerutil.WriteError(c, err)
187 0 : return
188 0 : }
189 0 : c.JSON(http.StatusOK, out)
190 : }
191 :
192 : // HasUserAddress は 現在ログイン中のユーザーに紐づく住所情報の有無を取得します
193 0 : func (h *ProfileHandler) HasUserAddress(c *gin.Context) {
194 0 : userID, ok := auth.UserID(c)
195 0 : if !ok || userID == 0 {
196 0 : handlerutil.WriteError(c, apperr.New(apperr.CodeUnauthorized, "unauthorized", nil))
197 0 : return
198 0 : }
199 0 : in := profile.UserInput{
200 0 : UserID: userID,
201 0 : }
202 0 : out, err := h.uc.HasUserAddress(c.Request.Context(), in)
203 0 : if err != nil {
204 0 : handlerutil.WriteError(c, err)
205 0 : return
206 0 : }
207 0 : c.JSON(http.StatusOK, out)
208 : }
209 :
210 : // ListUserAddress は 現在ログイン中のユーザーに紐づく住所一覧を取得します。
211 : // クエリでページング・ソート・目的IDなどを指定できます。
212 0 : func (h *ProfileHandler) ListUserAddress(c *gin.Context) {
213 0 : userID, ok := auth.UserID(c)
214 0 : if !ok || userID == 0 {
215 0 : handlerutil.WriteError(c, apperr.New(apperr.CodeUnauthorized, "unauthorized", nil))
216 0 : return
217 0 : }
218 0 : var q request.ListUserAddressQuery
219 0 : if err := c.ShouldBindQuery(&q); err != nil {
220 0 : var verrs validator.ValidationErrors
221 0 : if errors.As(err, &verrs) {
222 0 : env := handlerutil.BuildValidationEnvelope(c, verrs, "meta")
223 0 : handlerutil.WriteValidationError(c, env) // 422 + 新フォーマット
224 0 : return
225 0 : }
226 0 : handlerutil.WriteError(c, err)
227 0 : return
228 : }
229 0 : q.Normalize()
230 0 :
231 0 : in := profile.ListUserAddressInput{
232 0 : UserID: userID,
233 0 : Page: q.Page,
234 0 : PerPage: q.PerPage,
235 0 : Sort: q.Sort,
236 0 : SortOrder: q.Order,
237 0 :
238 0 : PurposeID: q.PurposeID,
239 0 : }
240 0 : slog.Debug("handler", "in", in)
241 0 : out, err := h.uc.ListUserAddress(c.Request.Context(), in)
242 0 : if err != nil {
243 0 : c.JSON(http.StatusInternalServerError, gin.H{
244 0 : "error": "failed to fetch user address",
245 0 : })
246 0 : }
247 :
248 0 : c.JSON(http.StatusOK, out)
249 : }
250 :
251 : // CreateUserAddress は 認証済みユーザーが住所を登録する
252 0 : func (h *ProfileHandler) CreateUserAddress(c *gin.Context) {
253 0 : var req request.CreateAddressRequest
254 0 : if err := c.ShouldBindJSON(&req); err != nil {
255 0 : if verrs, ok := err.(validator.ValidationErrors); ok {
256 0 : env := handlerutil.BuildValidationEnvelope(c, verrs, "domain.address")
257 0 : handlerutil.WriteValidationError(c, env) // 422 + {"error": {...}}
258 0 : return
259 0 : }
260 0 : handlerutil.WriteError(c, err)
261 0 : return
262 : }
263 0 : userID, ok := auth.UserID(c)
264 0 : if !ok || userID == 0 {
265 0 : handlerutil.WriteError(c, apperr.New(apperr.CodeUnauthorized, "unauthorized", nil))
266 0 : return
267 0 : }
268 0 : in := profile.CreateUserAddressInput{
269 0 : UserID: userID,
270 0 : PurposeID: *req.PurposeID,
271 0 : IsPrimary: false,
272 0 : CountryCode: req.CountryCode,
273 0 : AdministrativeArea: req.AdministrativeArea,
274 0 : Locality: req.Locality,
275 0 : DependentLocality: req.DependentLocality,
276 0 : PostalCode: req.PostalCode,
277 0 : AddressLine1: req.AddressLine1,
278 0 : AddressLine2: req.AddressLine2,
279 0 : AddressLine3: req.AddressLine3,
280 0 : Latitude: req.Latitude,
281 0 : Longitude: req.Longitude,
282 0 : }
283 0 : out, err := h.uc.CreateUserAddress(c.Request.Context(), in)
284 0 : if err != nil {
285 0 : handlerutil.WriteError(c, err)
286 0 : return
287 0 : }
288 0 : res := presenter.ToAddressResponse(out.UserAddress)
289 0 :
290 0 : c.Header("location", fmt.Sprintf("/fl/address/%d", res.ID))
291 0 : c.JSON(http.StatusCreated, res)
292 : }
293 :
294 : // UpdateUserAddress は 現在ログイン中のユーザーに紐づく住所情報を更新します。
295 : // パスパラメータの address_id と JSON ボディの内容をもとに更新を行います。
296 0 : func (h *ProfileHandler) UpdateUserAddress(c *gin.Context) {
297 0 : // 1. パスパラメータから address_id を取得
298 0 : addressIDStr := c.Param("address_id")
299 0 :
300 0 : // 2. uint64 に変換
301 0 : addressID, err := strconv.ParseUint(addressIDStr, 10, 64)
302 0 : if err != nil {
303 0 : // 不正な ID → 400 Bad Request
304 0 : c.JSON(http.StatusBadRequest, gin.H{
305 0 : "error": "invalid address_id",
306 0 : })
307 0 : return
308 0 : }
309 0 : var req request.UpdateUserAddressRequest
310 0 : if err := c.ShouldBindJSON(&req); err != nil {
311 0 : var verrs validator.ValidationErrors
312 0 : if errors.As(err, &verrs) {
313 0 : env := handlerutil.BuildValidationEnvelope(c, verrs, "domain.address")
314 0 : handlerutil.WriteValidationError(c, env) // 422 + {"error": {...}}
315 0 : return
316 0 : }
317 0 : handlerutil.WriteError(c, err)
318 0 : return
319 : }
320 0 : userID, ok := auth.UserID(c)
321 0 : if !ok || userID == 0 {
322 0 : handlerutil.WriteError(c, apperr.New(apperr.CodeUnauthorized, "unauthorized", nil))
323 0 : return
324 0 : }
325 0 : in := profile.UpdateUserAddressInput{
326 0 : AddressID: addressID,
327 0 : CreateUserAddressInput: profile.CreateUserAddressInput{
328 0 : UserID: userID,
329 0 : PurposeID: *req.PurposeID,
330 0 : IsPrimary: false,
331 0 : CountryCode: req.CountryCode,
332 0 : AdministrativeArea: req.AdministrativeArea,
333 0 : Locality: req.Locality,
334 0 : DependentLocality: req.DependentLocality,
335 0 : PostalCode: req.PostalCode,
336 0 : AddressLine1: req.AddressLine1,
337 0 : AddressLine2: req.AddressLine2,
338 0 : AddressLine3: req.AddressLine3,
339 0 : Latitude: req.Latitude,
340 0 : Longitude: req.Longitude,
341 0 : },
342 0 : }
343 0 : out, err := h.uc.UpdateUserAddress(c.Request.Context(), in)
344 0 : if err != nil {
345 0 : handlerutil.WriteError(c, err)
346 0 : return
347 0 : }
348 0 : res := presenter.ToAddressResponse(out.UserAddress)
349 0 : c.Header("location", fmt.Sprintf("/fl/address/%d", res.ID))
350 0 : c.JSON(http.StatusOK, res)
351 : }
352 :
353 : // DeleteUserAddress は 現在ログイン中のユーザーに紐付く住所を削除します
354 : // パスパラメータとfirebaseから求められたUserIDを元に削除を行います
355 0 : func (h *ProfileHandler) DeleteUserAddress(c *gin.Context) {
356 0 : addressIGStr := c.Param("address_id")
357 0 :
358 0 : addressID, err := strconv.ParseUint(addressIGStr, 10, 64)
359 0 : if err != nil {
360 0 : c.JSON(http.StatusBadRequest, gin.H{
361 0 : "error": "invalid address_id",
362 0 : })
363 0 : return
364 0 : }
365 0 : userID, ok := auth.UserID(c)
366 0 : if !ok || userID == 0 {
367 0 : handlerutil.WriteError(c, apperr.New(apperr.CodeUnprocessable, "unauthorized", nil))
368 0 : return
369 0 : }
370 :
371 0 : in := profile.DeleteUserAddressInput{AddressID: addressID, UserID: userID}
372 0 : _, err = h.uc.DeleteUserAddress(c.Request.Context(), in)
373 0 : if err != nil {
374 0 : handlerutil.WriteError(c, err)
375 0 : return
376 0 : }
377 :
378 : // ★ REST 的にもっとも自然なレスポンス
379 0 : c.Status(http.StatusNoContent)
380 : }
381 :
382 : // DetailUserAddress は 認証済みユーザーの住所を取得します
383 0 : func (h *ProfileHandler) DetailUserAddress(c *gin.Context) {
384 0 : addressIDStg := c.Param("address_id")
385 0 :
386 0 : addressID, err := strconv.ParseUint(addressIDStg, 10, 64)
387 0 : if err != nil {
388 0 : c.JSON(http.StatusBadRequest, gin.H{
389 0 : "error": "invalid address id",
390 0 : })
391 0 : return
392 0 : }
393 0 : userID, ok := auth.UserID(c)
394 0 : if !ok || userID == 0 {
395 0 : handlerutil.WriteError(c, apperr.New(apperr.CodeUnprocessable, "unauthorized", nil))
396 0 : return
397 0 : }
398 0 : in := profile.DetailUserAddressInput{
399 0 : AddressID: addressID,
400 0 : UserID: userID,
401 0 : }
402 0 : out, err := h.uc.DetailUserAddress(c.Request.Context(), in)
403 0 : if err != nil {
404 0 : handlerutil.WriteError(c, err)
405 0 : return
406 0 : }
407 0 : res := presenter.ToAddressResponse(out.UserAddress)
408 0 : c.Header("location", fmt.Sprintf("/fl/address/%d", res.ID))
409 0 : c.JSON(http.StatusOK, res)
410 : }
|