diff --git a/cmd/server/main.go b/cmd/server/main.go index 9d21aa4..038ab01 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -47,7 +47,7 @@ func main() { log.Fatalf("Failed to initialize external clients: %v", err) } - h := handler.NewHandler(clients.Academic, clients.Announcement) + h := handler.NewHandler(clients.Academic, clients.Announcement, clients.User) api.RegisterHandlers(router, h) addr := ":8080" diff --git a/internal/handler/course_registration.go b/internal/handler/course_registration.go new file mode 100644 index 0000000..99ac97b --- /dev/null +++ b/internal/handler/course_registration.go @@ -0,0 +1,85 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + api "github.com/fun-dotto/admin-bff-api/generated" + "github.com/fun-dotto/admin-bff-api/generated/external/academic_api" + "github.com/fun-dotto/admin-bff-api/internal/middleware" +) + +// CourseRegistrationsV1List 履修情報を取得する +func (h *Handler) CourseRegistrationsV1List(c *gin.Context, params api.CourseRegistrationsV1ListParams) { + if !middleware.RequireAnyClaim(c, "admin", "developer") { + return + } + + response, err := h.academicClient.CourseRegistrationsV1ListWithResponse(c.Request.Context(), &academic_api.CourseRegistrationsV1ListParams{ + UserId: params.UserId, + Year: params.Year, + Semesters: convertSlice[api.DottoFoundationV1CourseSemester, academic_api.DottoFoundationV1CourseSemester](params.Semesters), + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if response.JSON200 == nil { + c.JSON(response.StatusCode(), gin.H{"error": "unexpected response from upstream"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "registrations": response.JSON200.CourseRegistrations, + }) +} + +// CourseRegistrationsV1Create 履修情報を作成する +func (h *Handler) CourseRegistrationsV1Create(c *gin.Context) { + if !middleware.RequireAnyClaim(c, "admin", "developer") { + return + } + + var req academic_api.CourseRegistrationRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + response, err := h.academicClient.CourseRegistrationsV1CreateWithResponse(c.Request.Context(), req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if response.JSON201 == nil { + c.JSON(response.StatusCode(), gin.H{"error": "unexpected response from upstream"}) + return + } + + c.JSON(http.StatusCreated, gin.H{ + "registration": response.JSON201.CourseRegistration, + }) +} + +// CourseRegistrationsV1Delete 履修情報を削除する +func (h *Handler) CourseRegistrationsV1Delete(c *gin.Context, id string) { + if !middleware.RequireAnyClaim(c, "admin", "developer") { + return + } + + response, err := h.academicClient.CourseRegistrationsV1DeleteWithResponse(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if response.StatusCode() != http.StatusNoContent { + c.JSON(response.StatusCode(), gin.H{"error": "unexpected response from upstream"}) + return + } + + c.Status(http.StatusNoContent) +} diff --git a/internal/handler/course_registration_test.go b/internal/handler/course_registration_test.go new file mode 100644 index 0000000..7c5b8f7 --- /dev/null +++ b/internal/handler/course_registration_test.go @@ -0,0 +1,56 @@ +package handler + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/fun-dotto/admin-bff-api/generated/external/academic_api" + "github.com/gin-gonic/gin" +) + +func TestCourseRegistrationsV1Create_ProxiesAcademicAPI(t *testing.T) { + gin.SetMode(gin.TestMode) + + var gotMethod string + var gotPath string + var gotBody academic_api.CourseRegistrationRequest + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotMethod = r.Method + gotPath = r.URL.Path + + if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil { + t.Fatalf("decode request body: %v", err) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _, _ = w.Write([]byte(`{"courseRegistration":{"id":"reg-1","userId":"user-1","subject":{"id":"subject-1","name":"Algorithms","faculties":[]}}}`)) + })) + defer server.Close() + + h := newTestHandler(t, server.URL) + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Request = httptest.NewRequest( + http.MethodPost, + "/v1/courseRegistrations", + bytes.NewBufferString(`{"subjectId":"subject-1","userId":"user-1"}`), + ) + c.Request.Header.Set("Content-Type", "application/json") + setAdminClaim(c) + + h.CourseRegistrationsV1Create(c) + + if rec.Code != http.StatusCreated { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusCreated) + } + if gotMethod != http.MethodPost || gotPath != "/v1/courseRegistrations" { + t.Fatalf("method/path = %s %s", gotMethod, gotPath) + } + if gotBody.SubjectId != "subject-1" || gotBody.UserId != "user-1" { + t.Fatalf("unexpected upstream request body: %+v", gotBody) + } +} diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 26c4929..37d0a9c 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -4,16 +4,19 @@ import ( api "github.com/fun-dotto/admin-bff-api/generated" "github.com/fun-dotto/admin-bff-api/generated/external/academic_api" "github.com/fun-dotto/admin-bff-api/generated/external/announcement_api" + "github.com/fun-dotto/admin-bff-api/generated/external/user_api" ) type Handler struct { academicClient *academic_api.ClientWithResponses announcementClient *announcement_api.ClientWithResponses + userClient *user_api.ClientWithResponses } func NewHandler( academicClient *academic_api.ClientWithResponses, announcementClient *announcement_api.ClientWithResponses, + userClient *user_api.ClientWithResponses, ) *Handler { if academicClient == nil { panic("academicClient is required") @@ -21,9 +24,13 @@ func NewHandler( if announcementClient == nil { panic("announcementClient is required") } + if userClient == nil { + panic("userClient is required") + } return &Handler{ academicClient: academicClient, announcementClient: announcementClient, + userClient: userClient, } } diff --git a/internal/handler/personal_calendar_item_test.go b/internal/handler/personal_calendar_item_test.go new file mode 100644 index 0000000..7309b50 --- /dev/null +++ b/internal/handler/personal_calendar_item_test.go @@ -0,0 +1,58 @@ +package handler + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + api "github.com/fun-dotto/admin-bff-api/generated" + "github.com/gin-gonic/gin" +) + +func TestPersonalCalendarItemsV1List_ProxiesAcademicAPI(t *testing.T) { + gin.SetMode(gin.TestMode) + + wantDate := time.Date(2026, 3, 26, 9, 0, 0, 0, time.UTC) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + if got := query.Get("userId"); got != "user-1" { + t.Fatalf("userId = %q, want %q", got, "user-1") + } + if got := query.Get("dates"); got == "" { + t.Fatal("dates query parameter is empty") + } + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"personalCalendarItems":[{"date":"2026-03-26T09:00:00Z","slot":{"dayOfWeek":"Thursday","period":"first"},"timetableItem":{"id":"item-1","subject":{"id":"subject-1","name":"Algorithms"},"slot":{"dayOfWeek":"Thursday","period":"first"},"rooms":[]}}]}`)) + })) + defer server.Close() + + h := newTestHandler(t, server.URL) + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Request = httptest.NewRequest(http.MethodGet, "/v1/personalCalendarItems", nil) + setAdminClaim(c) + + h.PersonalCalendarItemsV1List(c, api.PersonalCalendarItemsV1ListParams{ + UserId: "user-1", + Dates: []time.Time{wantDate}, + }) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) + } + + var body struct { + PersonalCalendarItems []struct { + Date string `json:"date"` + } `json:"personalCalendarItems"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("unmarshal response: %v", err) + } + if len(body.PersonalCalendarItems) != 1 || body.PersonalCalendarItems[0].Date != "2026-03-26T09:00:00Z" { + t.Fatalf("unexpected response body: %s", rec.Body.String()) + } +} diff --git a/internal/handler/room.go b/internal/handler/room.go new file mode 100644 index 0000000..eeb3fa0 --- /dev/null +++ b/internal/handler/room.go @@ -0,0 +1,148 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + api "github.com/fun-dotto/admin-bff-api/generated" + "github.com/fun-dotto/admin-bff-api/generated/external/academic_api" + "github.com/fun-dotto/admin-bff-api/internal/middleware" +) + +// RoomsV1List 教室一覧を取得する +func (h *Handler) RoomsV1List(c *gin.Context, params api.RoomsV1ListParams) { + if !middleware.RequireAnyClaim(c, "admin", "developer") { + return + } + + response, err := h.academicClient.RoomsV1ListWithResponse(c.Request.Context(), &academic_api.RoomsV1ListParams{ + Floor: convertSlicePtr[api.DottoFoundationV1Floor, academic_api.DottoFoundationV1Floor](params.Floor), + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if response.JSON200 == nil { + c.JSON(response.StatusCode(), gin.H{"error": "unexpected response from upstream"}) + return + } + + c.JSON(http.StatusOK, response.JSON200) +} + +// RoomsV1Create 教室を作成する +func (h *Handler) RoomsV1Create(c *gin.Context) { + if !middleware.RequireAnyClaim(c, "admin", "developer") { + return + } + + var req academic_api.RoomRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + response, err := h.academicClient.RoomsV1CreateWithResponse(c.Request.Context(), req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if response.JSON201 == nil { + c.JSON(response.StatusCode(), gin.H{"error": "unexpected response from upstream"}) + return + } + + c.JSON(http.StatusCreated, response.JSON201) +} + +// RoomsV1Detail 教室を詳細取得する +func (h *Handler) RoomsV1Detail(c *gin.Context, id string) { + if !middleware.RequireAnyClaim(c, "admin", "developer") { + return + } + + response, err := h.academicClient.RoomsV1DetailWithResponse(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if response.JSON200 == nil { + c.JSON(response.StatusCode(), gin.H{"error": "unexpected response from upstream"}) + return + } + + c.JSON(http.StatusOK, response.JSON200) +} + +// RoomsV1Update 教室を更新する +func (h *Handler) RoomsV1Update(c *gin.Context, id string) { + if !middleware.RequireAnyClaim(c, "admin", "developer") { + return + } + + var req academic_api.RoomRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + response, err := h.academicClient.RoomsV1UpdateWithResponse(c.Request.Context(), id, req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if response.JSON200 == nil { + c.JSON(response.StatusCode(), gin.H{"error": "unexpected response from upstream"}) + return + } + + c.JSON(http.StatusOK, response.JSON200) +} + +// RoomsV1Delete 教室を削除する +func (h *Handler) RoomsV1Delete(c *gin.Context, id string) { + if !middleware.RequireAnyClaim(c, "admin", "developer") { + return + } + + response, err := h.academicClient.RoomsV1DeleteWithResponse(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if response.StatusCode() != http.StatusNoContent { + c.JSON(response.StatusCode(), gin.H{"error": "unexpected response from upstream"}) + return + } + + c.Status(http.StatusNoContent) +} + +// ReservationsV1List 教室の予約一覧を取得する +func (h *Handler) ReservationsV1List(c *gin.Context, id string, params api.ReservationsV1ListParams) { + if !middleware.RequireAnyClaim(c, "admin", "developer") { + return + } + + response, err := h.academicClient.ReservationsV1ListWithResponse(c.Request.Context(), id, &academic_api.ReservationsV1ListParams{ + From: params.From, + Until: params.Until, + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if response.JSON200 == nil { + c.JSON(response.StatusCode(), gin.H{"error": "unexpected response from upstream"}) + return + } + + c.JSON(http.StatusOK, response.JSON200) +} diff --git a/internal/handler/stub.go b/internal/handler/stub.go deleted file mode 100644 index ae96737..0000000 --- a/internal/handler/stub.go +++ /dev/null @@ -1,74 +0,0 @@ -package handler - -import ( - "net/http" - - "github.com/gin-gonic/gin" - - api "github.com/fun-dotto/admin-bff-api/generated" -) - -// CourseRegistrationsV1List 履修情報を取得する -func (h *Handler) CourseRegistrationsV1List(c *gin.Context, params api.CourseRegistrationsV1ListParams) { - c.JSON(http.StatusNotImplemented, gin.H{"error": "not implemented"}) -} - -// CourseRegistrationsV1Create 履修情報を作成する -func (h *Handler) CourseRegistrationsV1Create(c *gin.Context) { - c.JSON(http.StatusNotImplemented, gin.H{"error": "not implemented"}) -} - -// CourseRegistrationsV1Delete 履修情報を削除する -func (h *Handler) CourseRegistrationsV1Delete(c *gin.Context, id string) { - c.JSON(http.StatusNotImplemented, gin.H{"error": "not implemented"}) -} - -// RoomsV1List 教室一覧を取得する -func (h *Handler) RoomsV1List(c *gin.Context, params api.RoomsV1ListParams) { - c.JSON(http.StatusNotImplemented, gin.H{"error": "not implemented"}) -} - -// RoomsV1Create 教室を作成する -func (h *Handler) RoomsV1Create(c *gin.Context) { - c.JSON(http.StatusNotImplemented, gin.H{"error": "not implemented"}) -} - -// RoomsV1Detail 教室を詳細取得する -func (h *Handler) RoomsV1Detail(c *gin.Context, id string) { - c.JSON(http.StatusNotImplemented, gin.H{"error": "not implemented"}) -} - -// RoomsV1Update 教室を更新する -func (h *Handler) RoomsV1Update(c *gin.Context, id string) { - c.JSON(http.StatusNotImplemented, gin.H{"error": "not implemented"}) -} - -// RoomsV1Delete 教室を削除する -func (h *Handler) RoomsV1Delete(c *gin.Context, id string) { - c.JSON(http.StatusNotImplemented, gin.H{"error": "not implemented"}) -} - -// ReservationsV1List 教室の予約一覧を取得する -func (h *Handler) ReservationsV1List(c *gin.Context, id string, params api.ReservationsV1ListParams) { - c.JSON(http.StatusNotImplemented, gin.H{"error": "not implemented"}) -} - -// TimetableItemsV1List 時間割を取得する -func (h *Handler) TimetableItemsV1List(c *gin.Context, params api.TimetableItemsV1ListParams) { - c.JSON(http.StatusNotImplemented, gin.H{"error": "not implemented"}) -} - -// TimetableItemsV1Create 時間割に追加する -func (h *Handler) TimetableItemsV1Create(c *gin.Context) { - c.JSON(http.StatusNotImplemented, gin.H{"error": "not implemented"}) -} - -// TimetableItemsV1Delete 時間割を削除する -func (h *Handler) TimetableItemsV1Delete(c *gin.Context, id string) { - c.JSON(http.StatusNotImplemented, gin.H{"error": "not implemented"}) -} - -// UsersV1Detail ユーザーを取得する -func (h *Handler) UsersV1Detail(c *gin.Context, id string) { - c.JSON(http.StatusNotImplemented, gin.H{"error": "not implemented"}) -} diff --git a/internal/handler/handler_test.go b/internal/handler/subject_test.go similarity index 53% rename from internal/handler/handler_test.go rename to internal/handler/subject_test.go index 6ff28e9..c38ecec 100644 --- a/internal/handler/handler_test.go +++ b/internal/handler/subject_test.go @@ -1,18 +1,12 @@ package handler import ( - "encoding/json" "net/http" "net/http/httptest" "net/url" "testing" - "time" - firebaseauth "firebase.google.com/go/v4/auth" api "github.com/fun-dotto/admin-bff-api/generated" - "github.com/fun-dotto/admin-bff-api/generated/external/academic_api" - "github.com/fun-dotto/admin-bff-api/generated/external/announcement_api" - "github.com/fun-dotto/admin-bff-api/internal/middleware" "github.com/gin-gonic/gin" ) @@ -77,72 +71,3 @@ func TestSubjectsV1List_UsesRenamedQueryParameters(t *testing.T) { t.Fatalf("query culturalSubjectCategories = %q, culturalSubjectCategory = %q", gotQuery.Get("culturalSubjectCategories"), gotQuery.Get("culturalSubjectCategory")) } } - -func TestPersonalCalendarItemsV1List_ProxiesAcademicAPI(t *testing.T) { - gin.SetMode(gin.TestMode) - - wantDate := time.Date(2026, 3, 26, 9, 0, 0, 0, time.UTC) - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - query := r.URL.Query() - if got := query.Get("userId"); got != "user-1" { - t.Fatalf("userId = %q, want %q", got, "user-1") - } - if got := query.Get("dates"); got == "" { - t.Fatal("dates query parameter is empty") - } - - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"personalCalendarItems":[{"date":"2026-03-26T09:00:00Z","slot":{"dayOfWeek":"Thursday","period":"first"},"timetableItem":{"id":"item-1","subject":{"id":"subject-1","name":"Algorithms"},"slot":{"dayOfWeek":"Thursday","period":"first"},"rooms":[]}}]}`)) - })) - defer server.Close() - - h := newTestHandler(t, server.URL) - rec := httptest.NewRecorder() - c, _ := gin.CreateTestContext(rec) - c.Request = httptest.NewRequest(http.MethodGet, "/v1/personalCalendarItems", nil) - setAdminClaim(c) - - h.PersonalCalendarItemsV1List(c, api.PersonalCalendarItemsV1ListParams{ - UserId: "user-1", - Dates: []time.Time{wantDate}, - }) - - if rec.Code != http.StatusOK { - t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) - } - - var body struct { - PersonalCalendarItems []struct { - Date string `json:"date"` - } `json:"personalCalendarItems"` - } - if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { - t.Fatalf("unmarshal response: %v", err) - } - if len(body.PersonalCalendarItems) != 1 || body.PersonalCalendarItems[0].Date != "2026-03-26T09:00:00Z" { - t.Fatalf("unexpected response body: %s", rec.Body.String()) - } -} - -func newTestHandler(t *testing.T, baseURL string) *Handler { - t.Helper() - - academicClient, err := academic_api.NewClientWithResponses(baseURL) - if err != nil { - t.Fatalf("new academic client: %v", err) - } - announcementClient, err := announcement_api.NewClientWithResponses(baseURL) - if err != nil { - t.Fatalf("new announcement client: %v", err) - } - - return NewHandler(academicClient, announcementClient) -} - -func setAdminClaim(c *gin.Context) { - c.Set(middleware.FirebaseTokenContextKey, &firebaseauth.Token{ - Claims: map[string]interface{}{ - "admin": true, - }, - }) -} diff --git a/internal/handler/test_helper_test.go b/internal/handler/test_helper_test.go new file mode 100644 index 0000000..fb1f6c2 --- /dev/null +++ b/internal/handler/test_helper_test.go @@ -0,0 +1,39 @@ +package handler + +import ( + "testing" + + firebaseauth "firebase.google.com/go/v4/auth" + "github.com/fun-dotto/admin-bff-api/generated/external/academic_api" + "github.com/fun-dotto/admin-bff-api/generated/external/announcement_api" + "github.com/fun-dotto/admin-bff-api/generated/external/user_api" + "github.com/fun-dotto/admin-bff-api/internal/middleware" + "github.com/gin-gonic/gin" +) + +func newTestHandler(t *testing.T, baseURL string) *Handler { + t.Helper() + + academicClient, err := academic_api.NewClientWithResponses(baseURL) + if err != nil { + t.Fatalf("new academic client: %v", err) + } + announcementClient, err := announcement_api.NewClientWithResponses(baseURL) + if err != nil { + t.Fatalf("new announcement client: %v", err) + } + userClient, err := user_api.NewClientWithResponses(baseURL) + if err != nil { + t.Fatalf("new user client: %v", err) + } + + return NewHandler(academicClient, announcementClient, userClient) +} + +func setAdminClaim(c *gin.Context) { + c.Set(middleware.FirebaseTokenContextKey, &firebaseauth.Token{ + Claims: map[string]interface{}{ + "admin": true, + }, + }) +} diff --git a/internal/handler/timetable_item.go b/internal/handler/timetable_item.go new file mode 100644 index 0000000..ee873d0 --- /dev/null +++ b/internal/handler/timetable_item.go @@ -0,0 +1,92 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + api "github.com/fun-dotto/admin-bff-api/generated" + "github.com/fun-dotto/admin-bff-api/generated/external/academic_api" + "github.com/fun-dotto/admin-bff-api/internal/middleware" +) + +// TimetableItemsV1List 時間割を取得する +func (h *Handler) TimetableItemsV1List(c *gin.Context, params api.TimetableItemsV1ListParams) { + if !middleware.RequireAnyClaim(c, "admin", "developer") { + return + } + + response, err := h.academicClient.TimetableItemsV1ListWithResponse(c.Request.Context(), &academic_api.TimetableItemsV1ListParams{ + Year: params.Year, + Semesters: convertSlice[api.DottoFoundationV1CourseSemester, academic_api.DottoFoundationV1CourseSemester](params.Semesters), + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if response.JSON200 == nil { + c.JSON(response.StatusCode(), gin.H{"error": "unexpected response from upstream"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "items": response.JSON200.TimetableItems, + }) +} + +// TimetableItemsV1Create 時間割に追加する +func (h *Handler) TimetableItemsV1Create(c *gin.Context) { + if !middleware.RequireAnyClaim(c, "admin", "developer") { + return + } + + var req academic_api.TimetableItemRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + response, err := h.academicClient.TimetableItemsV1CreateWithResponse(c.Request.Context(), req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if response.JSON201 == nil { + c.JSON(response.StatusCode(), gin.H{"error": "unexpected response from upstream"}) + return + } + + c.JSON(http.StatusCreated, gin.H{ + "item": response.JSON201.TimetableItem, + }) +} + +// TimetableItemsV1Delete 時間割を削除する +func (h *Handler) TimetableItemsV1Delete(c *gin.Context, id string) { + if !middleware.RequireAnyClaim(c, "admin", "developer") { + return + } + + response, err := h.academicClient.TimetableItemsV1DeleteWithResponse(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if response.StatusCode() != http.StatusNoContent { + c.JSON(response.StatusCode(), gin.H{"error": "unexpected response from upstream"}) + return + } + + c.Status(http.StatusNoContent) +} + +func convertSlice[From, To ~string](src []From) []To { + result := make([]To, len(src)) + for i, v := range src { + result[i] = To(v) + } + return result +} diff --git a/internal/handler/user.go b/internal/handler/user.go new file mode 100644 index 0000000..6109ae1 --- /dev/null +++ b/internal/handler/user.go @@ -0,0 +1,29 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + "github.com/fun-dotto/admin-bff-api/internal/middleware" +) + +// UsersV1Detail ユーザーを取得する +func (h *Handler) UsersV1Detail(c *gin.Context, id string) { + if !middleware.RequireAnyClaim(c, "admin", "developer") { + return + } + + response, err := h.userClient.UsersV1DetailWithResponse(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if response.JSON200 == nil { + c.JSON(response.StatusCode(), gin.H{"error": "unexpected response from upstream"}) + return + } + + c.JSON(http.StatusOK, response.JSON200) +} diff --git a/internal/handler/user_test.go b/internal/handler/user_test.go new file mode 100644 index 0000000..231d8b9 --- /dev/null +++ b/internal/handler/user_test.go @@ -0,0 +1,49 @@ +package handler + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" +) + +func TestUsersV1Detail_ProxiesUserAPI(t *testing.T) { + gin.SetMode(gin.TestMode) + + var gotPath string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotPath = r.URL.Path + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"user":{"id":"user-1","name":"Jane Doe","email":"jane@example.com"}}`)) + })) + defer server.Close() + + h := newTestHandler(t, server.URL) + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Request = httptest.NewRequest(http.MethodGet, "/v1/users/user-1", nil) + setAdminClaim(c) + + h.UsersV1Detail(c, "user-1") + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) + } + if gotPath != "/v1/users/user-1" { + t.Fatalf("path = %q, want %q", gotPath, "/v1/users/user-1") + } + + var body struct { + User struct { + Id string `json:"id"` + } `json:"user"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("unmarshal response: %v", err) + } + if body.User.Id != "user-1" { + t.Fatalf("unexpected response body: %s", rec.Body.String()) + } +} diff --git a/internal/infrastructure/clients.go b/internal/infrastructure/clients.go index ebd360b..cc6d884 100644 --- a/internal/infrastructure/clients.go +++ b/internal/infrastructure/clients.go @@ -9,6 +9,7 @@ import ( "github.com/fun-dotto/admin-bff-api/generated/external/academic_api" "github.com/fun-dotto/admin-bff-api/generated/external/announcement_api" + "github.com/fun-dotto/admin-bff-api/generated/external/user_api" "google.golang.org/api/idtoken" ) @@ -18,6 +19,7 @@ const httpClientTimeout = 30 * time.Second type ExternalClients struct { Academic *academic_api.ClientWithResponses Announcement *announcement_api.ClientWithResponses + User *user_api.ClientWithResponses } // NewExternalClients 全ての外部APIクライアントを初期化 @@ -32,9 +34,15 @@ func NewExternalClients(ctx context.Context) (*ExternalClients, error) { return nil, fmt.Errorf("announcement client: %w", err) } + user, err := newUserClient(ctx) + if err != nil { + return nil, fmt.Errorf("user client: %w", err) + } + return &ExternalClients{ Academic: academic, Announcement: announcement, + User: user, }, nil } @@ -72,6 +80,23 @@ func newAnnouncementClient(ctx context.Context) (*announcement_api.ClientWithRes ) } +func newUserClient(ctx context.Context) (*user_api.ClientWithResponses, error) { + url := os.Getenv("USER_API_URL") + if url == "" { + return nil, fmt.Errorf("USER_API_URL is required") + } + + authClient, err := newAuthHTTPClient(ctx, url) + if err != nil { + return nil, err + } + + return user_api.NewClientWithResponses( + url, + user_api.WithHTTPClient(authClient), + ) +} + // newAuthHTTPClient Google Cloud認証付きHTTPクライアントを作成 func newAuthHTTPClient(ctx context.Context, targetURL string) (*http.Client, error) { client, err := idtoken.NewClient(ctx, targetURL)