@nakedev/go-scaffold 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +4 -0
  2. package/dist/commands/auth.js +25 -13
  3. package/dist/commands/method.js +2 -0
  4. package/dist/templates/auth-manifest.js +3 -0
  5. package/dist/utils/auth-patcher.js +10 -1
  6. package/dist/utils/hexagonal-method-patcher.js +12 -1
  7. package/package.json +1 -1
  8. package/templates/add/auth/docs/schemas.yaml.hbs +45 -3
  9. package/templates/add/auth/docs/users-me-identities.yaml.hbs +13 -0
  10. package/templates/add/auth/docs/users-me-identity-link-exchange.yaml.hbs +26 -0
  11. package/templates/add/auth/docs/users-me-identity-link.yaml.hbs +25 -0
  12. package/templates/add/auth/docs/users-me-identity-local-link.yaml.hbs +16 -0
  13. package/templates/add/auth/docs/users-me-identity.yaml.hbs +15 -0
  14. package/templates/add/auth/internal/app/user/adapters/inbound/http/dto.go.hbs +45 -3
  15. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler.go.hbs +7 -1
  16. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_identity.go.hbs +107 -0
  17. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_test.go.hbs +21 -0
  18. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/mfa_store_test.go.hbs +1 -1
  19. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/model.go.hbs +40 -17
  20. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/repository.go.hbs +253 -53
  21. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/repository_test.go.hbs +11 -8
  22. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_pg.go.hbs +4 -2
  23. package/templates/add/auth/internal/app/user/application/dto.go.hbs +14 -0
  24. package/templates/add/auth/internal/app/user/application/errors.go.hbs +20 -0
  25. package/templates/add/auth/internal/app/user/application/external_login.go.hbs +47 -18
  26. package/templates/add/auth/internal/app/user/application/identities.go.hbs +135 -0
  27. package/templates/add/auth/internal/app/user/application/identities_test.go.hbs +102 -0
  28. package/templates/add/auth/internal/app/user/application/local_auth.go.hbs +3 -0
  29. package/templates/add/auth/internal/app/user/application/oauth.go.hbs +1 -0
  30. package/templates/add/auth/internal/app/user/application/provider_test.go.hbs +2 -0
  31. package/templates/add/auth/internal/app/user/application/recovery.go.hbs +3 -0
  32. package/templates/add/auth/internal/app/user/application/service.go.hbs +16 -0
  33. package/templates/add/auth/internal/app/user/application/service_test.go.hbs +75 -4
  34. package/templates/add/auth/internal/app/user/application/user_query.go.hbs +3 -0
  35. package/templates/add/auth/internal/app/user/domain/entity.go.hbs +1 -0
  36. package/templates/add/auth/internal/app/user/domain/errors.go.hbs +4 -1
  37. package/templates/add/auth/internal/app/user/ports/repository.go.hbs +4 -1
  38. package/templates/add/auth/internal/platform/authprovider/google/google.go.hbs +1 -0
  39. package/templates/add/auth/internal/shared/middleware/ratelimit_redis.go.hbs +7 -1
  40. package/templates/add/auth/migrations/create_external_identities.down.sql.hbs +1 -0
  41. package/templates/add/auth/migrations/create_external_identities.up.sql.hbs +12 -0
  42. package/templates/add/auth/migrations/create_password_credentials.down.sql.hbs +1 -0
  43. package/templates/add/auth/migrations/create_password_credentials.up.sql.hbs +11 -0
  44. package/templates/add/auth/migrations/create_user_emails.down.sql.hbs +1 -0
  45. package/templates/add/auth/migrations/create_user_emails.up.sql.hbs +14 -0
  46. package/templates/add/auth/migrations/create_users.up.sql.hbs +0 -11
  47. package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +9 -0
  48. package/templates/create/base/AGENTS.md.hbs +14 -0
  49. package/templates/create/base/README.md.hbs +7 -0
  50. package/templates/create/features/docs/architecture.md.hbs +5 -5
  51. package/templates/create/features/docs/techstack.md.hbs +1 -1
  52. package/templates/add/auth/migrations/create_identities.down.sql.hbs +0 -1
  53. package/templates/add/auth/migrations/create_identities.up.sql.hbs +0 -15
