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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions sources/dev/authentication-go/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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.
Expand Down Expand Up @@ -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`).
13 changes: 10 additions & 3 deletions sources/dev/authentication-go/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"]
43 changes: 43 additions & 0 deletions sources/dev/authentication-go/Makefile
Original file line number Diff line number Diff line change
@@ -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
65 changes: 42 additions & 23 deletions sources/dev/authentication-go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,6 +31,17 @@ internal/
seed/ admin bootstrap
```

## CLI

The service is one binary with cobra subcommands (`auth-service <command>`):

| 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 <command> --help` for flags.

## Build, Test, Run

Start local MySQL:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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

Expand Down Expand Up @@ -135,14 +148,18 @@ 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
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 |
Expand All @@ -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` |
Expand All @@ -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) |
77 changes: 77 additions & 0 deletions sources/dev/authentication-go/cmd/auth-service/cmd_seed.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading