diff --git a/sources/dev/authentication-go/CLAUDE.md b/sources/dev/authentication-go/CLAUDE.md index b902ab4..5c16ae6 100644 --- a/sources/dev/authentication-go/CLAUDE.md +++ b/sources/dev/authentication-go/CLAUDE.md @@ -18,14 +18,38 @@ gofmt -w . # auto-format go test ./... # all tests (server suite needs MySQL) go test ./internal/auth/ # pure unit tests, no MySQL go test ./internal/server/ -run TestX -v # a single integration test +make swagger # regenerate cmd/auth-service/docs +go build -tags swagger ./... # build incl. the Swagger UI ``` +A `Makefile` wraps these (`make build|vet|fmt|test|swagger|build-service-swagger`). + Integration tests (`internal/server/integration_test.go`) require MySQL on `127.0.0.1:3306` by default; they `t.Skip` if it is unreachable. Start it with `docker compose up -d mysql`. Override the endpoint with `TEST_MYSQL_DSN`. Each test calls `newTestApp`, which clears all MySQL tables, then bootstraps an admin via `seed.Bootstrap`. +## CLI (cobra) + +`cmd/auth-service` is a single `github.com/spf13/cobra` binary. `main.go` is a +thin root that wires subcommands, each in its own `cmd_*.go` file: `serve` (HTTP +server) and `seed`. The container default command is `serve`; maintenance tasks +run by overriding it. Keep subcommands thin — config load + dependency wiring +only; all logic lives in `internal/`. + +## Swagger + +Endpoints carry [swaggo/swag](https://github.com/swaggo/swag) annotations; the +general API info (title, `securityDefinitions`: `ClientID`, `BearerAuth`, +`BasicAuth`) lives on `cmd/auth-service/main.go` (the `swag init -g` entry file). +The generated `cmd/auth-service/docs` package is committed. The UI mount +(`internal/server/swagger.go`) is behind the `swagger` build tag with a no-op +stub (`swagger_stub.go`) for the default build, so plain `go build`/`go test` +never need the generated code. `server.NewRouter` calls `mountSwagger` and the +UI is served only when `SWAGGER_ENABLED=true`. After changing routes/DTOs or +their annotations, rerun `make swagger` and commit the regenerated docs. + ## Architecture Layered with a swappable storage adapter (see README for the full tree): @@ -38,7 +62,7 @@ Layered with a swappable storage adapter (see README for the full tree): Azure Table secondary-index rows; invite codes are consumed atomically with a conditional `UPDATE ... WHERE used_at IS NULL`. - `internal/repository/aztables` — legacy Azure Table implementation plus - `ExportSnapshot`, used by `migrate-storage azure-to-mysql`. + `ExportSnapshot` (storage-neutral snapshot export helper). - `internal/auth` — JWT issue/verify (custom claims so `aud` stays a single string and `membership` is a snake_case string), argon2id passwords, SHA-256 client secrets (with legacy argon2 fallback), PKCE, OAuth2 helpers. @@ -69,5 +93,6 @@ Layered with a swappable storage adapter (see README for the full tree): ## Dependencies `go.mod` uses a local `replace github.com/zhaochy1990/x => ../../../../x` for the -shared `x` library. Docker builds use `go mod vendor` to capture it (vendor/ is -gitignored — run `go mod vendor` before `docker build`). +shared `x` library. Docker builds use `go mod vendor` to capture it — and the +swaggo packages needed by the `-tags swagger` image build — into vendor/ +(gitignored; run `go mod vendor` before `docker build`). diff --git a/sources/dev/authentication-go/Dockerfile b/sources/dev/authentication-go/Dockerfile index cb87f14..b718a95 100644 --- a/sources/dev/authentication-go/Dockerfile +++ b/sources/dev/authentication-go/Dockerfile @@ -1,16 +1,22 @@ # syntax=docker/dockerfile:1 # # Self-contained build using vendored dependencies. Run `go mod vendor` in this -# directory before building so the local `replace`d `x` module is captured in -# vendor/ (the build does not reach outside the build context). +# directory before building so the local `replace`d `x` module (and the swaggo +# packages) are captured in vendor/ (the build does not reach outside the build +# context). The committed cmd/auth-service/docs package is compiled in via +# `-tags swagger`, so the /swagger UI is available when SWAGGER_ENABLED=true. # # go mod vendor # docker build -t auth-service-go . +# +# The image bundles every subcommand in one binary (cobra): the default command +# is `serve`; override it to run maintenance tasks, e.g. +# docker run auth-service-go seed admin@example.com FROM golang:1.25-bookworm AS build WORKDIR /src COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -mod=vendor -trimpath -o /out/auth-service ./cmd/auth-service +RUN CGO_ENABLED=0 GOOS=linux go build -mod=vendor -tags swagger -trimpath -o /out/auth-service ./cmd/auth-service FROM gcr.io/distroless/static-debian12 WORKDIR /app @@ -21,3 +27,4 @@ ENV APP_VERSION=$APP_VERSION # (docker-compose volume locally; Azure File Share in production). EXPOSE 3000 ENTRYPOINT ["/app/auth-service"] +CMD ["serve"] diff --git a/sources/dev/authentication-go/Makefile b/sources/dev/authentication-go/Makefile new file mode 100644 index 0000000..f7246ec --- /dev/null +++ b/sources/dev/authentication-go/Makefile @@ -0,0 +1,43 @@ +# Makefile for the Go auth microservice (authentication-go). One `auth-service` +# binary bundles the HTTP server and every maintenance task as a cobra +# subcommand (`auth-service serve`, `auth-service seed`, `auth-service migrate`, +# `auth-service migrate-storage`); containers just set the entrypoint. + +SWAG_VERSION := v1.16.6 + +.PHONY: build build-service build-service-swagger swagger test vet fmt fmt-check vendor + +# Compile-check every package (no output binary emitted). +build: + go build ./... + +# Build the single binary to bin/auth-service (WITHOUT the Swagger UI). Use +# `make build-service-swagger` for the Swagger-enabled build. +build-service: + go build -o bin/auth-service ./cmd/auth-service + +# Generate the OpenAPI/Swagger docs (cmd/auth-service/docs). Committed and +# regenerated here; the general API info lives on cmd/auth-service/main.go. +swagger: + go run github.com/swaggo/swag/cmd/swag@$(SWAG_VERSION) init -g cmd/auth-service/main.go -o cmd/auth-service/docs --parseInternal --parseDependency + +# Build the binary with the Swagger UI compiled in (requires `make swagger`). +# The UI is served by `auth-service serve` when SWAGGER_ENABLED=true. +build-service-swagger: swagger + go build -tags swagger -o bin/auth-service ./cmd/auth-service + +test: + go test ./... + +vet: + go vet ./... + +fmt: + gofmt -w . + +fmt-check: + gofmt -l . + +# Capture the local `replace`d `x` module into vendor/ before docker build. +vendor: + go mod vendor diff --git a/sources/dev/authentication-go/README.md b/sources/dev/authentication-go/README.md index 9e3a38c..208aff7 100644 --- a/sources/dev/authentication-go/README.md +++ b/sources/dev/authentication-go/README.md @@ -11,7 +11,11 @@ Azure Table adapter retained for migration and rollback during the cutover. ## Architecture ```text -cmd/auth-service/main.go entrypoint + seed/migrate subcommands +cmd/auth-service/ unified cobra CLI (one binary, subcommands) + main.go root command + Swagger general API info + cmd_serve.go `serve` — start the Gin HTTP server + cmd_seed.go `seed` — bootstrap the admin user + app client + docs/ generated OpenAPI/Swagger docs (`make swagger`) internal/ config/ env-based configuration domain/ storage-agnostic entity models + value types @@ -27,6 +31,17 @@ internal/ seed/ admin bootstrap ``` +## CLI + +The service is one binary with cobra subcommands (`auth-service `): + +| Command | Purpose | +|---------|---------| +| `serve` | Start the Gin HTTP server (default container command) | +| `seed [email] [password]` | Bootstrap the admin user and Admin Dashboard app client | + +Run `auth-service --help` for flags. + ## Build, Test, Run Start local MySQL: @@ -35,13 +50,14 @@ Start local MySQL: docker compose up -d mysql ``` -Run checks: +Run checks (a `Makefile` wraps these): ```bash -go build ./... -go vet ./... -gofmt -l . -go test ./... -count=1 +make build # go build ./... +make vet # go vet ./... +make fmt-check # gofmt -l . +make test # go test ./... +make swagger # regenerate cmd/auth-service/docs from swag annotations ``` The integration suite uses MySQL. Override the local test database with: @@ -55,7 +71,7 @@ Run the service locally: ```bash STORAGE_BACKEND=mysql \ MYSQL_DSN="mysql://auth:auth_password@127.0.0.1:3306/auth" \ -go run ./cmd/auth-service +go run ./cmd/auth-service serve ``` Bootstrap the first admin: @@ -66,27 +82,24 @@ MYSQL_DSN="mysql://auth:auth_password@127.0.0.1:3306/auth" \ go run ./cmd/auth-service seed admin@example.com MyPassword1! ``` -## Azure Tables To MySQL Migration +## Swagger / OpenAPI -Dry-run export from the legacy Azure Tables backend: +Endpoints are annotated with [swaggo/swag](https://github.com/swaggo/swag). The +generated spec lives in `cmd/auth-service/docs` (committed; regenerate with +`make swagger`). The Swagger UI is compiled in only with the `swagger` build tag +and served at `/swagger/index.html` when `SWAGGER_ENABLED=true`: ```bash -AZURE_STORAGE_CONNECTION_STRING="..." \ -go run ./cmd/auth-service migrate-storage azure-to-mysql --dry-run +make build-service-swagger # go build -tags swagger ... +SWAGGER_ENABLED=true \ +STORAGE_BACKEND=mysql MYSQL_DSN="mysql://auth:auth_password@127.0.0.1:3306/auth" \ +./bin/auth-service serve +# open http://127.0.0.1:3000/swagger/index.html ``` -Import into MySQL: +Plain `go build` / `go test` never need the generated package (a build-tagged +no-op stub replaces the UI), so the default build stays lean. -```bash -AZURE_STORAGE_CONNECTION_STRING="..." \ -MYSQL_DSN="mysql://user:password@tcp-host:3306/auth" \ -go run ./cmd/auth-service migrate-storage azure-to-mysql -``` - -Without `--clear-target`, the command requires every target MySQL table to be -empty before importing. `--clear-target` deletes target MySQL rows and imports -the snapshot in one transaction. If the import fails, the target rows are rolled -back. Use it only for a fresh rehearsal or planned cutover window. ## Tencent Cloud MySQL @@ -135,7 +148,8 @@ Cutover checklist: ## Docker The module uses a local `replace` for the sibling `x` library. Build images from -vendored dependencies: +vendored dependencies (the image is built with `-tags swagger`, so the UI is +available at runtime when `SWAGGER_ENABLED=true`): ```bash go mod vendor @@ -143,6 +157,9 @@ docker build -t auth-service-go . docker compose up --build ``` +The image bundles every subcommand; the default command is `serve`. Override it +to run maintenance tasks, e.g. `docker run auth-service-go seed admin@example.com`. + ## Environment Variables | Variable | Required | Default | @@ -161,6 +178,7 @@ docker compose up --build | `SERVER_PORT` | No | `3000` | | `CORS_ALLOWED_ORIGINS` | No | `http://localhost:5173,http://localhost:3000` | | `AUTH_ENABLE_TEST_PROVIDERS` | No | `false` | +| `SWAGGER_ENABLED` | No | `false` (UI also requires the `swagger` build tag) | | `STRIDE_REQUIRE_INVITE_CODE` | No | `false` | | `APP_VERSION` | No | `dev` | | `LOG_LEVEL` / `LOG_FORMAT` | No | `debug` / `json` | @@ -175,3 +193,4 @@ docker compose up --build | `/api/teams/*` | Bearer | team CRUD, join/leave/transfer-owner, members | | `/admin/*` | Bearer admin | app/provider/user/team/invite-code management | | `/health` | none | health + version | +| `/swagger/*` | none | Swagger UI (when `SWAGGER_ENABLED=true` + `swagger` build tag) | diff --git a/sources/dev/authentication-go/cmd/auth-service/cmd_seed.go b/sources/dev/authentication-go/cmd/auth-service/cmd_seed.go new file mode 100644 index 0000000..3a8f1ca --- /dev/null +++ b/sources/dev/authentication-go/cmd/auth-service/cmd_seed.go @@ -0,0 +1,77 @@ +// Subcommand `auth-service seed`: bootstraps the admin user and the Admin +// Dashboard application client. Safe to re-run; existing records are reused. +package main + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + "github.com/zhaochy1990/auth-service/internal/config" + "github.com/zhaochy1990/auth-service/internal/seed" + "github.com/zhaochy1990/auth-service/internal/storage" +) + +func newSeedCmd() *cobra.Command { + return &cobra.Command{ + Use: "seed [email] [password]", + Short: "Bootstrap the admin user and Admin Dashboard application client", + Args: cobra.MaximumNArgs(2), + RunE: func(_ *cobra.Command, args []string) error { + email := "admin@example.com" + if len(args) > 0 { + email = args[0] + } + var password *string + if len(args) > 1 { + password = &args[1] + } + return runSeed(context.Background(), email, password) + }, + } +} + +func runSeed(ctx context.Context, email string, password *string) error { + cfg, err := config.FromEnv() + if err != nil { + return err + } + repo, err := storage.Open(ctx, cfg) + if err != nil { + return err + } + + fmt.Println("=== Auth Service Bootstrap ===") + fmt.Println() + + result, err := seed.Bootstrap(ctx, repo, email, password) + if err != nil { + return fmt.Errorf("bootstrap failed: %w", err) + } + + fmt.Printf(" Client ID: %s\n", result.AppClientID) + if result.AppClientSecret != nil { + fmt.Printf(" Client Secret: %s\n", *result.AppClientSecret) + fmt.Println(" (Save this secret — it won't be shown again!)") + } else { + fmt.Println(" Admin Dashboard application already exists.") + } + fmt.Println() + + switch result.UserAction { + case "created": + fmt.Printf("Created admin user: %s\n", email) + case "promoted": + fmt.Printf("Promoted %s to admin role.\n", email) + case "already_admin": + fmt.Printf("User %s is already an admin.\n", email) + } + + fmt.Println() + fmt.Println("=== Bootstrap complete ===") + fmt.Println() + fmt.Println("For frontend .env, set:") + fmt.Printf(" VITE_API_CLIENT_ID=%s\n", result.AppClientID) + return nil +} diff --git a/sources/dev/authentication-go/cmd/auth-service/cmd_serve.go b/sources/dev/authentication-go/cmd/auth-service/cmd_serve.go new file mode 100644 index 0000000..04a6f8e --- /dev/null +++ b/sources/dev/authentication-go/cmd/auth-service/cmd_serve.go @@ -0,0 +1,61 @@ +// Subcommand `auth-service serve`: loads config from the environment, opens the +// configured storage backend, and starts the Gin HTTP server. This is the +// default runtime entrypoint for the container. +package main + +import ( + "context" + + "github.com/spf13/cobra" + "github.com/zhaochy1990/x/logger" + + "github.com/zhaochy1990/auth-service/internal/auth" + "github.com/zhaochy1990/auth-service/internal/config" + "github.com/zhaochy1990/auth-service/internal/server" + "github.com/zhaochy1990/auth-service/internal/storage" +) + +func newServeCmd() *cobra.Command { + return &cobra.Command{ + Use: "serve", + Short: "Start the Gin HTTP server", + Args: cobra.NoArgs, + RunE: func(_ *cobra.Command, _ []string) error { + return runServe() + }, + } +} + +func runServe() error { + log := logger.MustGetLogger(&logger.LoggerConfig{ + Format: config.EnvOr("LOG_FORMAT", "json"), + ServiceName: "auth-service", + Level: config.EnvOr("LOG_LEVEL", "debug"), + }).Sugar() + + ctx := context.Background() + + cfg, err := config.FromEnv() + if err != nil { + return err + } + + log.Infow("opening storage backend", "backend", cfg.StorageBackend) + repo, err := storage.Open(ctx, cfg) + if err != nil { + return err + } + log.Infow("storage ready", "backend", cfg.StorageBackend) + + jwt, err := auth.NewJWTManager(cfg) + if err != nil { + return err + } + + r := server.NewRouter(repo, jwt, cfg) + log.Infow("starting server", "addr", cfg.Addr(), "swagger_enabled", cfg.SwaggerEnabled) + if cfg.SwaggerEnabled { + log.Infow("swagger UI available", "path", "/swagger/index.html") + } + return r.Run(cfg.Addr()) +} diff --git a/sources/dev/authentication-go/cmd/auth-service/docs/docs.go b/sources/dev/authentication-go/cmd/auth-service/docs/docs.go new file mode 100644 index 0000000..db85312 --- /dev/null +++ b/sources/dev/authentication-go/cmd/auth-service/docs/docs.go @@ -0,0 +1,3561 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/admin/applications": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List applications", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handlers.applicationResponse" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create application", + "parameters": [ + { + "description": "Application to create", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.createApplicationRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.createApplicationResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/applications/{id}": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Update application", + "parameters": [ + { + "type": "string", + "description": "Application ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Fields to update", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.updateApplicationRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.applicationResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/applications/{id}/providers": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List application providers", + "parameters": [ + { + "type": "string", + "description": "Application ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handlers.providerResponse" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Add application provider", + "parameters": [ + { + "type": "string", + "description": "Application ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Provider to attach", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.addProviderRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.providerResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/applications/{id}/providers/{provider_id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Remove application provider", + "parameters": [ + { + "type": "string", + "description": "Application ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Provider ID", + "name": "provider_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.StatusResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/applications/{id}/rotate-secret": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Rotate application secret", + "parameters": [ + { + "type": "string", + "description": "Application ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.rotateSecretResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/invite-codes": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List invite codes", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handlers.inviteCodeResponse" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create invite code", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.inviteCodeResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/invite-codes/{code}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Revoke invite code", + "parameters": [ + { + "type": "string", + "description": "Invite code", + "name": "code", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.StatusResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/stats": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get admin stats", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.statsResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/teams": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create team (admin)", + "parameters": [ + { + "description": "Team to create", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.adminCreateTeamRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.teamResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/teams/{id}/members": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Add team member (admin)", + "parameters": [ + { + "type": "string", + "description": "Team ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Member to add", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.adminAddMemberRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.adminTeamMembershipResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/teams/{id}/members/{user_id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Remove team member (admin)", + "parameters": [ + { + "type": "string", + "description": "Team ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "User ID", + "name": "user_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.StatusResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/users": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List users", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.userListResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create user", + "parameters": [ + { + "description": "User to create", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.createUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.userResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/users/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get user", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.userResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "tags": [ + "admin" + ], + "summary": "Delete user", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + }, + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Update user", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Fields to update", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.updateUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.userResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/users/{id}/accounts": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List user accounts", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handlers.userAccountResponse" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/users/{id}/accounts/{provider_id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Unlink user account", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Provider ID", + "name": "provider_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.StatusResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/users/{id}/reset-password": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Reset user password", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "New password", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.resetUserPasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.resetUserPasswordResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/auth/login": { + "post": { + "security": [ + { + "ClientID": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Log in with email and password", + "parameters": [ + { + "description": "Login credentials", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.loginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.tokenResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/auth/logout": { + "post": { + "security": [ + { + "ClientID": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Log out (revoke a refresh token)", + "parameters": [ + { + "description": "Refresh token to revoke", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.logoutRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.StatusResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/auth/provider/{provider_id}/login": { + "post": { + "security": [ + { + "ClientID": [] + } + ], + "description": "Authenticates against a configured provider, creating the user on first sign-in, and returns tokens.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Log in via an external identity provider", + "parameters": [ + { + "type": "string", + "description": "Provider id", + "name": "provider_id", + "in": "path", + "required": true + }, + { + "description": "Provider credential", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.providerLoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.tokenResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/auth/refresh": { + "post": { + "security": [ + { + "ClientID": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Rotate a refresh token", + "parameters": [ + { + "description": "Refresh token", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.refreshRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.tokenResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/auth/register": { + "post": { + "security": [ + { + "ClientID": [] + } + ], + "description": "Creates a password user (optionally invite-gated) and returns access + refresh tokens.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Register a new password user", + "parameters": [ + { + "description": "Registration details", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.registerRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/internal_handlers.registerResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/teams": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "teams" + ], + "summary": "List open teams", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "teams" + ], + "summary": "Create a team", + "parameters": [ + { + "description": "Team to create", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.createTeamRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.teamResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/teams/{team_id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "teams" + ], + "summary": "Get a team", + "parameters": [ + { + "type": "string", + "description": "Team id", + "name": "team_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.teamResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "teams" + ], + "summary": "Delete a team", + "parameters": [ + { + "type": "string", + "description": "Team id", + "name": "team_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.StatusResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/teams/{team_id}/join": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "teams" + ], + "summary": "Join a team", + "parameters": [ + { + "type": "string", + "description": "Team id", + "name": "team_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.teamMembershipResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/teams/{team_id}/leave": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "teams" + ], + "summary": "Leave a team", + "parameters": [ + { + "type": "string", + "description": "Team id", + "name": "team_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.StatusResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/teams/{team_id}/members": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "teams" + ], + "summary": "List team members", + "parameters": [ + { + "type": "string", + "description": "Team id", + "name": "team_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/teams/{team_id}/transfer-owner": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "teams" + ], + "summary": "Transfer team ownership", + "parameters": [ + { + "type": "string", + "description": "Team id", + "name": "team_id", + "in": "path", + "required": true + }, + { + "description": "New owner", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.transferOwnerRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.teamResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/users/me": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Get the current user's profile", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.userProfileResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Deletes the authenticated user and all dependent rows. Refuses if the user still owns any team.", + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Delete the current user's account", + "responses": { + "204": { + "description": "No Content" + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + }, + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Update the current user's profile", + "parameters": [ + { + "description": "Fields to update", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.updateProfileRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.userProfileResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/users/me/accounts": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "List the current user's linked accounts", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handlers.accountResponse" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/users/me/accounts/{provider_id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Unlinks a linked provider account. Refuses to remove the user's last remaining account.", + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Unlink a provider account", + "parameters": [ + { + "type": "string", + "description": "Provider id", + "name": "provider_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.StatusResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/users/me/accounts/{provider_id}/link": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Link an external provider account", + "parameters": [ + { + "type": "string", + "description": "Provider id", + "name": "provider_id", + "in": "path", + "required": true + }, + { + "description": "Provider credential", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.linkAccountRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.accountResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/users/me/teams": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "teams" + ], + "summary": "List my teams", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/oauth/introspect": { + "post": { + "security": [ + { + "BasicAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "oauth" + ], + "summary": "Introspect a token (RFC 7662 subset)", + "parameters": [ + { + "description": "Token to introspect", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.introspectRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.introspectResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/oauth/revoke": { + "post": { + "security": [ + { + "BasicAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "oauth" + ], + "summary": "Revoke a token (RFC 7009)", + "parameters": [ + { + "description": "Token to revoke", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.revokeRequest" + } + } + ], + "responses": { + "200": { + "description": "Token revoked (always 200 per RFC 7009)" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/oauth/token": { + "post": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Issues tokens for the authorization_code, client_credentials, refresh_token, and password grant types. The client authenticates with HTTP Basic (client_id:client_secret).", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "oauth" + ], + "summary": "OAuth2 token endpoint", + "parameters": [ + { + "description": "Grant request", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.tokenRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.oauthTokenResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + } + }, + "definitions": { + "github_com_zhaochy1990_auth-service_internal_domain.InviteCodeKind": { + "type": "string", + "enum": [ + "single_use", + "long_term" + ], + "x-enum-varnames": [ + "InviteSingleUse", + "InviteLongTerm" + ] + }, + "github_com_zhaochy1990_auth-service_internal_domain.MembershipTier": { + "type": "string", + "enum": [ + "regular", + "vip1" + ], + "x-enum-varnames": [ + "MembershipRegular", + "MembershipVip1" + ] + }, + "github_com_zhaochy1990_auth-service_internal_domain.UserType": { + "type": "string", + "enum": [ + "regular", + "testing" + ], + "x-enum-varnames": [ + "UserTypeRegular", + "UserTypeTesting" + ] + }, + "internal_handlers.ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "invalid_credentials" + }, + "message": { + "type": "string", + "example": "Invalid credentials" + } + } + }, + "internal_handlers.StatusResponse": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "ok" + } + } + }, + "internal_handlers.accountResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "provider_account_id": { + "type": "string" + }, + "provider_id": { + "type": "string" + } + } + }, + "internal_handlers.addProviderRequest": { + "type": "object", + "properties": { + "config": { + "type": "array", + "items": { + "type": "integer" + } + }, + "provider_id": { + "type": "string" + } + } + }, + "internal_handlers.adminAddMemberRequest": { + "type": "object", + "properties": { + "role": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "internal_handlers.adminCreateTeamRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "is_open": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "owner_user_id": { + "type": "string" + } + } + }, + "internal_handlers.adminTeamMembershipResponse": { + "type": "object", + "properties": { + "joined_at": { + "type": "string" + }, + "role": { + "type": "string" + }, + "team_id": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "internal_handlers.appStats": { + "type": "object", + "properties": { + "active": { + "type": "integer" + }, + "inactive": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "internal_handlers.applicationResponse": { + "type": "object", + "properties": { + "allowed_scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "client_id": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "redirect_uris": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "internal_handlers.createApplicationRequest": { + "type": "object", + "properties": { + "allowed_scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "redirect_uris": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "internal_handlers.createApplicationResponse": { + "type": "object", + "properties": { + "allowed_scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "client_id": { + "type": "string" + }, + "client_secret": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "redirect_uris": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "internal_handlers.createTeamRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "internal_handlers.createUserRequest": { + "type": "object", + "properties": { + "custom_attributes": { + "type": "object", + "additionalProperties": {} + }, + "email": { + "type": "string" + }, + "membership": { + "$ref": "#/definitions/github_com_zhaochy1990_auth-service_internal_domain.MembershipTier" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "role": { + "type": "string" + }, + "user_type": { + "$ref": "#/definitions/github_com_zhaochy1990_auth-service_internal_domain.UserType" + } + } + }, + "internal_handlers.introspectRequest": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, + "internal_handlers.introspectResponse": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "aud": { + "type": "string" + }, + "exp": { + "type": "integer" + }, + "scope": { + "type": "string" + }, + "sub": { + "type": "string" + } + } + }, + "internal_handlers.inviteCodeResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "created_by": { + "type": "string" + }, + "grants_membership": { + "type": "string" + }, + "grants_membership_days": { + "type": "integer" + }, + "grants_user_type": { + "$ref": "#/definitions/github_com_zhaochy1990_auth-service_internal_domain.UserType" + }, + "id": { + "type": "string" + }, + "is_revoked": { + "type": "boolean" + }, + "kind": { + "$ref": "#/definitions/github_com_zhaochy1990_auth-service_internal_domain.InviteCodeKind" + }, + "used_at": { + "type": "string" + }, + "used_by": { + "type": "string" + } + } + }, + "internal_handlers.linkAccountRequest": { + "type": "object", + "properties": { + "credential": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "internal_handlers.loginRecordResponse": { + "type": "object", + "properties": { + "at": { + "type": "string" + }, + "ip": { + "type": "string" + } + } + }, + "internal_handlers.loginRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "internal_handlers.logoutRequest": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string" + } + } + }, + "internal_handlers.oauthTokenResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "expires_in": { + "type": "integer" + }, + "refresh_token": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "token_type": { + "type": "string" + } + } + }, + "internal_handlers.providerLoginRequest": { + "type": "object", + "properties": { + "credential": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "internal_handlers.providerResponse": { + "type": "object", + "properties": { + "config": { + "type": "array", + "items": { + "type": "integer" + } + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "provider_id": { + "type": "string" + } + } + }, + "internal_handlers.refreshRequest": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string" + } + } + }, + "internal_handlers.registerRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "invite_code": { + "type": "string" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "internal_handlers.registerResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "expires_in": { + "type": "integer" + }, + "refresh_token": { + "type": "string" + }, + "token_type": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "internal_handlers.resetUserPasswordRequest": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "revoke_sessions": { + "type": "boolean" + } + } + }, + "internal_handlers.resetUserPasswordResponse": { + "type": "object", + "properties": { + "revoked_sessions": { + "type": "boolean" + }, + "user_id": { + "type": "string" + } + } + }, + "internal_handlers.revokeRequest": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, + "internal_handlers.rotateSecretResponse": { + "type": "object", + "properties": { + "client_id": { + "type": "string" + }, + "client_secret": { + "type": "string" + } + } + }, + "internal_handlers.statsResponse": { + "type": "object", + "properties": { + "applications": { + "$ref": "#/definitions/internal_handlers.appStats" + }, + "users": { + "$ref": "#/definitions/internal_handlers.userStats" + } + } + }, + "internal_handlers.teamMembershipResponse": { + "type": "object", + "properties": { + "joined_at": { + "type": "string" + }, + "role": { + "type": "string" + }, + "team_id": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "internal_handlers.teamResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_open": { + "type": "boolean" + }, + "member_count": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "owner_user_id": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "internal_handlers.tokenRequest": { + "type": "object", + "properties": { + "code": { + "description": "authorization_code flow", + "type": "string" + }, + "code_verifier": { + "type": "string" + }, + "grant_type": { + "type": "string" + }, + "password": { + "type": "string" + }, + "redirect_uri": { + "type": "string" + }, + "refresh_token": { + "description": "refresh_token flow", + "type": "string" + }, + "scope": { + "description": "common", + "type": "string" + }, + "username": { + "description": "password flow", + "type": "string" + } + } + }, + "internal_handlers.tokenResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "expires_in": { + "type": "integer" + }, + "refresh_token": { + "type": "string" + }, + "token_type": { + "type": "string" + } + } + }, + "internal_handlers.transferOwnerRequest": { + "type": "object", + "properties": { + "new_owner_user_id": { + "type": "string" + } + } + }, + "internal_handlers.updateApplicationRequest": { + "type": "object", + "properties": { + "allowed_scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "is_active": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "redirect_uris": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "internal_handlers.updateProfileRequest": { + "type": "object", + "properties": { + "avatar_url": { + "type": "string" + }, + "custom_attributes": { + "type": "object", + "additionalProperties": {} + }, + "name": { + "type": "string" + } + } + }, + "internal_handlers.updateUserRequest": { + "type": "object", + "properties": { + "custom_attributes": { + "type": "object", + "additionalProperties": {} + }, + "is_active": { + "type": "boolean" + }, + "membership": { + "$ref": "#/definitions/github_com_zhaochy1990_auth-service_internal_domain.MembershipTier" + }, + "membership_expires_at": { + "type": "string" + }, + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "role": { + "type": "string" + }, + "user_type": { + "$ref": "#/definitions/github_com_zhaochy1990_auth-service_internal_domain.UserType" + } + } + }, + "internal_handlers.userAccountResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "provider_account_id": { + "type": "string" + }, + "provider_id": { + "type": "string" + } + } + }, + "internal_handlers.userListResponse": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "per_page": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "users": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handlers.userResponse" + } + } + } + }, + "internal_handlers.userProfileResponse": { + "type": "object", + "properties": { + "avatar_url": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "custom_attributes": { + "type": "object", + "additionalProperties": {} + }, + "email": { + "type": "string" + }, + "email_verified": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "membership": { + "$ref": "#/definitions/github_com_zhaochy1990_auth-service_internal_domain.MembershipTier" + }, + "membership_expires_at": { + "type": "string" + }, + "name": { + "type": "string" + }, + "user_type": { + "$ref": "#/definitions/github_com_zhaochy1990_auth-service_internal_domain.UserType" + } + } + }, + "internal_handlers.userResponse": { + "type": "object", + "properties": { + "avatar_url": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "custom_attributes": { + "type": "object", + "additionalProperties": {} + }, + "email": { + "type": "string" + }, + "email_verified": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "last_login_at": { + "type": "string" + }, + "membership": { + "$ref": "#/definitions/github_com_zhaochy1990_auth-service_internal_domain.MembershipTier" + }, + "membership_expires_at": { + "type": "string" + }, + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "recent_logins": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handlers.loginRecordResponse" + } + }, + "role": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "user_type": { + "$ref": "#/definitions/github_com_zhaochy1990_auth-service_internal_domain.UserType" + } + } + }, + "internal_handlers.userStats": { + "type": "object", + "properties": { + "recent": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + } + }, + "securityDefinitions": { + "BasicAuth": { + "type": "basic" + }, + "BearerAuth": { + "description": "\"Bearer \u003cJWT\u003e\" for end-user and admin callers.", + "type": "apiKey", + "name": "Authorization", + "in": "header" + }, + "ClientID": { + "description": "Application client id for the /api/auth/* endpoints.", + "type": "apiKey", + "name": "X-Client-Id", + "in": "header" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "Auth Service API", + Description: "HTTP Basic auth (client_id:client_secret) for the /oauth/* endpoints.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/sources/dev/authentication-go/cmd/auth-service/docs/swagger.json b/sources/dev/authentication-go/cmd/auth-service/docs/swagger.json new file mode 100644 index 0000000..c5a71fa --- /dev/null +++ b/sources/dev/authentication-go/cmd/auth-service/docs/swagger.json @@ -0,0 +1,3535 @@ +{ + "swagger": "2.0", + "info": { + "description": "HTTP Basic auth (client_id:client_secret) for the /oauth/* endpoints.", + "title": "Auth Service API", + "contact": {}, + "version": "1.0" + }, + "paths": { + "/admin/applications": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List applications", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handlers.applicationResponse" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create application", + "parameters": [ + { + "description": "Application to create", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.createApplicationRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.createApplicationResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/applications/{id}": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Update application", + "parameters": [ + { + "type": "string", + "description": "Application ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Fields to update", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.updateApplicationRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.applicationResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/applications/{id}/providers": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List application providers", + "parameters": [ + { + "type": "string", + "description": "Application ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handlers.providerResponse" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Add application provider", + "parameters": [ + { + "type": "string", + "description": "Application ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Provider to attach", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.addProviderRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.providerResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/applications/{id}/providers/{provider_id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Remove application provider", + "parameters": [ + { + "type": "string", + "description": "Application ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Provider ID", + "name": "provider_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.StatusResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/applications/{id}/rotate-secret": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Rotate application secret", + "parameters": [ + { + "type": "string", + "description": "Application ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.rotateSecretResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/invite-codes": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List invite codes", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handlers.inviteCodeResponse" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create invite code", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.inviteCodeResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/invite-codes/{code}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Revoke invite code", + "parameters": [ + { + "type": "string", + "description": "Invite code", + "name": "code", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.StatusResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/stats": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get admin stats", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.statsResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/teams": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create team (admin)", + "parameters": [ + { + "description": "Team to create", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.adminCreateTeamRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.teamResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/teams/{id}/members": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Add team member (admin)", + "parameters": [ + { + "type": "string", + "description": "Team ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Member to add", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.adminAddMemberRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.adminTeamMembershipResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/teams/{id}/members/{user_id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Remove team member (admin)", + "parameters": [ + { + "type": "string", + "description": "Team ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "User ID", + "name": "user_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.StatusResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/users": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List users", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.userListResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create user", + "parameters": [ + { + "description": "User to create", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.createUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.userResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/users/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get user", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.userResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "tags": [ + "admin" + ], + "summary": "Delete user", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + }, + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Update user", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Fields to update", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.updateUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.userResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/users/{id}/accounts": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List user accounts", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handlers.userAccountResponse" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/users/{id}/accounts/{provider_id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Unlink user account", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Provider ID", + "name": "provider_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.StatusResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/admin/users/{id}/reset-password": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Reset user password", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "New password", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.resetUserPasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.resetUserPasswordResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/auth/login": { + "post": { + "security": [ + { + "ClientID": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Log in with email and password", + "parameters": [ + { + "description": "Login credentials", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.loginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.tokenResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/auth/logout": { + "post": { + "security": [ + { + "ClientID": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Log out (revoke a refresh token)", + "parameters": [ + { + "description": "Refresh token to revoke", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.logoutRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.StatusResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/auth/provider/{provider_id}/login": { + "post": { + "security": [ + { + "ClientID": [] + } + ], + "description": "Authenticates against a configured provider, creating the user on first sign-in, and returns tokens.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Log in via an external identity provider", + "parameters": [ + { + "type": "string", + "description": "Provider id", + "name": "provider_id", + "in": "path", + "required": true + }, + { + "description": "Provider credential", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.providerLoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.tokenResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/auth/refresh": { + "post": { + "security": [ + { + "ClientID": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Rotate a refresh token", + "parameters": [ + { + "description": "Refresh token", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.refreshRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.tokenResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/auth/register": { + "post": { + "security": [ + { + "ClientID": [] + } + ], + "description": "Creates a password user (optionally invite-gated) and returns access + refresh tokens.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Register a new password user", + "parameters": [ + { + "description": "Registration details", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.registerRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/internal_handlers.registerResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/teams": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "teams" + ], + "summary": "List open teams", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "teams" + ], + "summary": "Create a team", + "parameters": [ + { + "description": "Team to create", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.createTeamRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.teamResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/teams/{team_id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "teams" + ], + "summary": "Get a team", + "parameters": [ + { + "type": "string", + "description": "Team id", + "name": "team_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.teamResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "teams" + ], + "summary": "Delete a team", + "parameters": [ + { + "type": "string", + "description": "Team id", + "name": "team_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.StatusResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/teams/{team_id}/join": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "teams" + ], + "summary": "Join a team", + "parameters": [ + { + "type": "string", + "description": "Team id", + "name": "team_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.teamMembershipResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/teams/{team_id}/leave": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "teams" + ], + "summary": "Leave a team", + "parameters": [ + { + "type": "string", + "description": "Team id", + "name": "team_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.StatusResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/teams/{team_id}/members": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "teams" + ], + "summary": "List team members", + "parameters": [ + { + "type": "string", + "description": "Team id", + "name": "team_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/teams/{team_id}/transfer-owner": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "teams" + ], + "summary": "Transfer team ownership", + "parameters": [ + { + "type": "string", + "description": "Team id", + "name": "team_id", + "in": "path", + "required": true + }, + { + "description": "New owner", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.transferOwnerRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.teamResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/users/me": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Get the current user's profile", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.userProfileResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Deletes the authenticated user and all dependent rows. Refuses if the user still owns any team.", + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Delete the current user's account", + "responses": { + "204": { + "description": "No Content" + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + }, + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Update the current user's profile", + "parameters": [ + { + "description": "Fields to update", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.updateProfileRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.userProfileResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/users/me/accounts": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "List the current user's linked accounts", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handlers.accountResponse" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/users/me/accounts/{provider_id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Unlinks a linked provider account. Refuses to remove the user's last remaining account.", + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Unlink a provider account", + "parameters": [ + { + "type": "string", + "description": "Provider id", + "name": "provider_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.StatusResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/users/me/accounts/{provider_id}/link": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Link an external provider account", + "parameters": [ + { + "type": "string", + "description": "Provider id", + "name": "provider_id", + "in": "path", + "required": true + }, + { + "description": "Provider credential", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.linkAccountRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.accountResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/api/users/me/teams": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "teams" + ], + "summary": "List my teams", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/oauth/introspect": { + "post": { + "security": [ + { + "BasicAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "oauth" + ], + "summary": "Introspect a token (RFC 7662 subset)", + "parameters": [ + { + "description": "Token to introspect", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.introspectRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.introspectResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/oauth/revoke": { + "post": { + "security": [ + { + "BasicAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "oauth" + ], + "summary": "Revoke a token (RFC 7009)", + "parameters": [ + { + "description": "Token to revoke", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.revokeRequest" + } + } + ], + "responses": { + "200": { + "description": "Token revoked (always 200 per RFC 7009)" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + }, + "/oauth/token": { + "post": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Issues tokens for the authorization_code, client_credentials, refresh_token, and password grant types. The client authenticates with HTTP Basic (client_id:client_secret).", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "oauth" + ], + "summary": "OAuth2 token endpoint", + "parameters": [ + { + "description": "Grant request", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handlers.tokenRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_handlers.oauthTokenResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_handlers.ErrorResponse" + } + } + } + } + } + }, + "definitions": { + "github_com_zhaochy1990_auth-service_internal_domain.InviteCodeKind": { + "type": "string", + "enum": [ + "single_use", + "long_term" + ], + "x-enum-varnames": [ + "InviteSingleUse", + "InviteLongTerm" + ] + }, + "github_com_zhaochy1990_auth-service_internal_domain.MembershipTier": { + "type": "string", + "enum": [ + "regular", + "vip1" + ], + "x-enum-varnames": [ + "MembershipRegular", + "MembershipVip1" + ] + }, + "github_com_zhaochy1990_auth-service_internal_domain.UserType": { + "type": "string", + "enum": [ + "regular", + "testing" + ], + "x-enum-varnames": [ + "UserTypeRegular", + "UserTypeTesting" + ] + }, + "internal_handlers.ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "invalid_credentials" + }, + "message": { + "type": "string", + "example": "Invalid credentials" + } + } + }, + "internal_handlers.StatusResponse": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "ok" + } + } + }, + "internal_handlers.accountResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "provider_account_id": { + "type": "string" + }, + "provider_id": { + "type": "string" + } + } + }, + "internal_handlers.addProviderRequest": { + "type": "object", + "properties": { + "config": { + "type": "array", + "items": { + "type": "integer" + } + }, + "provider_id": { + "type": "string" + } + } + }, + "internal_handlers.adminAddMemberRequest": { + "type": "object", + "properties": { + "role": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "internal_handlers.adminCreateTeamRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "is_open": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "owner_user_id": { + "type": "string" + } + } + }, + "internal_handlers.adminTeamMembershipResponse": { + "type": "object", + "properties": { + "joined_at": { + "type": "string" + }, + "role": { + "type": "string" + }, + "team_id": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "internal_handlers.appStats": { + "type": "object", + "properties": { + "active": { + "type": "integer" + }, + "inactive": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "internal_handlers.applicationResponse": { + "type": "object", + "properties": { + "allowed_scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "client_id": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "redirect_uris": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "internal_handlers.createApplicationRequest": { + "type": "object", + "properties": { + "allowed_scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "redirect_uris": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "internal_handlers.createApplicationResponse": { + "type": "object", + "properties": { + "allowed_scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "client_id": { + "type": "string" + }, + "client_secret": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "redirect_uris": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "internal_handlers.createTeamRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "internal_handlers.createUserRequest": { + "type": "object", + "properties": { + "custom_attributes": { + "type": "object", + "additionalProperties": {} + }, + "email": { + "type": "string" + }, + "membership": { + "$ref": "#/definitions/github_com_zhaochy1990_auth-service_internal_domain.MembershipTier" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "role": { + "type": "string" + }, + "user_type": { + "$ref": "#/definitions/github_com_zhaochy1990_auth-service_internal_domain.UserType" + } + } + }, + "internal_handlers.introspectRequest": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, + "internal_handlers.introspectResponse": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "aud": { + "type": "string" + }, + "exp": { + "type": "integer" + }, + "scope": { + "type": "string" + }, + "sub": { + "type": "string" + } + } + }, + "internal_handlers.inviteCodeResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "created_by": { + "type": "string" + }, + "grants_membership": { + "type": "string" + }, + "grants_membership_days": { + "type": "integer" + }, + "grants_user_type": { + "$ref": "#/definitions/github_com_zhaochy1990_auth-service_internal_domain.UserType" + }, + "id": { + "type": "string" + }, + "is_revoked": { + "type": "boolean" + }, + "kind": { + "$ref": "#/definitions/github_com_zhaochy1990_auth-service_internal_domain.InviteCodeKind" + }, + "used_at": { + "type": "string" + }, + "used_by": { + "type": "string" + } + } + }, + "internal_handlers.linkAccountRequest": { + "type": "object", + "properties": { + "credential": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "internal_handlers.loginRecordResponse": { + "type": "object", + "properties": { + "at": { + "type": "string" + }, + "ip": { + "type": "string" + } + } + }, + "internal_handlers.loginRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "internal_handlers.logoutRequest": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string" + } + } + }, + "internal_handlers.oauthTokenResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "expires_in": { + "type": "integer" + }, + "refresh_token": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "token_type": { + "type": "string" + } + } + }, + "internal_handlers.providerLoginRequest": { + "type": "object", + "properties": { + "credential": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "internal_handlers.providerResponse": { + "type": "object", + "properties": { + "config": { + "type": "array", + "items": { + "type": "integer" + } + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "provider_id": { + "type": "string" + } + } + }, + "internal_handlers.refreshRequest": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string" + } + } + }, + "internal_handlers.registerRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "invite_code": { + "type": "string" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "internal_handlers.registerResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "expires_in": { + "type": "integer" + }, + "refresh_token": { + "type": "string" + }, + "token_type": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "internal_handlers.resetUserPasswordRequest": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "revoke_sessions": { + "type": "boolean" + } + } + }, + "internal_handlers.resetUserPasswordResponse": { + "type": "object", + "properties": { + "revoked_sessions": { + "type": "boolean" + }, + "user_id": { + "type": "string" + } + } + }, + "internal_handlers.revokeRequest": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, + "internal_handlers.rotateSecretResponse": { + "type": "object", + "properties": { + "client_id": { + "type": "string" + }, + "client_secret": { + "type": "string" + } + } + }, + "internal_handlers.statsResponse": { + "type": "object", + "properties": { + "applications": { + "$ref": "#/definitions/internal_handlers.appStats" + }, + "users": { + "$ref": "#/definitions/internal_handlers.userStats" + } + } + }, + "internal_handlers.teamMembershipResponse": { + "type": "object", + "properties": { + "joined_at": { + "type": "string" + }, + "role": { + "type": "string" + }, + "team_id": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "internal_handlers.teamResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_open": { + "type": "boolean" + }, + "member_count": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "owner_user_id": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "internal_handlers.tokenRequest": { + "type": "object", + "properties": { + "code": { + "description": "authorization_code flow", + "type": "string" + }, + "code_verifier": { + "type": "string" + }, + "grant_type": { + "type": "string" + }, + "password": { + "type": "string" + }, + "redirect_uri": { + "type": "string" + }, + "refresh_token": { + "description": "refresh_token flow", + "type": "string" + }, + "scope": { + "description": "common", + "type": "string" + }, + "username": { + "description": "password flow", + "type": "string" + } + } + }, + "internal_handlers.tokenResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "expires_in": { + "type": "integer" + }, + "refresh_token": { + "type": "string" + }, + "token_type": { + "type": "string" + } + } + }, + "internal_handlers.transferOwnerRequest": { + "type": "object", + "properties": { + "new_owner_user_id": { + "type": "string" + } + } + }, + "internal_handlers.updateApplicationRequest": { + "type": "object", + "properties": { + "allowed_scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "is_active": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "redirect_uris": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "internal_handlers.updateProfileRequest": { + "type": "object", + "properties": { + "avatar_url": { + "type": "string" + }, + "custom_attributes": { + "type": "object", + "additionalProperties": {} + }, + "name": { + "type": "string" + } + } + }, + "internal_handlers.updateUserRequest": { + "type": "object", + "properties": { + "custom_attributes": { + "type": "object", + "additionalProperties": {} + }, + "is_active": { + "type": "boolean" + }, + "membership": { + "$ref": "#/definitions/github_com_zhaochy1990_auth-service_internal_domain.MembershipTier" + }, + "membership_expires_at": { + "type": "string" + }, + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "role": { + "type": "string" + }, + "user_type": { + "$ref": "#/definitions/github_com_zhaochy1990_auth-service_internal_domain.UserType" + } + } + }, + "internal_handlers.userAccountResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "provider_account_id": { + "type": "string" + }, + "provider_id": { + "type": "string" + } + } + }, + "internal_handlers.userListResponse": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "per_page": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "users": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handlers.userResponse" + } + } + } + }, + "internal_handlers.userProfileResponse": { + "type": "object", + "properties": { + "avatar_url": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "custom_attributes": { + "type": "object", + "additionalProperties": {} + }, + "email": { + "type": "string" + }, + "email_verified": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "membership": { + "$ref": "#/definitions/github_com_zhaochy1990_auth-service_internal_domain.MembershipTier" + }, + "membership_expires_at": { + "type": "string" + }, + "name": { + "type": "string" + }, + "user_type": { + "$ref": "#/definitions/github_com_zhaochy1990_auth-service_internal_domain.UserType" + } + } + }, + "internal_handlers.userResponse": { + "type": "object", + "properties": { + "avatar_url": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "custom_attributes": { + "type": "object", + "additionalProperties": {} + }, + "email": { + "type": "string" + }, + "email_verified": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "last_login_at": { + "type": "string" + }, + "membership": { + "$ref": "#/definitions/github_com_zhaochy1990_auth-service_internal_domain.MembershipTier" + }, + "membership_expires_at": { + "type": "string" + }, + "name": { + "type": "string" + }, + "note": { + "type": "string" + }, + "recent_logins": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handlers.loginRecordResponse" + } + }, + "role": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "user_type": { + "$ref": "#/definitions/github_com_zhaochy1990_auth-service_internal_domain.UserType" + } + } + }, + "internal_handlers.userStats": { + "type": "object", + "properties": { + "recent": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + } + }, + "securityDefinitions": { + "BasicAuth": { + "type": "basic" + }, + "BearerAuth": { + "description": "\"Bearer \u003cJWT\u003e\" for end-user and admin callers.", + "type": "apiKey", + "name": "Authorization", + "in": "header" + }, + "ClientID": { + "description": "Application client id for the /api/auth/* endpoints.", + "type": "apiKey", + "name": "X-Client-Id", + "in": "header" + } + } +} \ No newline at end of file diff --git a/sources/dev/authentication-go/cmd/auth-service/docs/swagger.yaml b/sources/dev/authentication-go/cmd/auth-service/docs/swagger.yaml new file mode 100644 index 0000000..9b1fa7b --- /dev/null +++ b/sources/dev/authentication-go/cmd/auth-service/docs/swagger.yaml @@ -0,0 +1,2267 @@ +definitions: + github_com_zhaochy1990_auth-service_internal_domain.InviteCodeKind: + enum: + - single_use + - long_term + type: string + x-enum-varnames: + - InviteSingleUse + - InviteLongTerm + github_com_zhaochy1990_auth-service_internal_domain.MembershipTier: + enum: + - regular + - vip1 + type: string + x-enum-varnames: + - MembershipRegular + - MembershipVip1 + github_com_zhaochy1990_auth-service_internal_domain.UserType: + enum: + - regular + - testing + type: string + x-enum-varnames: + - UserTypeRegular + - UserTypeTesting + internal_handlers.ErrorResponse: + properties: + error: + example: invalid_credentials + type: string + message: + example: Invalid credentials + type: string + type: object + internal_handlers.StatusResponse: + properties: + status: + example: ok + type: string + type: object + internal_handlers.accountResponse: + properties: + created_at: + type: string + provider_account_id: + type: string + provider_id: + type: string + type: object + internal_handlers.addProviderRequest: + properties: + config: + items: + type: integer + type: array + provider_id: + type: string + type: object + internal_handlers.adminAddMemberRequest: + properties: + role: + type: string + user_id: + type: string + type: object + internal_handlers.adminCreateTeamRequest: + properties: + description: + type: string + is_open: + type: boolean + name: + type: string + owner_user_id: + type: string + type: object + internal_handlers.adminTeamMembershipResponse: + properties: + joined_at: + type: string + role: + type: string + team_id: + type: string + user_id: + type: string + type: object + internal_handlers.appStats: + properties: + active: + type: integer + inactive: + type: integer + total: + type: integer + type: object + internal_handlers.applicationResponse: + properties: + allowed_scopes: + items: + type: string + type: array + client_id: + type: string + created_at: + type: string + id: + type: string + is_active: + type: boolean + name: + type: string + redirect_uris: + items: + type: string + type: array + type: object + internal_handlers.createApplicationRequest: + properties: + allowed_scopes: + items: + type: string + type: array + name: + type: string + redirect_uris: + items: + type: string + type: array + type: object + internal_handlers.createApplicationResponse: + properties: + allowed_scopes: + items: + type: string + type: array + client_id: + type: string + client_secret: + type: string + id: + type: string + name: + type: string + redirect_uris: + items: + type: string + type: array + type: object + internal_handlers.createTeamRequest: + properties: + description: + type: string + name: + type: string + type: object + internal_handlers.createUserRequest: + properties: + custom_attributes: + additionalProperties: {} + type: object + email: + type: string + membership: + $ref: '#/definitions/github_com_zhaochy1990_auth-service_internal_domain.MembershipTier' + name: + type: string + password: + type: string + role: + type: string + user_type: + $ref: '#/definitions/github_com_zhaochy1990_auth-service_internal_domain.UserType' + type: object + internal_handlers.introspectRequest: + properties: + token: + type: string + type: object + internal_handlers.introspectResponse: + properties: + active: + type: boolean + aud: + type: string + exp: + type: integer + scope: + type: string + sub: + type: string + type: object + internal_handlers.inviteCodeResponse: + properties: + code: + type: string + created_at: + type: string + created_by: + type: string + grants_membership: + type: string + grants_membership_days: + type: integer + grants_user_type: + $ref: '#/definitions/github_com_zhaochy1990_auth-service_internal_domain.UserType' + id: + type: string + is_revoked: + type: boolean + kind: + $ref: '#/definitions/github_com_zhaochy1990_auth-service_internal_domain.InviteCodeKind' + used_at: + type: string + used_by: + type: string + type: object + internal_handlers.linkAccountRequest: + properties: + credential: + items: + type: integer + type: array + type: object + internal_handlers.loginRecordResponse: + properties: + at: + type: string + ip: + type: string + type: object + internal_handlers.loginRequest: + properties: + email: + type: string + password: + type: string + type: object + internal_handlers.logoutRequest: + properties: + refresh_token: + type: string + type: object + internal_handlers.oauthTokenResponse: + properties: + access_token: + type: string + expires_in: + type: integer + refresh_token: + type: string + scope: + type: string + token_type: + type: string + type: object + internal_handlers.providerLoginRequest: + properties: + credential: + items: + type: integer + type: array + type: object + internal_handlers.providerResponse: + properties: + config: + items: + type: integer + type: array + created_at: + type: string + id: + type: string + is_active: + type: boolean + provider_id: + type: string + type: object + internal_handlers.refreshRequest: + properties: + refresh_token: + type: string + type: object + internal_handlers.registerRequest: + properties: + email: + type: string + invite_code: + type: string + name: + type: string + password: + type: string + type: object + internal_handlers.registerResponse: + properties: + access_token: + type: string + expires_in: + type: integer + refresh_token: + type: string + token_type: + type: string + user_id: + type: string + type: object + internal_handlers.resetUserPasswordRequest: + properties: + password: + type: string + revoke_sessions: + type: boolean + type: object + internal_handlers.resetUserPasswordResponse: + properties: + revoked_sessions: + type: boolean + user_id: + type: string + type: object + internal_handlers.revokeRequest: + properties: + token: + type: string + type: object + internal_handlers.rotateSecretResponse: + properties: + client_id: + type: string + client_secret: + type: string + type: object + internal_handlers.statsResponse: + properties: + applications: + $ref: '#/definitions/internal_handlers.appStats' + users: + $ref: '#/definitions/internal_handlers.userStats' + type: object + internal_handlers.teamMembershipResponse: + properties: + joined_at: + type: string + role: + type: string + team_id: + type: string + user_id: + type: string + type: object + internal_handlers.teamResponse: + properties: + created_at: + type: string + description: + type: string + id: + type: string + is_open: + type: boolean + member_count: + type: integer + name: + type: string + owner_user_id: + type: string + updated_at: + type: string + type: object + internal_handlers.tokenRequest: + properties: + code: + description: authorization_code flow + type: string + code_verifier: + type: string + grant_type: + type: string + password: + type: string + redirect_uri: + type: string + refresh_token: + description: refresh_token flow + type: string + scope: + description: common + type: string + username: + description: password flow + type: string + type: object + internal_handlers.tokenResponse: + properties: + access_token: + type: string + expires_in: + type: integer + refresh_token: + type: string + token_type: + type: string + type: object + internal_handlers.transferOwnerRequest: + properties: + new_owner_user_id: + type: string + type: object + internal_handlers.updateApplicationRequest: + properties: + allowed_scopes: + items: + type: string + type: array + is_active: + type: boolean + name: + type: string + redirect_uris: + items: + type: string + type: array + type: object + internal_handlers.updateProfileRequest: + properties: + avatar_url: + type: string + custom_attributes: + additionalProperties: {} + type: object + name: + type: string + type: object + internal_handlers.updateUserRequest: + properties: + custom_attributes: + additionalProperties: {} + type: object + is_active: + type: boolean + membership: + $ref: '#/definitions/github_com_zhaochy1990_auth-service_internal_domain.MembershipTier' + membership_expires_at: + type: string + name: + type: string + note: + type: string + role: + type: string + user_type: + $ref: '#/definitions/github_com_zhaochy1990_auth-service_internal_domain.UserType' + type: object + internal_handlers.userAccountResponse: + properties: + created_at: + type: string + id: + type: string + provider_account_id: + type: string + provider_id: + type: string + type: object + internal_handlers.userListResponse: + properties: + page: + type: integer + per_page: + type: integer + total: + type: integer + users: + items: + $ref: '#/definitions/internal_handlers.userResponse' + type: array + type: object + internal_handlers.userProfileResponse: + properties: + avatar_url: + type: string + created_at: + type: string + custom_attributes: + additionalProperties: {} + type: object + email: + type: string + email_verified: + type: boolean + id: + type: string + membership: + $ref: '#/definitions/github_com_zhaochy1990_auth-service_internal_domain.MembershipTier' + membership_expires_at: + type: string + name: + type: string + user_type: + $ref: '#/definitions/github_com_zhaochy1990_auth-service_internal_domain.UserType' + type: object + internal_handlers.userResponse: + properties: + avatar_url: + type: string + created_at: + type: string + custom_attributes: + additionalProperties: {} + type: object + email: + type: string + email_verified: + type: boolean + id: + type: string + is_active: + type: boolean + last_login_at: + type: string + membership: + $ref: '#/definitions/github_com_zhaochy1990_auth-service_internal_domain.MembershipTier' + membership_expires_at: + type: string + name: + type: string + note: + type: string + recent_logins: + items: + $ref: '#/definitions/internal_handlers.loginRecordResponse' + type: array + role: + type: string + updated_at: + type: string + user_type: + $ref: '#/definitions/github_com_zhaochy1990_auth-service_internal_domain.UserType' + type: object + internal_handlers.userStats: + properties: + recent: + type: integer + total: + type: integer + type: object +info: + contact: {} + description: HTTP Basic auth (client_id:client_secret) for the /oauth/* endpoints. + title: Auth Service API + version: "1.0" +paths: + /admin/applications: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/internal_handlers.applicationResponse' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: List applications + tags: + - admin + post: + consumes: + - application/json + parameters: + - description: Application to create + in: body + name: body + required: true + schema: + $ref: '#/definitions/internal_handlers.createApplicationRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.createApplicationResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Create application + tags: + - admin + /admin/applications/{id}: + patch: + consumes: + - application/json + parameters: + - description: Application ID + in: path + name: id + required: true + type: string + - description: Fields to update + in: body + name: body + required: true + schema: + $ref: '#/definitions/internal_handlers.updateApplicationRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.applicationResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Update application + tags: + - admin + /admin/applications/{id}/providers: + get: + parameters: + - description: Application ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/internal_handlers.providerResponse' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: List application providers + tags: + - admin + post: + consumes: + - application/json + parameters: + - description: Application ID + in: path + name: id + required: true + type: string + - description: Provider to attach + in: body + name: body + required: true + schema: + $ref: '#/definitions/internal_handlers.addProviderRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.providerResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Add application provider + tags: + - admin + /admin/applications/{id}/providers/{provider_id}: + delete: + parameters: + - description: Application ID + in: path + name: id + required: true + type: string + - description: Provider ID + in: path + name: provider_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.StatusResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Remove application provider + tags: + - admin + /admin/applications/{id}/rotate-secret: + post: + parameters: + - description: Application ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.rotateSecretResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Rotate application secret + tags: + - admin + /admin/invite-codes: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/internal_handlers.inviteCodeResponse' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: List invite codes + tags: + - admin + post: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.inviteCodeResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Create invite code + tags: + - admin + /admin/invite-codes/{code}: + delete: + parameters: + - description: Invite code + in: path + name: code + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.StatusResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Revoke invite code + tags: + - admin + /admin/stats: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.statsResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Get admin stats + tags: + - admin + /admin/teams: + post: + consumes: + - application/json + parameters: + - description: Team to create + in: body + name: body + required: true + schema: + $ref: '#/definitions/internal_handlers.adminCreateTeamRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.teamResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Create team (admin) + tags: + - admin + /admin/teams/{id}/members: + post: + consumes: + - application/json + parameters: + - description: Team ID + in: path + name: id + required: true + type: string + - description: Member to add + in: body + name: body + required: true + schema: + $ref: '#/definitions/internal_handlers.adminAddMemberRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.adminTeamMembershipResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Add team member (admin) + tags: + - admin + /admin/teams/{id}/members/{user_id}: + delete: + parameters: + - description: Team ID + in: path + name: id + required: true + type: string + - description: User ID + in: path + name: user_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.StatusResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Remove team member (admin) + tags: + - admin + /admin/users: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.userListResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: List users + tags: + - admin + post: + consumes: + - application/json + parameters: + - description: User to create + in: body + name: body + required: true + schema: + $ref: '#/definitions/internal_handlers.createUserRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.userResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "409": + description: Conflict + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Create user + tags: + - admin + /admin/users/{id}: + delete: + parameters: + - description: User ID + in: path + name: id + required: true + type: string + responses: + "204": + description: No Content + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Delete user + tags: + - admin + get: + parameters: + - description: User ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.userResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Get user + tags: + - admin + patch: + consumes: + - application/json + parameters: + - description: User ID + in: path + name: id + required: true + type: string + - description: Fields to update + in: body + name: body + required: true + schema: + $ref: '#/definitions/internal_handlers.updateUserRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.userResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Update user + tags: + - admin + /admin/users/{id}/accounts: + get: + parameters: + - description: User ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/internal_handlers.userAccountResponse' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: List user accounts + tags: + - admin + /admin/users/{id}/accounts/{provider_id}: + delete: + parameters: + - description: User ID + in: path + name: id + required: true + type: string + - description: Provider ID + in: path + name: provider_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.StatusResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Unlink user account + tags: + - admin + /admin/users/{id}/reset-password: + post: + consumes: + - application/json + parameters: + - description: User ID + in: path + name: id + required: true + type: string + - description: New password + in: body + name: body + required: true + schema: + $ref: '#/definitions/internal_handlers.resetUserPasswordRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.resetUserPasswordResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Reset user password + tags: + - admin + /api/auth/login: + post: + consumes: + - application/json + parameters: + - description: Login credentials + in: body + name: body + required: true + schema: + $ref: '#/definitions/internal_handlers.loginRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.tokenResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - ClientID: [] + summary: Log in with email and password + tags: + - auth + /api/auth/logout: + post: + consumes: + - application/json + parameters: + - description: Refresh token to revoke + in: body + name: body + required: true + schema: + $ref: '#/definitions/internal_handlers.logoutRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.StatusResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - ClientID: [] + summary: Log out (revoke a refresh token) + tags: + - auth + /api/auth/provider/{provider_id}/login: + post: + consumes: + - application/json + description: Authenticates against a configured provider, creating the user + on first sign-in, and returns tokens. + parameters: + - description: Provider id + in: path + name: provider_id + required: true + type: string + - description: Provider credential + in: body + name: body + required: true + schema: + $ref: '#/definitions/internal_handlers.providerLoginRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.tokenResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - ClientID: [] + summary: Log in via an external identity provider + tags: + - auth + /api/auth/refresh: + post: + consumes: + - application/json + parameters: + - description: Refresh token + in: body + name: body + required: true + schema: + $ref: '#/definitions/internal_handlers.refreshRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.tokenResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - ClientID: [] + summary: Rotate a refresh token + tags: + - auth + /api/auth/register: + post: + consumes: + - application/json + description: Creates a password user (optionally invite-gated) and returns access + + refresh tokens. + parameters: + - description: Registration details + in: body + name: body + required: true + schema: + $ref: '#/definitions/internal_handlers.registerRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/internal_handlers.registerResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "409": + description: Conflict + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - ClientID: [] + summary: Register a new password user + tags: + - auth + /api/teams: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: List open teams + tags: + - teams + post: + consumes: + - application/json + parameters: + - description: Team to create + in: body + name: body + required: true + schema: + $ref: '#/definitions/internal_handlers.createTeamRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.teamResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Create a team + tags: + - teams + /api/teams/{team_id}: + delete: + parameters: + - description: Team id + in: path + name: team_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.StatusResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Delete a team + tags: + - teams + get: + parameters: + - description: Team id + in: path + name: team_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.teamResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Get a team + tags: + - teams + /api/teams/{team_id}/join: + post: + parameters: + - description: Team id + in: path + name: team_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.teamMembershipResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Join a team + tags: + - teams + /api/teams/{team_id}/leave: + post: + parameters: + - description: Team id + in: path + name: team_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.StatusResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Leave a team + tags: + - teams + /api/teams/{team_id}/members: + get: + parameters: + - description: Team id + in: path + name: team_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: List team members + tags: + - teams + /api/teams/{team_id}/transfer-owner: + post: + consumes: + - application/json + parameters: + - description: Team id + in: path + name: team_id + required: true + type: string + - description: New owner + in: body + name: body + required: true + schema: + $ref: '#/definitions/internal_handlers.transferOwnerRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.teamResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Transfer team ownership + tags: + - teams + /api/users/me: + delete: + description: Deletes the authenticated user and all dependent rows. Refuses + if the user still owns any team. + produces: + - application/json + responses: + "204": + description: No Content + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "409": + description: Conflict + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Delete the current user's account + tags: + - users + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.userProfileResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Get the current user's profile + tags: + - users + patch: + consumes: + - application/json + parameters: + - description: Fields to update + in: body + name: body + required: true + schema: + $ref: '#/definitions/internal_handlers.updateProfileRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.userProfileResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Update the current user's profile + tags: + - users + /api/users/me/accounts: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/internal_handlers.accountResponse' + type: array + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: List the current user's linked accounts + tags: + - users + /api/users/me/accounts/{provider_id}: + delete: + description: Unlinks a linked provider account. Refuses to remove the user's + last remaining account. + parameters: + - description: Provider id + in: path + name: provider_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.StatusResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Unlink a provider account + tags: + - users + /api/users/me/accounts/{provider_id}/link: + post: + consumes: + - application/json + parameters: + - description: Provider id + in: path + name: provider_id + required: true + type: string + - description: Provider credential + in: body + name: body + required: true + schema: + $ref: '#/definitions/internal_handlers.linkAccountRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.accountResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "409": + description: Conflict + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: Link an external provider account + tags: + - users + /api/users/me/teams: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BearerAuth: [] + summary: List my teams + tags: + - teams + /oauth/introspect: + post: + consumes: + - application/json + parameters: + - description: Token to introspect + in: body + name: body + required: true + schema: + $ref: '#/definitions/internal_handlers.introspectRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.introspectResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BasicAuth: [] + summary: Introspect a token (RFC 7662 subset) + tags: + - oauth + /oauth/revoke: + post: + consumes: + - application/json + parameters: + - description: Token to revoke + in: body + name: body + required: true + schema: + $ref: '#/definitions/internal_handlers.revokeRequest' + produces: + - application/json + responses: + "200": + description: Token revoked (always 200 per RFC 7009) + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BasicAuth: [] + summary: Revoke a token (RFC 7009) + tags: + - oauth + /oauth/token: + post: + consumes: + - application/json + description: Issues tokens for the authorization_code, client_credentials, refresh_token, + and password grant types. The client authenticates with HTTP Basic (client_id:client_secret). + parameters: + - description: Grant request + in: body + name: body + required: true + schema: + $ref: '#/definitions/internal_handlers.tokenRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_handlers.oauthTokenResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_handlers.ErrorResponse' + security: + - BasicAuth: [] + summary: OAuth2 token endpoint + tags: + - oauth +securityDefinitions: + BasicAuth: + type: basic + BearerAuth: + description: '"Bearer " for end-user and admin callers.' + in: header + name: Authorization + type: apiKey + ClientID: + description: Application client id for the /api/auth/* endpoints. + in: header + name: X-Client-Id + type: apiKey +swagger: "2.0" diff --git a/sources/dev/authentication-go/cmd/auth-service/main.go b/sources/dev/authentication-go/cmd/auth-service/main.go index d56844f..d91b903 100644 --- a/sources/dev/authentication-go/cmd/auth-service/main.go +++ b/sources/dev/authentication-go/cmd/auth-service/main.go @@ -1,270 +1,61 @@ -// Command auth-service is the entrypoint for the Go auth microservice. It loads -// config from the environment, opens the configured storage backend, and either -// runs a subcommand or starts the Gin HTTP server. +// Command auth-service is the single entry point for the Go auth microservice. +// Every operation is a cobra subcommand of one binary (built once, deployed as +// different container entrypoints, e.g. `auth-service serve`): +// +// auth-service serve start the Gin HTTP server (default runtime) +// auth-service seed [email] [pw] bootstrap the admin user + dashboard app client +// +// Each subcommand stays thin: load config, open storage, run. All logic lives +// in internal/. +// +// The Swagger general API info below is attached to this file because +// `swag init -g cmd/auth-service/main.go` reads it from the -g entry package. +// +// @title Auth Service API +// @version 1.0 +// @description Authentication microservice: password/provider login, OAuth2 token issuance, user & team management, and admin operations. +// @securityDefinitions.apikey ClientID +// @in header +// @name X-Client-Id +// @description Application client id for the /api/auth/* endpoints. +// @securityDefinitions.apikey BearerAuth +// @in header +// @name Authorization +// @description "Bearer " for end-user and admin callers. +// @securityDefinitions.basic BasicAuth +// @description HTTP Basic auth (client_id:client_secret) for the /oauth/* endpoints. package main import ( - "context" "fmt" "os" - "sort" - "strings" - "github.com/zhaochy1990/x/logger" - - "github.com/zhaochy1990/auth-service/internal/auth" - "github.com/zhaochy1990/auth-service/internal/config" - "github.com/zhaochy1990/auth-service/internal/repository" - "github.com/zhaochy1990/auth-service/internal/repository/aztables" - mysqlrepo "github.com/zhaochy1990/auth-service/internal/repository/mysql" - "github.com/zhaochy1990/auth-service/internal/seed" - "github.com/zhaochy1990/auth-service/internal/server" - "github.com/zhaochy1990/auth-service/internal/storage" + "github.com/spf13/cobra" ) func main() { - log := logger.MustGetLogger(&logger.LoggerConfig{ - Format: config.EnvOr("LOG_FORMAT", "json"), - ServiceName: "auth-service", - Level: config.EnvOr("LOG_LEVEL", "debug"), - }).Sugar() - - args := os.Args - ctx := context.Background() - if len(args) > 1 && args[1] == "migrate-storage" { - runMigrateStorage(ctx, args) - return - } - - cfg, err := config.FromEnv() - if err != nil { - log.Fatalw("failed to load configuration", "error", err) - } - - log.Infow("opening storage backend", "backend", cfg.StorageBackend) - repo, err := storage.Open(ctx, cfg) - if err != nil { - log.Fatalw("failed to open storage", "backend", cfg.StorageBackend, "error", err) - } - log.Infow("storage ready", "backend", cfg.StorageBackend) - - if len(args) > 1 && args[1] == "seed" { - runSeed(ctx, repo, args) - return - } - if len(args) > 1 && args[1] == "migrate" { - runMigrate(ctx, repo) - return - } - jwt, err := auth.NewJWTManager(cfg) - if err != nil { - log.Fatalw("failed to initialize JWT manager", "error", err) - } - - r := server.NewRouter(repo, jwt, cfg) - log.Infow("starting server", "addr", cfg.Addr()) - if err := r.Run(cfg.Addr()); err != nil { - log.Fatalw("server exited", "error", err) - } -} - -func runSeed(ctx context.Context, repo repository.Repository, args []string) { - email := "admin@example.com" - if len(args) > 2 { - email = args[2] - } - var password *string - if len(args) > 3 { - password = &args[3] - } - - fmt.Println("=== Auth Service Bootstrap ===") - fmt.Println() - - result, err := seed.Bootstrap(ctx, repo, email, password) - if err != nil { - fmt.Println("bootstrap failed:", err) - os.Exit(1) - } - - fmt.Printf(" Client ID: %s\n", result.AppClientID) - if result.AppClientSecret != nil { - fmt.Printf(" Client Secret: %s\n", *result.AppClientSecret) - fmt.Println(" (Save this secret — it won't be shown again!)") - } else { - fmt.Println(" Admin Dashboard application already exists.") - } - fmt.Println() - - switch result.UserAction { - case "created": - fmt.Printf("Created admin user: %s\n", email) - case "promoted": - fmt.Printf("Promoted %s to admin role.\n", email) - case "already_admin": - fmt.Printf("User %s is already an admin.\n", email) - } - - fmt.Println() - fmt.Println("=== Bootstrap complete ===") - fmt.Println() - fmt.Println("For frontend .env, set:") - fmt.Printf(" VITE_API_CLIENT_ID=%s\n", result.AppClientID) -} - -func runMigrate(ctx context.Context, repo repository.Repository) { - azRepo, ok := repo.(*aztables.Repository) - if !ok { - fmt.Println("migrate is only needed for the legacy azure_table backend") - return - } - fmt.Println("=== Auth Service Migration ===") - fmt.Println() - kinds, err := azRepo.MigrateInviteCodeKinds(ctx) - if err != nil { - fmt.Println("migration failed:", err) - os.Exit(1) - } - fmt.Printf(" Invite codes backfilled with `kind`: %d\n", kinds) - users, err := azRepo.MigrateUserInviteCodes(ctx) - if err != nil { - fmt.Println("migration failed:", err) - os.Exit(1) - } - fmt.Printf(" Users backfilled with `invite_code`: %d\n", users) - sortIndexes, err := azRepo.MigrateUserSortIndexes(ctx) - if err != nil { - fmt.Println("migration failed:", err) - os.Exit(1) - } - fmt.Printf(" Users indexed for admin list sorting: %d\n", sortIndexes) - fmt.Println() - fmt.Println("=== Migration complete ===") -} - -func runMigrateStorage(ctx context.Context, args []string) { - if len(args) < 3 || args[2] != "azure-to-mysql" { - fmt.Println("usage: auth-service migrate-storage azure-to-mysql [--dry-run] [--clear-target]") - os.Exit(2) - } - dryRun := hasArg(args[3:], "--dry-run") - clearTarget := hasArg(args[3:], "--clear-target") - azureConn := os.Getenv("AZURE_STORAGE_CONNECTION_STRING") - mysqlDSN := os.Getenv("MYSQL_DSN") - mysqlTLSCAPEM := os.Getenv("MYSQL_TLS_CA_PEM") - mysqlTLSCAPath := os.Getenv("MYSQL_TLS_CA_PATH") - if azureConn == "" { - fmt.Println("AZURE_STORAGE_CONNECTION_STRING is required") - os.Exit(2) - } - if mysqlDSN == "" && !dryRun { - fmt.Println("MYSQL_DSN is required unless --dry-run is set") - os.Exit(2) - } - - source, err := aztables.New(azureConn) - if err != nil { - fmt.Println("failed to open Azure Tables source:", err) - os.Exit(1) - } - data, err := source.ExportSnapshot(ctx) - if err != nil { - fmt.Println("failed to export Azure Tables snapshot:", err) - os.Exit(1) - } - - fmt.Println("=== Azure Tables -> MySQL Storage Migration ===") - fmt.Println() - printCounts("exported", data.Counts()) - if dryRun { - fmt.Println() - fmt.Println("Dry run complete; no MySQL rows were written.") - return - } - - target, err := mysqlrepo.NewWithOptions(ctx, mysqlDSN, mysqlrepo.Options{TLSCAPEM: mysqlTLSCAPEM, TLSCAPath: mysqlTLSCAPath}) - if err != nil { - fmt.Println("failed to open MySQL target:", err) - os.Exit(1) - } - defer target.Close() - if clearTarget { - err = target.ReplaceWithSnapshot(ctx, *data) - } else { - counts, err := target.SnapshotCounts(ctx) - if err != nil { - fmt.Println("failed to count MySQL target:", err) - os.Exit(1) - } - if !countsEmpty(counts) { - fmt.Println("MySQL target is not empty; use --clear-target for an atomic replacement during a planned cutover") - printCounts("existing", counts) - os.Exit(1) - } - err = target.ImportSnapshot(ctx, *data) - } - if err != nil { - fmt.Println("failed to import MySQL snapshot:", err) - os.Exit(1) - } - fmt.Println() - counts, err := target.SnapshotCounts(ctx) - if err != nil { - fmt.Println("failed to count MySQL target:", err) - os.Exit(1) - } - printCounts("imported", counts) - if err := compareCounts(data.Counts(), counts); err != nil { - fmt.Println("migration verification failed:", err) - os.Exit(1) - } - fmt.Println() - fmt.Println("Import complete.") -} - -func compareCounts(want, got map[string]int) error { - keys := make([]string, 0, len(want)) - for key := range want { - keys = append(keys, key) - } - sort.Strings(keys) - for _, key := range keys { - if want[key] != got[key] { - return fmt.Errorf("%s count mismatch: exported=%d imported=%d", key, want[key], got[key]) - } - } - return nil -} - -func countsEmpty(counts map[string]int) bool { - for _, n := range counts { - if n != 0 { - return false - } - } - return true -} - -func hasArg(args []string, want string) bool { - for _, arg := range args { - if arg == want { - return true - } - } - return false -} - -func printCounts(label string, counts map[string]int) { - keys := make([]string, 0, len(counts)) - for key := range counts { - keys = append(keys, key) - } - sort.Strings(keys) - prefix := label - if label != "" { - prefix = strings.ToUpper(label[:1]) + label[1:] - } - for _, key := range keys { - fmt.Printf(" %s %-18s %d\n", prefix, key+":", counts[key]) - } + if err := newRootCmd().Execute(); err != nil { + fmt.Fprintln(os.Stderr, "error:", err) + os.Exit(1) + } +} + +// newRootCmd builds the `auth-service` root and attaches every subcommand. +// SilenceErrors/SilenceUsage keep runtime failures to a single "error: ..." +// line (printed by main) instead of cobra also dumping usage. +func newRootCmd() *cobra.Command { + root := &cobra.Command{ + Use: "auth-service", + Short: "Go auth microservice: HTTP server plus seed/migrate maintenance commands", + Long: "auth-service is the unified CLI for the Go auth module. The HTTP server and\n" + + "every maintenance task is a subcommand of one binary; containers set the\n" + + "entrypoint (e.g. `auth-service serve`).", + SilenceErrors: true, + SilenceUsage: true, + } + root.AddCommand( + newServeCmd(), + newSeedCmd(), + ) + return root } diff --git a/sources/dev/authentication-go/cmd/auth-service/main_test.go b/sources/dev/authentication-go/cmd/auth-service/main_test.go deleted file mode 100644 index 4f68287..0000000 --- a/sources/dev/authentication-go/cmd/auth-service/main_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package main - -import "testing" - -func TestCountsEmpty(t *testing.T) { - if !countsEmpty(map[string]int{"users": 0, "applications": 0}) { - t.Fatal("expected zero counts to be empty") - } - if countsEmpty(map[string]int{"users": 1, "applications": 0}) { - t.Fatal("expected non-zero counts to be non-empty") - } -} - -func TestCompareCounts(t *testing.T) { - if err := compareCounts(map[string]int{"users": 1}, map[string]int{"users": 1}); err != nil { - t.Fatalf("matching counts returned error: %v", err) - } - if err := compareCounts(map[string]int{"users": 1}, map[string]int{"users": 2}); err == nil { - t.Fatal("expected mismatched counts to fail") - } -} diff --git a/sources/dev/authentication-go/docker-compose.yml b/sources/dev/authentication-go/docker-compose.yml index 93fa299..eb4b9db 100644 --- a/sources/dev/authentication-go/docker-compose.yml +++ b/sources/dev/authentication-go/docker-compose.yml @@ -20,6 +20,7 @@ services: auth: build: . + command: ["serve"] ports: - "3001:3000" volumes: @@ -32,6 +33,7 @@ services: JWT_PRIVATE_KEY_PATH: "./keys/private.pem" JWT_PUBLIC_KEY_PATH: "./keys/public.pem" CORS_ALLOWED_ORIGINS: "http://localhost:5173" + SWAGGER_ENABLED: "true" LOG_LEVEL: "debug" LOG_FORMAT: "json" depends_on: diff --git a/sources/dev/authentication-go/go.mod b/sources/dev/authentication-go/go.mod index 5183717..af4964c 100644 --- a/sources/dev/authentication-go/go.mod +++ b/sources/dev/authentication-go/go.mod @@ -11,32 +11,47 @@ require ( github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/uuid v1.6.0 github.com/mozillazg/go-pinyin v0.21.0 + github.com/spf13/cobra v1.10.1 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.1 + github.com/swaggo/swag v1.16.6 github.com/zhaochy1990/x v0.0.0-00010101000000-000000000000 ) require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.0 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.19.15 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.30.3 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.4.2 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect @@ -44,10 +59,14 @@ require ( go.uber.org/zap v1.28.0 // indirect golang.org/x/arch v0.22.0 // indirect golang.org/x/crypto v0.53.0 // indirect + golang.org/x/mod v0.36.0 // indirect golang.org/x/net v0.55.0 // indirect + golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/text v0.38.0 // indirect + golang.org/x/tools v0.45.0 // indirect google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) replace github.com/zhaochy1990/x => ../../../../x diff --git a/sources/dev/authentication-go/go.sum b/sources/dev/authentication-go/go.sum index 3e49fa1..c56d0a3 100644 --- a/sources/dev/authentication-go/go.sum +++ b/sources/dev/authentication-go/go.sum @@ -10,6 +10,12 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.0 h1:Bg8m3nq/X1DeePkAbCfb6m github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.0/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w= github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= @@ -20,15 +26,29 @@ github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiD github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -50,14 +70,29 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -67,6 +102,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mozillazg/go-pinyin v0.21.0 h1:Wo8/NT45z7P3er/9YSLHA3/kjZzbLz5hR7i+jGeIGao= github.com/mozillazg/go-pinyin v0.21.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pelletier/go-toml/v2 v2.4.2 h1:M2fKKbmyvI+hGId/D0W64qDBMVhJnNR10O5gIbMc//Q= github.com/pelletier/go-toml/v2 v2.4.2/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= @@ -77,17 +113,32 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY= +github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= @@ -114,18 +165,25 @@ golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -142,6 +200,7 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= @@ -152,10 +211,20 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/sources/dev/authentication-go/internal/config/config.go b/sources/dev/authentication-go/internal/config/config.go index e4ee8dc..28b2552 100644 --- a/sources/dev/authentication-go/internal/config/config.go +++ b/sources/dev/authentication-go/internal/config/config.go @@ -24,6 +24,9 @@ type Config struct { CORSAllowedOrigins string // EnableTestProviders gates the "test" auth provider. Off in production. EnableTestProviders bool + // SwaggerEnabled gates the /swagger UI + spec. Off in production. The UI is + // only served when the binary is also built with `-tags swagger`. + SwaggerEnabled bool } const ( @@ -74,6 +77,7 @@ func FromEnv() (*Config, error) { ServerPort: int(envInt64("SERVER_PORT", 3000)), CORSAllowedOrigins: EnvOr("CORS_ALLOWED_ORIGINS", "http://localhost:5173,http://localhost:3000"), EnableTestProviders: envBool("AUTH_ENABLE_TEST_PROVIDERS", false), + SwaggerEnabled: envBool("SWAGGER_ENABLED", false), }, nil } diff --git a/sources/dev/authentication-go/internal/handlers/admin.go b/sources/dev/authentication-go/internal/handlers/admin.go index 0d77db8..9296525 100644 --- a/sources/dev/authentication-go/internal/handlers/admin.go +++ b/sources/dev/authentication-go/internal/handlers/admin.go @@ -237,6 +237,19 @@ type adminTeamMembershipResponse struct { // --- Application handlers --- // CreateApplication registers a new OAuth2 application. +// +// @Summary Create application +// @Tags admin +// @Accept json +// @Produce json +// @Param body body createApplicationRequest true "Application to create" +// @Success 200 {object} createApplicationResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /admin/applications [post] func (h *Handler) CreateApplication(c *gin.Context) { var req createApplicationRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -277,6 +290,17 @@ func (h *Handler) CreateApplication(c *gin.Context) { } // ListApplications lists all applications. +// +// @Summary List applications +// @Tags admin +// @Produce json +// @Success 200 {array} applicationResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /admin/applications [get] func (h *Handler) ListApplications(c *gin.Context) { apps, err := h.Repo.Applications().FindAll(c.Request.Context()) if err != nil { @@ -303,6 +327,21 @@ func toApplicationResponse(a *domain.Application) applicationResponse { } // UpdateApplication patches an application. +// +// @Summary Update application +// @Tags admin +// @Accept json +// @Produce json +// @Param id path string true "Application ID" +// @Param body body updateApplicationRequest true "Fields to update" +// @Success 200 {object} applicationResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /admin/applications/{id} [patch] func (h *Handler) UpdateApplication(c *gin.Context) { var req updateApplicationRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -342,6 +381,21 @@ func (h *Handler) UpdateApplication(c *gin.Context) { } // AddProvider attaches an auth provider to an application. +// +// @Summary Add application provider +// @Tags admin +// @Accept json +// @Produce json +// @Param id path string true "Application ID" +// @Param body body addProviderRequest true "Provider to attach" +// @Success 200 {object} providerResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /admin/applications/{id}/providers [post] func (h *Handler) AddProvider(c *gin.Context) { var req addProviderRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -385,6 +439,19 @@ func (h *Handler) AddProvider(c *gin.Context) { } // RemoveProvider detaches a provider from an application. +// +// @Summary Remove application provider +// @Tags admin +// @Produce json +// @Param id path string true "Application ID" +// @Param provider_id path string true "Provider ID" +// @Success 200 {object} StatusResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /admin/applications/{id}/providers/{provider_id} [delete] func (h *Handler) RemoveProvider(c *gin.Context) { ctx := c.Request.Context() provider, err := h.Repo.AppProviders().FindByAppAndProvider(ctx, c.Param("id"), c.Param("provider_id")) @@ -404,6 +471,19 @@ func (h *Handler) RemoveProvider(c *gin.Context) { } // RotateSecret rotates an application's client secret. +// +// @Summary Rotate application secret +// @Tags admin +// @Produce json +// @Param id path string true "Application ID" +// @Success 200 {object} rotateSecretResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /admin/applications/{id}/rotate-secret [post] func (h *Handler) RotateSecret(c *gin.Context) { ctx := c.Request.Context() app, err := h.Repo.Applications().FindByID(ctx, c.Param("id")) @@ -426,6 +506,19 @@ func (h *Handler) RotateSecret(c *gin.Context) { } // ListProviders lists an application's providers. +// +// @Summary List application providers +// @Tags admin +// @Produce json +// @Param id path string true "Application ID" +// @Success 200 {array} providerResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /admin/applications/{id}/providers [get] func (h *Handler) ListProviders(c *gin.Context) { ctx := c.Request.Context() appID := c.Param("id") @@ -459,6 +552,17 @@ func (h *Handler) ListProviders(c *gin.Context) { // --- User handlers --- // ListUsers lists users with pagination and optional search. +// +// @Summary List users +// @Tags admin +// @Produce json +// @Success 200 {object} userListResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /admin/users [get] func (h *Handler) ListUsers(c *gin.Context) { page := parseUintDefault(c.Query("page"), 1) if page < 1 { @@ -494,6 +598,19 @@ func (h *Handler) ListUsers(c *gin.Context) { } // GetUser returns a single user. +// +// @Summary Get user +// @Tags admin +// @Produce json +// @Param id path string true "User ID" +// @Success 200 {object} userResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /admin/users/{id} [get] func (h *Handler) GetUser(c *gin.Context) { user, err := h.Repo.Users().FindByID(c.Request.Context(), c.Param("id")) if err != nil { @@ -508,6 +625,19 @@ func (h *Handler) GetUser(c *gin.Context) { } // GetUserAccounts lists a user's linked accounts. +// +// @Summary List user accounts +// @Tags admin +// @Produce json +// @Param id path string true "User ID" +// @Success 200 {array} userAccountResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /admin/users/{id}/accounts [get] func (h *Handler) GetUserAccounts(c *gin.Context) { ctx := c.Request.Context() userID := c.Param("id") @@ -535,6 +665,20 @@ func (h *Handler) GetUserAccounts(c *gin.Context) { } // CreateUser creates a user with a password account. +// +// @Summary Create user +// @Tags admin +// @Accept json +// @Produce json +// @Param body body createUserRequest true "User to create" +// @Success 200 {object} userResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 409 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /admin/users [post] func (h *Handler) CreateUser(c *gin.Context) { var req createUserRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -604,6 +748,21 @@ func (h *Handler) CreateUser(c *gin.Context) { } // UpdateUser patches a user. +// +// @Summary Update user +// @Tags admin +// @Accept json +// @Produce json +// @Param id path string true "User ID" +// @Param body body updateUserRequest true "Fields to update" +// @Success 200 {object} userResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /admin/users/{id} [patch] func (h *Handler) UpdateUser(c *gin.Context) { var req updateUserRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -678,6 +837,18 @@ func (h *Handler) UpdateUser(c *gin.Context) { } // DeleteUser deletes a user account (admin). +// +// @Summary Delete user +// @Tags admin +// @Param id path string true "User ID" +// @Success 204 "No Content" +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /admin/users/{id} [delete] func (h *Handler) DeleteUser(c *gin.Context) { if err := h.deleteUserAccount(c.Request.Context(), c.Param("id")); err != nil { middleware.RespondError(c, err) @@ -687,6 +858,21 @@ func (h *Handler) DeleteUser(c *gin.Context) { } // ResetUserPassword sets a new password for a user, optionally revoking sessions. +// +// @Summary Reset user password +// @Tags admin +// @Accept json +// @Produce json +// @Param id path string true "User ID" +// @Param body body resetUserPasswordRequest true "New password" +// @Success 200 {object} resetUserPasswordResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /admin/users/{id}/reset-password [post] func (h *Handler) ResetUserPassword(c *gin.Context) { var req resetUserPasswordRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -742,6 +928,20 @@ func (h *Handler) ResetUserPassword(c *gin.Context) { } // AdminUnlinkAccount unlinks a provider account from a user (never the last). +// +// @Summary Unlink user account +// @Tags admin +// @Produce json +// @Param id path string true "User ID" +// @Param provider_id path string true "Provider ID" +// @Success 200 {object} StatusResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /admin/users/{id}/accounts/{provider_id} [delete] func (h *Handler) AdminUnlinkAccount(c *gin.Context) { ctx := c.Request.Context() userID := c.Param("id") @@ -781,6 +981,17 @@ func (h *Handler) AdminUnlinkAccount(c *gin.Context) { } // Stats returns application and user counts. +// +// @Summary Get admin stats +// @Tags admin +// @Produce json +// @Success 200 {object} statsResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /admin/stats [get] func (h *Handler) Stats(c *gin.Context) { ctx := c.Request.Context() totalApps, err := h.Repo.Applications().CountAll(ctx) @@ -812,6 +1023,17 @@ func (h *Handler) Stats(c *gin.Context) { // --- Invite code handlers --- // CreateInviteCode mints an invite code. +// +// @Summary Create invite code +// @Tags admin +// @Produce json +// @Success 200 {object} inviteCodeResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /admin/invite-codes [post] func (h *Handler) CreateInviteCode(c *gin.Context) { kind := domain.InviteKindFromString(c.Query("kind")) var grants *domain.MembershipTier @@ -849,6 +1071,17 @@ func (h *Handler) CreateInviteCode(c *gin.Context) { } // ListInviteCodes lists invite codes, optionally filtered by used status. +// +// @Summary List invite codes +// @Tags admin +// @Produce json +// @Success 200 {array} inviteCodeResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /admin/invite-codes [get] func (h *Handler) ListInviteCodes(c *gin.Context) { var used *bool if u := c.Query("used"); u == "true" || u == "false" { @@ -868,6 +1101,18 @@ func (h *Handler) ListInviteCodes(c *gin.Context) { } // RevokeInviteCode revokes an invite code. +// +// @Summary Revoke invite code +// @Tags admin +// @Produce json +// @Param code path string true "Invite code" +// @Success 200 {object} StatusResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /admin/invite-codes/{code} [delete] func (h *Handler) RevokeInviteCode(c *gin.Context) { if err := h.Repo.InviteCodes().Revoke(c.Request.Context(), c.Param("code")); err != nil { middleware.RespondError(c, err) @@ -879,6 +1124,20 @@ func (h *Handler) RevokeInviteCode(c *gin.Context) { // --- Admin team management --- // AdminCreateTeam creates a team on behalf of a user. +// +// @Summary Create team (admin) +// @Tags admin +// @Accept json +// @Produce json +// @Param body body adminCreateTeamRequest true "Team to create" +// @Success 200 {object} teamResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /admin/teams [post] func (h *Handler) AdminCreateTeam(c *gin.Context) { var req adminCreateTeamRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -923,6 +1182,21 @@ func (h *Handler) AdminCreateTeam(c *gin.Context) { } // AdminAddTeamMember adds a user to a team. +// +// @Summary Add team member (admin) +// @Tags admin +// @Accept json +// @Produce json +// @Param id path string true "Team ID" +// @Param body body adminAddMemberRequest true "Member to add" +// @Success 200 {object} adminTeamMembershipResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /admin/teams/{id}/members [post] func (h *Handler) AdminAddTeamMember(c *gin.Context) { var req adminAddMemberRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -980,6 +1254,20 @@ func (h *Handler) AdminAddTeamMember(c *gin.Context) { } // AdminRemoveTeamMember removes a non-owner member from a team. +// +// @Summary Remove team member (admin) +// @Tags admin +// @Produce json +// @Param id path string true "Team ID" +// @Param user_id path string true "User ID" +// @Success 200 {object} StatusResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /admin/teams/{id}/members/{user_id} [delete] func (h *Handler) AdminRemoveTeamMember(c *gin.Context) { ctx := c.Request.Context() teamID := c.Param("id") diff --git a/sources/dev/authentication-go/internal/handlers/auth.go b/sources/dev/authentication-go/internal/handlers/auth.go index 545d53d..4fcdf29 100644 --- a/sources/dev/authentication-go/internal/handlers/auth.go +++ b/sources/dev/authentication-go/internal/handlers/auth.go @@ -59,6 +59,20 @@ type registerResponse struct { // --- Handlers --- // Register creates a password user (optionally invite-gated), and returns tokens. +// +// @Summary Register a new password user +// @Description Creates a password user (optionally invite-gated) and returns access + refresh tokens. +// @Tags auth +// @Accept json +// @Produce json +// @Param body body registerRequest true "Registration details" +// @Success 201 {object} registerResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 409 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security ClientID +// @Router /api/auth/register [post] func (h *Handler) Register(c *gin.Context) { var req registerRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -207,6 +221,19 @@ func (h *Handler) Register(c *gin.Context) { } // Login authenticates a password user and returns tokens. +// +// @Summary Log in with email and password +// @Tags auth +// @Accept json +// @Produce json +// @Param body body loginRequest true "Login credentials" +// @Success 200 {object} tokenResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security ClientID +// @Router /api/auth/login [post] func (h *Handler) Login(c *gin.Context) { var req loginRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -273,6 +300,22 @@ func (h *Handler) Login(c *gin.Context) { // ProviderLogin authenticates via an external provider, creating the user on // first sign-in. +// +// @Summary Log in via an external identity provider +// @Description Authenticates against a configured provider, creating the user on first sign-in, and returns tokens. +// @Tags auth +// @Accept json +// @Produce json +// @Param provider_id path string true "Provider id" +// @Param body body providerLoginRequest true "Provider credential" +// @Success 200 {object} tokenResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security ClientID +// @Router /api/auth/provider/{provider_id}/login [post] func (h *Handler) ProviderLogin(c *gin.Context) { providerID := c.Param("provider_id") var req providerLoginRequest @@ -396,6 +439,20 @@ func (h *Handler) ProviderLogin(c *gin.Context) { } // Refresh rotates a refresh token and issues a new access token. +// +// @Summary Rotate a refresh token +// @Tags auth +// @Accept json +// @Produce json +// @Param body body refreshRequest true "Refresh token" +// @Success 200 {object} tokenResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security ClientID +// @Router /api/auth/refresh [post] func (h *Handler) Refresh(c *gin.Context) { var req refreshRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -437,6 +494,17 @@ func (h *Handler) Refresh(c *gin.Context) { } // Logout revokes a refresh token. +// +// @Summary Log out (revoke a refresh token) +// @Tags auth +// @Accept json +// @Produce json +// @Param body body logoutRequest true "Refresh token to revoke" +// @Success 200 {object} StatusResponse +// @Failure 400 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security ClientID +// @Router /api/auth/logout [post] func (h *Handler) Logout(c *gin.Context) { var req logoutRequest if err := c.ShouldBindJSON(&req); err != nil { diff --git a/sources/dev/authentication-go/internal/handlers/handlers.go b/sources/dev/authentication-go/internal/handlers/handlers.go index b77ae24..f6a216d 100644 --- a/sources/dev/authentication-go/internal/handlers/handlers.go +++ b/sources/dev/authentication-go/internal/handlers/handlers.go @@ -26,6 +26,20 @@ type Handler struct { Cfg *config.Config } +// ErrorResponse is the JSON body returned for every error. It mirrors +// middleware.RespondError: a stable machine-readable `error` code plus a +// human-readable `message`. Referenced by the Swagger `@Failure` annotations. +type ErrorResponse struct { + Error string `json:"error" example:"invalid_credentials"` + Message string `json:"message" example:"Invalid credentials"` +} + +// StatusResponse is the JSON body for endpoints that only acknowledge success +// (e.g. logout, revoke). Referenced by Swagger `@Success` annotations. +type StatusResponse struct { + Status string `json:"status" example:"ok"` +} + // New builds a Handler. func New(repo repository.Repository, jwt *auth.JWTManager, cfg *config.Config) *Handler { return &Handler{Repo: repo, JWT: jwt, Cfg: cfg} diff --git a/sources/dev/authentication-go/internal/handlers/oauth2.go b/sources/dev/authentication-go/internal/handlers/oauth2.go index 02384fb..24cb0a2 100644 --- a/sources/dev/authentication-go/internal/handlers/oauth2.go +++ b/sources/dev/authentication-go/internal/handlers/oauth2.go @@ -55,6 +55,21 @@ type introspectResponse struct { // --- Handlers --- // Token implements the OAuth2 token endpoint (multiple grant types). +// +// @Summary OAuth2 token endpoint +// @Description Issues tokens for the authorization_code, client_credentials, refresh_token, and password grant types. The client authenticates with HTTP Basic (client_id:client_secret). +// @Tags oauth +// @Accept json +// @Produce json +// @Param body body tokenRequest true "Grant request" +// @Success 200 {object} oauthTokenResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BasicAuth +// @Router /oauth/token [post] func (h *Handler) Token(c *gin.Context) { var req tokenRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -263,6 +278,16 @@ func (h *Handler) handlePasswordGrant(c *gin.Context, req *tokenRequest) { } // Revoke revokes a refresh token. Per RFC 7009, always returns 200. +// +// @Summary Revoke a token (RFC 7009) +// @Tags oauth +// @Accept json +// @Produce json +// @Param body body revokeRequest true "Token to revoke" +// @Success 200 "Token revoked (always 200 per RFC 7009)" +// @Failure 400 {object} ErrorResponse +// @Security BasicAuth +// @Router /oauth/revoke [post] func (h *Handler) Revoke(c *gin.Context) { var req revokeRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -274,6 +299,16 @@ func (h *Handler) Revoke(c *gin.Context) { } // Introspect reports whether an access token is active (RFC 7662 subset). +// +// @Summary Introspect a token (RFC 7662 subset) +// @Tags oauth +// @Accept json +// @Produce json +// @Param body body introspectRequest true "Token to introspect" +// @Success 200 {object} introspectResponse +// @Failure 400 {object} ErrorResponse +// @Security BasicAuth +// @Router /oauth/introspect [post] func (h *Handler) Introspect(c *gin.Context) { var req introspectRequest if err := c.ShouldBindJSON(&req); err != nil { diff --git a/sources/dev/authentication-go/internal/handlers/teams.go b/sources/dev/authentication-go/internal/handlers/teams.go index b733383..299df8a 100644 --- a/sources/dev/authentication-go/internal/handlers/teams.go +++ b/sources/dev/authentication-go/internal/handlers/teams.go @@ -74,6 +74,18 @@ func toTeamResponse(t *domain.Team, count uint64) teamResponse { // --- Handlers --- // CreateTeam creates a team owned by the authenticated user. +// +// @Summary Create a team +// @Tags teams +// @Accept json +// @Produce json +// @Param body body createTeamRequest true "Team to create" +// @Success 200 {object} teamResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /api/teams [post] func (h *Handler) CreateTeam(c *gin.Context) { var req createTeamRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -111,6 +123,16 @@ func (h *Handler) CreateTeam(c *gin.Context) { } // ListTeams lists all open teams. +// +// @Summary List open teams +// @Tags teams +// @Produce json +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /api/teams [get] func (h *Handler) ListTeams(c *gin.Context) { ctx := c.Request.Context() teams, err := h.Repo.Teams().FindAllOpen(ctx) @@ -131,6 +153,18 @@ func (h *Handler) ListTeams(c *gin.Context) { } // GetTeam returns a single team. +// +// @Summary Get a team +// @Tags teams +// @Produce json +// @Param team_id path string true "Team id" +// @Success 200 {object} teamResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /api/teams/{team_id} [get] func (h *Handler) GetTeam(c *gin.Context) { ctx := c.Request.Context() team, err := h.Repo.Teams().FindByID(ctx, c.Param("team_id")) @@ -151,6 +185,19 @@ func (h *Handler) GetTeam(c *gin.Context) { } // JoinTeam adds the authenticated user to an open team (idempotent). +// +// @Summary Join a team +// @Tags teams +// @Produce json +// @Param team_id path string true "Team id" +// @Success 200 {object} teamMembershipResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /api/teams/{team_id}/join [post] func (h *Handler) JoinTeam(c *gin.Context) { ctx := c.Request.Context() teamID := c.Param("team_id") @@ -192,6 +239,18 @@ func (h *Handler) JoinTeam(c *gin.Context) { } // LeaveTeam removes the authenticated user from a team. +// +// @Summary Leave a team +// @Tags teams +// @Produce json +// @Param team_id path string true "Team id" +// @Success 200 {object} StatusResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /api/teams/{team_id}/leave [post] func (h *Handler) LeaveTeam(c *gin.Context) { ctx := c.Request.Context() teamID := c.Param("team_id") @@ -234,6 +293,21 @@ func (h *Handler) LeaveTeam(c *gin.Context) { } // TransferOwner transfers team ownership to another member. +// +// @Summary Transfer team ownership +// @Tags teams +// @Accept json +// @Produce json +// @Param team_id path string true "Team id" +// @Param body body transferOwnerRequest true "New owner" +// @Success 200 {object} teamResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /api/teams/{team_id}/transfer-owner [post] func (h *Handler) TransferOwner(c *gin.Context) { var req transferOwnerRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -309,6 +383,19 @@ func (h *Handler) TransferOwner(c *gin.Context) { } // DeleteTeam deletes a team owned by the authenticated user. +// +// @Summary Delete a team +// @Tags teams +// @Produce json +// @Param team_id path string true "Team id" +// @Success 200 {object} StatusResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 403 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /api/teams/{team_id} [delete] func (h *Handler) DeleteTeam(c *gin.Context) { ctx := c.Request.Context() teamID := c.Param("team_id") @@ -337,6 +424,18 @@ func (h *Handler) DeleteTeam(c *gin.Context) { } // ListMembers lists a team's members with name/email. +// +// @Summary List team members +// @Tags teams +// @Produce json +// @Param team_id path string true "Team id" +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /api/teams/{team_id}/members [get] func (h *Handler) ListMembers(c *gin.Context) { ctx := c.Request.Context() teamID := c.Param("team_id") @@ -368,6 +467,15 @@ func (h *Handler) ListMembers(c *gin.Context) { } // ListMyTeams lists the teams the authenticated user belongs to. +// +// @Summary List my teams +// @Tags teams +// @Produce json +// @Success 200 {object} map[string]interface{} +// @Failure 401 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /api/users/me/teams [get] func (h *Handler) ListMyTeams(c *gin.Context) { ctx := c.Request.Context() memberships, err := h.Repo.TeamMemberships().FindAllByUser(ctx, middleware.UserID(c)) diff --git a/sources/dev/authentication-go/internal/handlers/user.go b/sources/dev/authentication-go/internal/handlers/user.go index 14571a3..3273059 100644 --- a/sources/dev/authentication-go/internal/handlers/user.go +++ b/sources/dev/authentication-go/internal/handlers/user.go @@ -49,6 +49,16 @@ type linkAccountRequest struct { // --- Handlers --- // GetProfile returns the authenticated user's profile. +// +// @Summary Get the current user's profile +// @Tags users +// @Produce json +// @Success 200 {object} userProfileResponse +// @Failure 401 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /api/users/me [get] func (h *Handler) GetProfile(c *gin.Context) { ctx := c.Request.Context() user, err := h.Repo.Users().FindByID(ctx, middleware.UserID(c)) @@ -76,6 +86,19 @@ func (h *Handler) GetProfile(c *gin.Context) { } // UpdateProfile updates the authenticated user's name/avatar. +// +// @Summary Update the current user's profile +// @Tags users +// @Accept json +// @Produce json +// @Param body body updateProfileRequest true "Fields to update" +// @Success 200 {object} userProfileResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /api/users/me [patch] func (h *Handler) UpdateProfile(c *gin.Context) { var req updateProfileRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -122,6 +145,15 @@ func (h *Handler) UpdateProfile(c *gin.Context) { } // ListAccounts lists the authenticated user's linked accounts. +// +// @Summary List the current user's linked accounts +// @Tags users +// @Produce json +// @Success 200 {array} accountResponse +// @Failure 401 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /api/users/me/accounts [get] func (h *Handler) ListAccounts(c *gin.Context) { accounts, err := h.Repo.Accounts().FindAllByUser(c.Request.Context(), middleware.UserID(c)) if err != nil { @@ -140,6 +172,21 @@ func (h *Handler) ListAccounts(c *gin.Context) { } // LinkAccount links an external provider account to the authenticated user. +// +// @Summary Link an external provider account +// @Tags users +// @Accept json +// @Produce json +// @Param provider_id path string true "Provider id" +// @Param body body linkAccountRequest true "Provider credential" +// @Success 200 {object} accountResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 409 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /api/users/me/accounts/{provider_id}/link [post] func (h *Handler) LinkAccount(c *gin.Context) { providerID := c.Param("provider_id") var req linkAccountRequest @@ -222,6 +269,18 @@ func (h *Handler) LinkAccount(c *gin.Context) { } // UnlinkAccount unlinks a provider account (never the last one). +// +// @Summary Unlink a provider account +// @Description Unlinks a linked provider account. Refuses to remove the user's last remaining account. +// @Tags users +// @Produce json +// @Param provider_id path string true "Provider id" +// @Success 200 {object} StatusResponse +// @Failure 400 {object} ErrorResponse +// @Failure 401 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /api/users/me/accounts/{provider_id} [delete] func (h *Handler) UnlinkAccount(c *gin.Context) { providerID := c.Param("provider_id") ctx := c.Request.Context() @@ -255,6 +314,18 @@ func (h *Handler) UnlinkAccount(c *gin.Context) { } // DeleteMe deletes the authenticated user's account. +// +// @Summary Delete the current user's account +// @Description Deletes the authenticated user and all dependent rows. Refuses if the user still owns any team. +// @Tags users +// @Produce json +// @Success 204 "No Content" +// @Failure 401 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 409 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Security BearerAuth +// @Router /api/users/me [delete] func (h *Handler) DeleteMe(c *gin.Context) { if err := h.deleteUserAccount(c.Request.Context(), middleware.UserID(c)); err != nil { middleware.RespondError(c, err) diff --git a/sources/dev/authentication-go/internal/repository/aztables/aztables.go b/sources/dev/authentication-go/internal/repository/aztables/aztables.go index dce27a2..87e930e 100644 --- a/sources/dev/authentication-go/internal/repository/aztables/aztables.go +++ b/sources/dev/authentication-go/internal/repository/aztables/aztables.go @@ -964,19 +964,6 @@ func (r *userRepo) listUnfilteredPage(ctx context.Context, indexes []indexEntity return out, total, nil } -func (r *userRepo) migrateSortIndexes(ctx context.Context) (int, error) { - es, err := queryEntities[userEntity](ctx, r.c, "PartitionKey eq 'user'") - if err != nil { - return 0, err - } - for i := range es { - if err := r.upsertSortIndexes(ctx, &es[i]); err != nil { - return i, err - } - } - return len(es), nil -} - // ─── Application ───────────────────────────────────────────────────────────── type appEntity struct { @@ -1845,60 +1832,4 @@ func (r *teamMembershipRepo) DeleteAllByUser(ctx context.Context, userID string) return nil } -// ─── Migrations ────────────────────────────────────────────────────────────── - -// MigrateInviteCodeKinds backfills the `kind` field on every invite-code row. -func (r *Repository) MigrateInviteCodeKinds(ctx context.Context) (int, error) { - es, err := queryEntities[inviteCodeEntity](ctx, r.inviteCodes, "PartitionKey eq 'invite_code'") - if err != nil { - return 0, err - } - count := 0 - for i := range es { - if es[i].Kind == "" { - es[i].Kind = string(domain.InviteSingleUse) - } - if err := upsertEntity(ctx, r.inviteCodes, &es[i]); err != nil { - return count, err - } - count++ - } - return count, nil -} - -// MigrateUserInviteCodes backfills users.invite_code from each single-use -// code's used_by linkage. -func (r *Repository) MigrateUserInviteCodes(ctx context.Context) (int, error) { - codes, err := queryEntities[inviteCodeEntity](ctx, r.inviteCodes, "PartitionKey eq 'invite_code'") - if err != nil { - return 0, err - } - count := 0 - for _, c := range codes { - if c.UsedBy == nil { - continue - } - var ue userEntity - ok, err := getEntity(ctx, r.users, "user", *c.UsedBy, &ue) - if err != nil { - return count, err - } - if !ok || ue.InviteCode != nil { - continue - } - rowKey := c.RowKey - ue.InviteCode = &rowKey - if err := upsertEntity(ctx, r.users, &ue); err != nil { - return count, err - } - count++ - } - return count, nil -} - -// MigrateUserSortIndexes backfills the admin user-list sort indexes. -func (r *Repository) MigrateUserSortIndexes(ctx context.Context) (int, error) { - return r.userRepo.migrateSortIndexes(ctx) -} - var _ repository.Repository = (*Repository)(nil) diff --git a/sources/dev/authentication-go/internal/server/server.go b/sources/dev/authentication-go/internal/server/server.go index 8fc8ea4..2eb67d6 100644 --- a/sources/dev/authentication-go/internal/server/server.go +++ b/sources/dev/authentication-go/internal/server/server.go @@ -23,6 +23,10 @@ func NewRouter(repo repository.Repository, jwt *auth.JWTManager, cfg *config.Con r.Use(gin.Recovery()) r.Use(middleware.CORS(cfg.CORSAllowedOrigins)) + // Swagger UI at /swagger/*any (only when built with `-tags swagger` and + // SwaggerEnabled is set; a no-op stub otherwise). + mountSwagger(r, cfg.SwaggerEnabled) + h := handlers.New(repo, jwt, cfg) am := &middleware.Auth{Repo: repo, JWT: jwt} diff --git a/sources/dev/authentication-go/internal/server/swagger.go b/sources/dev/authentication-go/internal/server/swagger.go new file mode 100644 index 0000000..1e50c11 --- /dev/null +++ b/sources/dev/authentication-go/internal/server/swagger.go @@ -0,0 +1,23 @@ +//go:build swagger + +package server + +import ( + "github.com/gin-gonic/gin" + swaggerfiles "github.com/swaggo/files" + ginswagger "github.com/swaggo/gin-swagger" + + // docs is generated by `swag init` (see the Makefile `swagger` target) and + // committed so the `-tags swagger` build is self-contained. + _ "github.com/zhaochy1990/auth-service/cmd/auth-service/docs" +) + +// mountSwagger serves the Swagger UI at /swagger/*any when enabled. Compiled +// only with `-tags swagger`, which requires the generated +// cmd/auth-service/docs package. +func mountSwagger(r *gin.Engine, enabled bool) { + if !enabled { + return + } + r.GET("/swagger/*any", ginswagger.WrapHandler(swaggerfiles.Handler)) +} diff --git a/sources/dev/authentication-go/internal/server/swagger_stub.go b/sources/dev/authentication-go/internal/server/swagger_stub.go new file mode 100644 index 0000000..78d75fd --- /dev/null +++ b/sources/dev/authentication-go/internal/server/swagger_stub.go @@ -0,0 +1,10 @@ +//go:build !swagger + +package server + +import "github.com/gin-gonic/gin" + +// mountSwagger is a no-op in the default build. The Swagger UI + generated docs +// package are compiled in only with `-tags swagger` (after `swag init` has run), +// so plain `go build`/`go test` never need the generated code. +func mountSwagger(_ *gin.Engine, _ bool) {}