@@ -0,0 +1,11 @@
1
+ CREATE TABLE user_svc.password_credentials (
2
+ id UUID PRIMARY KEY,
3
+ user_id UUID NOT NULL REFERENCES user_svc.users(id) ON DELETE CASCADE,
4
+ password_hash TEXT NOT NULL,
5
+ hash_algorithm VARCHAR(32) NOT NULL DEFAULT 'bcrypt',
6
+ password_changed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
7
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
8
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
9
+ );
10
+
11
+ CREATE UNIQUE INDEX idx_password_credentials_user ON user_svc.password_credentials (user_id);
@@ -0,0 +1 @@
1
+ DROP TABLE IF EXISTS user_svc.user_emails;
@@ -0,0 +1,14 @@
1
+ CREATE TABLE user_svc.user_emails (
2
+ id UUID PRIMARY KEY,
3
+ user_id UUID NOT NULL REFERENCES user_svc.users(id) ON DELETE CASCADE,
4
+ email VARCHAR(255) NOT NULL,
5
+ email_normalized VARCHAR(255) NOT NULL,
6
+ is_primary BOOLEAN NOT NULL DEFAULT true,
7
+ verified_at TIMESTAMPTZ,
8
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
9
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
10
+ );
11
+
12
+ CREATE UNIQUE INDEX idx_user_emails_normalized ON user_svc.user_emails (email_normalized);
13
+ CREATE UNIQUE INDEX idx_user_emails_primary ON user_svc.user_emails (user_id) WHERE is_primary;
14
+ CREATE INDEX idx_user_emails_user ON user_svc.user_emails (user_id);
@@ -2,20 +2,9 @@ CREATE SCHEMA IF NOT EXISTS user_svc;
2
2
 
3
3
  CREATE TABLE user_svc.users (
4
4
  id UUID PRIMARY KEY,
5
- email VARCHAR(255) NOT NULL,
6
5
  name VARCHAR(255) NOT NULL DEFAULT '',
7
6
  avatar_url TEXT NOT NULL DEFAULT '',
8
- email_verified BOOLEAN NOT NULL DEFAULT false,
9
7
  role VARCHAR(20) NOT NULL DEFAULT 'staff',
10
8
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
11
9
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
12
10
  );
13
-
14
- -- A unique INDEX, not an inline UNIQUE constraint, and named to match the
15
- -- `uniqueIndex:idx_users_email` tag on the outbound persistence model exactly. Postgres names an
16
- -- inline UNIQUE "users_email_key"; the development bootstrap then sees
17
- -- uniqueness it didn't create and tries to drop it under its own
18
- -- name, which fails the boot with
19
- -- constraint "uni_users_email" of relation "users" does not exist
20
- -- for anyone who ran `make migrate-up` first — which `add rbac` tells you to.
21
- CREATE UNIQUE INDEX idx_users_email ON user_svc.users (email);
@@ -260,6 +260,15 @@ boundary. For browser OAuth, preserve this client-owned callback contract:
260
260
  `Pragma: no-cache`.
261
261
  Never accept a request-supplied redirect destination or place an access token,
262
262
  refresh token, authorization code, or state in a URI.
263
+ - Authenticated identity management is a separate account flow, not another
264
+ login endpoint: list safe metadata at `GET /users/me/identities`, start and
265
+ finish linking at `POST /users/me/identities/:provider/link` and
266
+ `POST /users/me/identities/:provider/link/exchange`, and unlink with
267
+ `DELETE /users/me/identities/:provider`. Bind the server-side transaction to
268
+ the caller's user ID, never issue or replace a session during link exchange,
269
+ keep one identity per provider per user, and reject unlinking the last login
270
+ method. Provider subjects and password hashes must never cross the HTTP
271
+ boundary.
263
272
  - Map provider and validation failures to controlled public codes only:
264
273
  `oauth_denied`, `oauth_state_invalid`, `oauth_provider_unavailable`, and
265
274
  `oauth_failed`. Do not expose raw provider descriptions or technical causes.
@@ -186,6 +186,20 @@ approval before changing their contract.
186
186
  rotation; responses expose only User-Agent and lifecycle timestamps, never
187
187
  refresh tokens or token hashes. The current access token carries the session
188
188
  ID as `sid` so the adapter can mark the current device.
189
+ - Authenticated users can manage login identities through
190
+ `GET /users/me/identities`, `POST /users/me/identities/local`, and
191
+ `POST /users/me/identities/:provider/link`,
192
+ `POST /users/me/identities/:provider/link/exchange`, and
193
+ `DELETE /users/me/identities/:provider`. The link transaction is bound to
194
+ the current user as well as provider/state/PKCE/nonce; it can never create a
195
+ session or silently move a provider account between users. Return only safe
196
+ provider metadata, allow at most one identity per provider per user, and
197
+ refuse to unlink the last remaining login method.
198
+ - Auth persistence keeps account profile, email addresses, password
199
+ credentials, and external provider identities in separate tables. Resolve an
200
+ external login by `(issuer, subject)`; never use an email address as the
201
+ provider identity key. Adding a password to a Google/OIDC account must write
202
+ a password credential for that existing user.
189
203
  - Password reset and email verification consume a one-time token in the same
190
204
  retry-safe transaction as the user/identity update. A failed post-commit
191
205
  session revocation must not make a successful reset impossible to retry.
@@ -198,6 +198,13 @@ Authenticated users can list active device sessions with
198
198
  `GET /users/me/sessions` and revoke an individual session with
199
199
  `DELETE /users/me/sessions/:id`; only User-Agent and session lifecycle
200
200
  timestamps are returned.
201
+ They can also list safe login-provider metadata with
202
+ `GET /users/me/identities`, link a provider through the authenticated
203
+ `/users/me/identities/:provider/link` start/exchange flow, and unlink a
204
+ provider with `DELETE /users/me/identities/:provider`. For a Google/OIDC-only
205
+ account, `POST /users/me/identities/local` adds an email/password credential to
206
+ the same account; it does not create a second user. The last login method
207
+ cannot be removed.
201
208
  For `cross-site`, configure HTTPS origins, `COOKIE_SAMESITE=none`,
202
209
  `COOKIE_SECURE=true`, and an exact allowed Origin; the API applies an Origin
203
210
  guard independently of CORS.
@@ -60,7 +60,7 @@ internal/
60
60
  ```
61
61
 
62
62
  Installed process and platform additions:
63
- {{#if auth}}- `cmd/seed` and `platform/authprovider/google/` for user bootstrap and Google OAuth/OIDC
63
+ {{#if auth}}- `cmd/seed` and `platform/authprovider/google/` for user bootstrap, Google OAuth/OIDC login, and authenticated identity linking
64
64
  {{/if}}{{#if worker}}- `cmd/worker` and `platform/{mail,queue}/` for queue/SMTP jobs{{#if (eq queue "asynq")}}, plus `platform/cache/` for Redis{{/if}}
65
65
  {{/if}}{{#if observability}}- `platform/telemetry/` for the OTLP exporter
66
66
  {{/if}}{{#if rbac}}- `shared/middleware/authz.go` and `app/role/` for RBAC policy
@@ -73,10 +73,10 @@ a real external system.
73
73
  add a `shared/utils` or similarly generic package — name it after what it
74
74
  actually does.
75
75
 
76
- Optional feature boundaries are explicit: auth owns user authentication and
77
- provider ports; RBAC owns role policy and authorization; worker owns queue and
78
- mail adapters; observability owns metrics/tracing setup. They do not create a
79
- second domain architecture.
76
+ Optional feature boundaries are explicit: auth owns user authentication,
77
+ identity linking, and provider ports; RBAC owns role policy and authorization;
78
+ worker owns queue and mail adapters; observability owns metrics/tracing setup.
79
+ They do not create a second domain architecture.
80
80
 
81
81
  ## 3. API Style
82
82
 
@@ -15,7 +15,7 @@
15
15
  | Logging | `log/slog`, JSON handler |
16
16
  | Migrations | [golang-migrate](https://github.com/golang-migrate/migrate) |
17
17
  | Testing | stdlib `testing`, real Postgres for integration tests |
18
- | Authentication | {{#if auth}}JWT access/refresh sessions, password recovery, email verification, provider login, and MFA{{else}}optional via `go-scaffold add auth`{{/if}} |
18
+ | Authentication | {{#if auth}}JWT access/refresh sessions, password recovery, email verification, provider login/linking, and MFA{{else}}optional via `go-scaffold add auth`{{/if}} |
19
19
  | Authorization | {{#if rbac}}RBAC role/permission middleware{{else}}optional RBAC via `go-scaffold add rbac`{{/if}} |
20
20
  | Background jobs | {{#if worker}}{{queue}} queue backend with SMTP mail and `cmd/worker`{{else}}optional via `go-scaffold add worker`{{/if}} |
21
21
 
@@ -1 +0,0 @@
1
- DROP TABLE IF EXISTS user_svc.identities;
@@ -1,15 +0,0 @@
1
- CREATE TABLE user_svc.identities (
2
- id UUID PRIMARY KEY,
3
- user_id UUID NOT NULL REFERENCES user_svc.users(id) ON DELETE CASCADE,
4
- provider VARCHAR(20) NOT NULL,
5
- password_hash TEXT,
6
- provider_uid TEXT,
7
- created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
8
- updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
9
- );
10
-
11
- -- Named unique indexes matching model.Identity's own
12
- -- `uniqueIndex:idx_identities_*` tags — see create_users.up.sql for why an
13
- -- anonymous UNIQUE constraint breaks schema parity.
14
- CREATE UNIQUE INDEX idx_identities_user_provider ON user_svc.identities (user_id, provider);
15
- CREATE UNIQUE INDEX idx_identities_provider_uid ON user_svc.identities (provider, provider_uid);