Skip to content

Client の CRUD API の作成#52

Open
anko9801 wants to merge 5 commits intomainfrom
feat/client-crud
Open

Client の CRUD API の作成#52
anko9801 wants to merge 5 commits intomainfrom
feat/client-crud

Conversation

@anko9801
Copy link
Collaborator

@anko9801 anko9801 commented Jan 25, 2026

OAuth2/OIDC における Client の CRUD を /api/v1/admin/client/{clientId} のパスで実装しました。

@anko9801 anko9801 requested a review from Pugma January 25, 2026 15:57
@anko9801 anko9801 changed the title feat: Client CRUD API Client の CRUD API の作成 Jan 25, 2026
Copy link
Collaborator

@saitenntaisei saitenntaisei left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Collaborator

@Pugma Pugma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

確認しました! 実装ありがとうございます
また、レビュー遅くなってすみません 🙇
テストでもう少しエラーに関するケースも追加したほうがいいのかなという印象はありましたが、概ね実装は問題なさそうです 👀

一点だけ、軽く確認してもらえればいいかなという感じです

Comment on lines +25 to +28
type ClientWithSecret struct {
Client
ClientSecret string
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

直前の Client 構造体はアプリで管理する情報 (domain) として用意されていて、現に他のところでも使われていると思います

ただ ClientWithSecret は、現状は Client 登録時の response としてしか使われておらず、また domain として ClientSecret 要素がそのまま string で平文として保管されていそうな印象もある気がします

個人的には

  • この構造体を router 層に移動する
  • ClientSecret を domain として持つときは CryptedClientSecret とか、暗号化がされていることを明示できる命名にする

ほうが、読んだときに意図がわかりやすい気がします!

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

OAuth2/OIDC の Client 管理(CRUD + シークレット再生成)を admin API として追加し、OpenAPI 定義・ルーティング・永続化・テストまで一通り実装する PR です。

Changes:

  • /api/v1/admin/clients 配下に Client の CRUD とシークレット再生成 API を追加(OpenAPI/生成コード/ハンドラ)
  • Client のドメイン・リポジトリ・ユースケースを追加し、MariaDB(sqlc) 永続化を実装
  • ユースケース単体テストと DB を使った統合テストを追加

Reviewed changes

Copilot reviewed 18 out of 19 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
internal/usecase/client.go Client CRUD/secret 再生成のユースケースを追加
internal/usecase/client_test.go ユースケースの単体テスト(インメモリ mock repo)を追加
internal/router/v1/handler.go v1 ハンドラの DI コンテナを追加
internal/router/v1/client.go Client CRUD/secret API の Echo ハンドラ実装を追加
internal/router/v1/client_test.go MariaDB を使った admin client API の統合テストを追加
internal/router/v1/gen/server.go OpenAPI から生成された server ラッパ/ルーティング/strict ハンドラを追加
internal/router/v1/gen/models.go OpenAPI から生成された Client モデル群を追加
internal/domain/client.go Client/ClientType のドメインモデルを追加
internal/repository/client.go clients テーブル向けリポジトリ実装を追加
internal/repository/oidc/querier.go sqlc Querier に DeleteAllClients を追加
internal/repository/oidc/oidc.sql.go DeleteAllClients の生成コードを追加
internal/repository/oidc/db.go DeleteAllClients の prepared statement を追加
internal/testutil/root.go go.mod を辿ってプロジェクトルートを探すテストユーティリティを追加
api/openapi.yaml Client 管理 API とスキーマ定義を追加
db/schema.sql テーブル作成を IF NOT EXISTS に変更(テスト/初期化向け)
db/query/oidc.sql DeleteAllClients を追加(sqlc 入力)
cmd/serve.go Client 用の repo/usecase/handler を組み立ててルート登録
go.mod uuid/x-crypto 依存追加
go.sum 依存追加に伴う更新

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +29 to +33
func (h *Handler) CreateClient(ctx echo.Context) error {
var req gen.ClientCreate
if err := ctx.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenAPI では redirect_uris の minItems=1 や client_type の enum、URI format 等が定義されていますが、現状は Bind のみで不正入力を受け入れます。name/redirect_uris/client_type のバリデーションを追加して 400 を返すようにしてください(Update も同様)。

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +19
clients, err := h.clientUseCase.List(ctx.Request().Context())
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

内部エラー時に err.Error() をそのままレスポンスに載せると、SQL エラー等の内部情報が外部に露出します。ログには詳細を残しつつ、クライアントへのメッセージは一般化(例: "internal server error")するなど情報漏えいを避けてください。

Copilot uses AI. Check for mistakes.
uc := NewClientUseCase(repo)
ctx := context.Background()

created, _ := uc.Create(ctx, "original", domain.ClientTypeConfidential, []string{"http://localhost:3000/callback"})
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

テスト内で uc.Create のエラーを捨てています。失敗時に created が nil になり後続がパニックする可能性があるため、ここも if err != nil { t.Fatalf(...) } の形で検証してください。

Suggested change
created, _ := uc.Create(ctx, "original", domain.ClientTypeConfidential, []string{"http://localhost:3000/callback"})
created, err := uc.Create(ctx, "original", domain.ClientTypeConfidential, []string{"http://localhost:3000/callback"})
if err != nil {
t.Fatalf("Create failed: %v", err)
}

Copilot uses AI. Check for mistakes.
Comment on lines +267 to +271
created, _ := uc.Create(ctx, "test-client", domain.ClientTypeConfidential, []string{"http://localhost:3000/callback"})
originalHash := repo.getSecretHash(created.ClientID)

newSecret, err := uc.RegenerateSecret(ctx, created.ClientID)
if err != nil {
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

テスト内で uc.Create のエラーを捨てています。失敗時に created が nil になり後続がパニックする可能性があるため、ここも if err != nil { t.Fatalf(...) } の形で検証してください。

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +18
paths:
/api/v1/admin/clients:
get:
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR 説明では /api/v1/admin/client/{clientId}(client が単数)となっていますが、OpenAPI/実装は /api/v1/admin/clients/{clientId}(複数形)です。どちらが正かを揃えて、PR 説明またはパス定義を修正してください。

Copilot uses AI. Check for mistakes.
uc := NewClientUseCase(repo)
ctx := context.Background()

created, _ := uc.Create(ctx, "test-client", domain.ClientTypeConfidential, []string{"http://localhost:3000/callback"})
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

テスト内で uc.Create のエラーを捨てています。失敗時に created が nil になり後続がパニックする可能性があるため、ここも if err != nil { t.Fatalf(...) } の形で検証してください。

Suggested change
created, _ := uc.Create(ctx, "test-client", domain.ClientTypeConfidential, []string{"http://localhost:3000/callback"})
created, err := uc.Create(ctx, "test-client", domain.ClientTypeConfidential, []string{"http://localhost:3000/callback"})
if err != nil {
t.Fatalf("Create failed: %v", err)
}

Copilot uses AI. Check for mistakes.
Comment on lines +238 to +241
created, _ := uc.Create(ctx, "test-client", domain.ClientTypeConfidential, []string{"http://localhost:3000/callback"})

err := uc.Delete(ctx, created.ClientID)
if err != nil {
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

テスト内で uc.Create のエラーを捨てています。失敗時に created が nil になり後続がパニックする可能性があるため、ここも if err != nil { t.Fatalf(...) } の形で検証してください。

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +46
secret, err := generateSecret()
if err != nil {
return nil, err
}

secretHash, err := hashSecret(secret)
if err != nil {
return nil, err
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

client_typepublic の場合でも無条件にシークレットを生成しています。public client はシークレットを保持しない前提とするなら、confidential のみ生成・保存し、public は client_secret_hash を NULL にする/そもそも public を受け付けない等、型ごとの扱いを明確にしてください。

Suggested change
secret, err := generateSecret()
if err != nil {
return nil, err
}
secretHash, err := hashSecret(secret)
if err != nil {
return nil, err
var (
secret string
secretHash string
)
switch clientType {
case domain.ClientTypeConfidential:
s, err := generateSecret()
if err != nil {
return nil, err
}
h, err := hashSecret(s)
if err != nil {
return nil, err
}
secret = s
secretHash = h
case domain.ClientTypePublic:
// Public clients do not have a client secret.
// Leave secret and secretHash as empty values so that
// the repository can persist a NULL/empty secret hash.
default:
return nil, errors.New("invalid client type")

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

これは確実に対応しておいていただきたいです
#53 で対応されている fosite の実装の都合や、また secret が public client でも必ず返ってしまうため、仕様の範囲外になります

Comment on lines +109 to +113
func (u *clientUseCase) RegenerateSecret(ctx context.Context, clientID uuid.UUID) (string, error) {
_, err := u.repo.Get(ctx, clientID)
if err != nil {
if errors.Is(err, repository.ErrClientNotFound) {
return "", ErrClientNotFound
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RegenerateSecret は clientType を確認せず再生成しています。public client を許可する設計なら、public では拒否する(400/409 等)か、secret を保持しない方針に合わせて挙動を定義してください。

Copilot uses AI. Check for mistakes.
Comment on lines +26 to 29
clientRepo := repository.NewClientRepository(queries)
clientUC := usecase.NewClientUseCase(clientRepo)
handler := v1.NewHandler(clientUC)

Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここで admin 系の CRUD ハンドラを組み立てていますが、ルーティング前段の認証・認可ミドルウェアが見当たりません(/api/v1/admin/* が無条件で公開されます)。admin 専用の認証と権限チェックを追加してください。

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants