Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/actions/set-env/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ runs:
# 対象を増減するときはこの DEPLOY_TARGETS を編集する。
DEPLOY_TARGETS="
academic-api
admin-api
announcement-api
app-api
user-api
build-class-change-notifications-job
dispatch-notifications-job
Expand Down
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
set -eux; \
mkdir -p /out/bin; \
for cmd in academic-api announcement-api apply-table-privileges build-class-change-notifications-job dispatch-notifications-job migrate user-api; do \
for cmd in academic-api admin-api announcement-api app-api apply-table-privileges build-class-change-notifications-job dispatch-notifications-job migrate user-api; do \
CGO_ENABLED=0 GOOS=linux \
go build -tags timetzdata -trimpath -ldflags='-s -w' \
-o /out/bin/${cmd} ./cmd/${cmd}; \
Expand All @@ -29,6 +29,8 @@ COPY --from=builder /out/bin/ /bin/
# cmd/migrate は相対パス "migrations" で SQL を読むため、runtime image にも同梱する。
# WORKDIR は distroless のデフォルト "/" を前提に、"migrations" → "/migrations" に解決される。
COPY --from=builder /src/migrations/ /migrations/
# cmd/admin-api は相対パス "api/openapi/admin/openapi.yaml" で仕様を読むため、同様に同梱する。
COPY --from=builder /src/api/openapi/ /api/openapi/

USER nonroot:nonroot

Expand Down
70 changes: 70 additions & 0 deletions cmd/admin-api/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package main

import (
"context"
"log"

firebase "firebase.google.com/go/v4"
api "github.com/fun-dotto/server/gen/admin"
"github.com/fun-dotto/server/internal/modules/admin/handler"
"github.com/fun-dotto/server/internal/modules/admin/middleware"
"github.com/fun-dotto/server/internal/shared/apiclient"
"github.com/fun-dotto/server/internal/shared/server"
"github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/openapi3filter"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
ginmiddleware "github.com/oapi-codegen/gin-middleware"
)

func main() {
if err := godotenv.Load(); err != nil {
log.Printf("Warning: .env file not found: %v", err)
}

ctx := context.Background()
app, err := firebase.NewApp(ctx, nil)
if err != nil {
log.Fatalf("Failed to initialize Firebase App: %v", err)
}
authClient, err := app.Auth(ctx)
if err != nil {
log.Fatalf("Failed to get Firebase Auth client: %v", err)
}

// 仕様の原本は api/openapi/admin/openapi.yaml だけに置く。runtime image にも
// 同梱しているため、WORKDIR "/" 基準でこの相対パスに解決される。
spec, err := openapi3.NewLoader().LoadFromFile("api/openapi/admin/openapi.yaml")
if err != nil {
log.Fatalf("Failed to load OpenAPI spec: %v", err)
}

spec.Servers = nil

router := gin.Default()

router.Use(ginmiddleware.OapiRequestValidatorWithOptions(spec, &ginmiddleware.Options{
ErrorHandler: func(c *gin.Context, message string, statusCode int) {
if authStatusCode, authMessage, ok := middleware.GetAuthenticationError(c); ok {
c.AbortWithStatusJSON(authStatusCode, gin.H{"error": authMessage})
return
}
c.AbortWithStatusJSON(statusCode, gin.H{"error": message})
},
Options: openapi3filter.Options{
AuthenticationFunc: middleware.FirebaseAuthenticationFunc(authClient),
},
}))

clients, err := apiclient.NewExternalClients(ctx)
if err != nil {
log.Fatalf("Failed to initialize external clients: %v", err)
}

h := handler.NewHandler(clients.Academic, clients.Announcement, clients.Funch, clients.User)
api.RegisterHandlers(router, h)

if err := server.Run(router, ":8080"); err != nil {
log.Fatalf("Server exited with error: %v", err)
}
}
80 changes: 80 additions & 0 deletions cmd/app-api/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package main

import (
"context"
"log"

firebaseAdmin "firebase.google.com/go/v4"
api "github.com/fun-dotto/server/gen/app"
"github.com/fun-dotto/server/internal/modules/app/handler"
"github.com/fun-dotto/server/internal/modules/app/middleware"
"github.com/fun-dotto/server/internal/modules/app/repository"
"github.com/fun-dotto/server/internal/modules/app/service"
"github.com/fun-dotto/server/internal/shared/apiclient"
"github.com/fun-dotto/server/internal/shared/server"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
)

func main() {
if err := godotenv.Load(); err != nil {
log.Printf("Warning: .env file not found: %v", err)
}

ctx := context.Background()

// Firebase App Check の初期化
app, err := firebaseAdmin.NewApp(ctx, nil)
if err != nil {
log.Fatalf("error initializing Firebase app: %v\n", err)
}

appCheckClient, err := app.AppCheck(ctx)
if err != nil {
log.Fatalf("error initializing App Check client: %v\n", err)
}

authClient, err := app.Auth(ctx)
if err != nil {
log.Fatalf("error initializing Auth client: %v\n", err)
}

router := gin.Default()

// 外部APIクライアントを初期化
clients, err := apiclient.NewExternalClients(ctx)
if err != nil {
log.Fatalf("Failed to create external clients: %v", err)
}

announcementRepository := repository.NewAnnouncementRepository(clients.Announcement)
announcementService := service.NewAnnouncementService(announcementRepository)

academicRepository := repository.NewAcademicRepository(clients.Academic)
academicService := service.NewAcademicService(academicRepository)

userRepository := repository.NewUserRepository(clients.User)
userService := service.NewUserService(userRepository)

funchRepository := repository.NewFunchRepository(clients.Funch)
funchService := service.NewFunchService(funchRepository)

h := handler.NewHandler(
handler.WithAnnouncementService(announcementService),
handler.WithAcademicService(academicService),
handler.WithUserService(userService),
handler.WithFunchService(funchService),
)

strictHandler := api.NewStrictHandler(h, nil)
api.RegisterHandlersWithOptions(router, strictHandler, api.GinServerOptions{
Middlewares: []api.MiddlewareFunc{
api.MiddlewareFunc(middleware.AppCheckMiddleware(appCheckClient)),
api.MiddlewareFunc(middleware.AuthMiddleware(authClient)),
},
})

if err := server.Run(router, ":8080"); err != nil {
log.Fatalf("Server exited with error: %v", err)
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ require (
github.com/oapi-codegen/gin-middleware v1.0.2
github.com/oapi-codegen/runtime v1.4.0
github.com/stretchr/testify v1.11.1
google.golang.org/api v0.276.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.1
)
Expand Down Expand Up @@ -141,7 +142,6 @@ require (
golang.org/x/text v0.36.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.43.0 // indirect
google.golang.org/api v0.276.0 // indirect
google.golang.org/appengine/v2 v2.0.6 // indirect
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 // indirect
Expand Down
122 changes: 122 additions & 0 deletions internal/modules/admin/handler/announcement.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package handler

import (
"net/http"

"github.com/gin-gonic/gin"

announcement_api "github.com/fun-dotto/server/gen/announcement"
"github.com/fun-dotto/server/internal/modules/admin/middleware"
)

// AnnouncementsV1List 一覧を取得する
func (h *Handler) AnnouncementsV1List(c *gin.Context) {
if !middleware.RequireAnyClaim(c, "admin", "developer") {
return
}

response, err := h.announcementClient.AnnouncementsV1ListWithResponse(c.Request.Context(), nil)
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)
}

// AnnouncementsV1Detail 詳細を取得する
func (h *Handler) AnnouncementsV1Detail(c *gin.Context, id string) {
if !middleware.RequireAnyClaim(c, "admin", "developer") {
return
}

response, err := h.announcementClient.AnnouncementsV1DetailWithResponse(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)
}

// AnnouncementsV1Create 新規作成する
func (h *Handler) AnnouncementsV1Create(c *gin.Context) {
if !middleware.RequireAnyClaim(c, "admin", "developer") {
return
}

var req announcement_api.AnnouncementRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

response, err := h.announcementClient.AnnouncementsV1CreateWithResponse(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)
}

// AnnouncementsV1Delete 削除する
func (h *Handler) AnnouncementsV1Delete(c *gin.Context, id string) {
if !middleware.RequireAnyClaim(c, "admin", "developer") {
return
}

response, err := h.announcementClient.AnnouncementsV1DeleteWithResponse(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)
}

// AnnouncementsV1Update 更新する
func (h *Handler) AnnouncementsV1Update(c *gin.Context, id string) {
if !middleware.RequireAnyClaim(c, "admin", "developer") {
return
}

var req announcement_api.AnnouncementRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

response, err := h.announcementClient.AnnouncementsV1UpdateWithResponse(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)
}
Loading
Loading