@nakedev/go-scaffold 0.4.0 → 0.5.0
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.
- package/README.md +598 -306
- package/dist/commands/auth.js +65 -23
- package/dist/commands/check.js +281 -0
- package/dist/commands/config.js +50 -0
- package/dist/commands/create.js +33 -2
- package/dist/commands/generate.js +29 -3
- package/dist/commands/method.js +74 -63
- package/dist/commands/migration.js +2 -2
- package/dist/commands/observability.js +4 -53
- package/dist/commands/rbac.js +21 -10
- package/dist/commands/undo.js +11 -3
- package/dist/commands/worker.js +15 -5
- package/dist/index.js +198 -59
- package/dist/prompts/auth-wizard.js +40 -6
- package/dist/prompts/create-wizard.js +42 -1
- package/dist/prompts/generate-wizard.js +89 -9
- package/dist/templates/auth-manifest.js +50 -19
- package/dist/templates/create-manifest.js +8 -0
- package/dist/templates/module-manifest.js +84 -26
- package/dist/templates/rbac-manifest.js +16 -11
- package/dist/templates/worker-manifest.js +4 -1
- package/dist/types.js +8 -0
- package/dist/utils/auth-patcher.js +124 -33
- package/dist/utils/config.js +167 -4
- package/dist/utils/docs-patcher.js +68 -0
- package/dist/utils/hexagonal-method-patcher.js +334 -0
- package/dist/utils/main-patcher.js +32 -30
- package/dist/utils/marker-patch.js +7 -1
- package/dist/utils/module-location.js +17 -11
- package/dist/utils/module-profile.js +32 -0
- package/dist/utils/platform-patcher.js +56 -7
- package/dist/utils/rbac-patcher.js +89 -210
- package/package.json +7 -2
- package/templates/add/auth/cmd/seed/main.go.hbs +15 -3
- package/templates/add/auth/docs/login.yaml.hbs +11 -1
- package/templates/add/auth/docs/mfa-verify.yaml.hbs +19 -0
- package/templates/add/auth/docs/provider-exchange.yaml.hbs +40 -0
- package/templates/add/auth/docs/provider-login.yaml.hbs +31 -0
- package/templates/add/auth/docs/refresh.yaml.hbs +7 -0
- package/templates/add/auth/docs/register.yaml.hbs +7 -0
- package/templates/add/auth/docs/reset-password.yaml.hbs +1 -1
- package/templates/add/auth/docs/schemas.yaml.hbs +59 -1
- package/templates/add/auth/docs/users-me-mfa-confirm.yaml.hbs +19 -0
- package/templates/add/auth/docs/users-me-mfa-disable.yaml.hbs +15 -0
- package/templates/add/auth/docs/users-me-mfa-setup.yaml.hbs +14 -0
- package/templates/add/auth/docs/users-me-mfa.yaml.hbs +12 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/browser_policy.go.hbs +98 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/dto.go.hbs +159 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler.go.hbs +228 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_local.go.hbs +76 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_mfa.go.hbs +83 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_oauth.go.hbs +70 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_recovery.go.hbs +49 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_test.go.hbs +311 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_user.go.hbs +41 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/session_cookie.go.hbs +35 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt.go.hbs +35 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt_test.go.hbs +20 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/mfa_store.go.hbs +129 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/mfa_store_test.go.hbs +174 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/model.go.hbs +84 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/repository.go.hbs +211 -0
- package/templates/add/auth/internal/app/user/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +18 -19
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_pg.go.hbs +213 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_pg_test.go.hbs +103 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_recovery.go.hbs +84 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/redis/tokenstore.go.hbs +228 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/redis/tokenstore_test.go.hbs +196 -0
- package/templates/add/auth/internal/app/user/application/contracts.go.hbs +52 -0
- package/templates/add/auth/internal/app/user/application/dto.go.hbs +75 -0
- package/templates/add/auth/internal/app/user/application/errors.go.hbs +62 -0
- package/templates/add/auth/internal/app/user/application/external_login.go.hbs +198 -0
- package/templates/add/auth/internal/app/user/application/jwt.go.hbs +58 -0
- package/templates/add/auth/internal/app/user/application/local_auth.go.hbs +96 -0
- package/templates/add/auth/internal/app/user/application/mfa_service.go.hbs +449 -0
- package/templates/add/auth/internal/app/user/application/mfa_service_test.go.hbs +200 -0
- package/templates/add/auth/internal/app/user/application/oauth.go.hbs +132 -0
- package/templates/add/auth/internal/app/user/application/provider_test.go.hbs +285 -0
- package/templates/add/auth/internal/app/user/application/recovery.go.hbs +82 -0
- package/templates/add/auth/internal/app/user/application/recovery_service.go.hbs +112 -0
- package/templates/add/auth/internal/app/user/application/service.go.hbs +145 -0
- package/templates/add/auth/internal/app/user/application/service_test.go.hbs +891 -0
- package/templates/add/auth/internal/app/user/application/sessions.go.hbs +99 -0
- package/templates/add/auth/internal/app/user/application/tokenstore_ports.go.hbs +14 -0
- package/templates/add/auth/internal/app/user/application/user_query.go.hbs +65 -0
- package/templates/add/auth/internal/app/user/composition.go.hbs +168 -0
- package/templates/add/auth/internal/app/user/domain/entity.go.hbs +41 -0
- package/templates/add/auth/internal/app/user/domain/errors.go.hbs +32 -0
- package/templates/add/auth/internal/app/user/ports/password.go.hbs +9 -0
- package/templates/add/auth/internal/app/user/ports/repository.go.hbs +90 -0
- package/templates/add/auth/internal/platform/authprovider/google/google.go.hbs +389 -0
- package/templates/add/auth/internal/platform/authprovider/google/google_test.go.hbs +312 -0
- package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +10 -5
- package/templates/add/auth/migrations/create_identities.up.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_mfa.down.sql.hbs +3 -0
- package/templates/add/auth/migrations/create_mfa.up.sql.hbs +29 -0
- package/templates/add/auth/migrations/create_users.up.sql.hbs +4 -3
- package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler.go.hbs +142 -0
- package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler_test.go.hbs +19 -0
- package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/model.go.hbs +48 -0
- package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/repository.go.hbs +127 -0
- package/templates/add/rbac/internal/app/role/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +8 -8
- package/templates/add/rbac/internal/app/role/application/dto.go.hbs +47 -0
- package/templates/add/rbac/internal/app/role/application/errors.go.hbs +19 -0
- package/templates/add/rbac/internal/app/role/application/service.go.hbs +157 -0
- package/templates/add/rbac/internal/app/role/{service_test.go.hbs → application/service_test.go.hbs} +26 -19
- package/templates/add/rbac/internal/app/role/composition.go.hbs +48 -0
- package/templates/add/rbac/internal/app/role/domain/entity.go.hbs +23 -0
- package/templates/add/rbac/internal/app/role/domain/errors.go.hbs +26 -0
- package/templates/add/rbac/internal/app/role/ports/repository.go.hbs +25 -0
- package/templates/add/rbac/migrations/add_roles.down.sql.hbs +3 -11
- package/templates/add/rbac/migrations/add_roles.up.sql.hbs +17 -6
- package/templates/add/worker/internal/platform/queue/river_test.go.hbs +84 -0
- package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +358 -121
- package/templates/create/base/.env.example.hbs +0 -1
- package/templates/create/base/.golangci.yml.hbs +2 -2
- package/templates/create/base/AGENTS.md.hbs +279 -67
- package/templates/create/base/Makefile.hbs +2 -1
- package/templates/create/base/README.md.hbs +115 -32
- package/templates/create/base/cmd/api/wiring.go.hbs +13 -9
- package/templates/create/base/internal/composition/doc.go.hbs +7 -0
- package/templates/create/base/internal/platform/database/database.go.hbs +3 -3
- package/templates/create/base/internal/shared/apperror/apperror.go.hbs +15 -2
- package/templates/create/base/internal/shared/config/config.go.hbs +0 -8
- package/templates/create/base/internal/shared/middleware/cors_test.go.hbs +40 -0
- package/templates/create/base/internal/shared/middleware/error.go.hbs +15 -5
- package/templates/create/features/docs/architecture.md.hbs +92 -32
- package/templates/create/features/docs/patterns.md.hbs +137 -91
- package/templates/create/features/docs/techstack.md.hbs +18 -3
- package/templates/generate/module/hexagonal/adapters/inbound/http/dto.go.hbs +45 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/dto.minimal.go.hbs +28 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/handler.go.hbs +182 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/handler.minimal.go.hbs +83 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/handler_crud_test.go.hbs +18 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/handler_test.go.hbs +30 -0
- package/templates/generate/module/hexagonal/adapters/outbound/postgres/model.go.hbs +37 -0
- package/templates/generate/module/hexagonal/adapters/outbound/postgres/repository.go.hbs +95 -0
- package/templates/generate/module/{repository_test.go.hbs → hexagonal/adapters/outbound/postgres/repository_test.go.hbs} +8 -8
- package/templates/generate/module/hexagonal/application/commands.crud.go.hbs +54 -0
- package/templates/generate/module/hexagonal/application/commands.go.hbs +25 -0
- package/templates/generate/module/hexagonal/application/cqrs_test.go.hbs +66 -0
- package/templates/generate/module/hexagonal/application/dto.go.hbs +35 -0
- package/templates/generate/module/hexagonal/application/dto.minimal.go.hbs +25 -0
- package/templates/generate/module/hexagonal/application/queries.crud.go.hbs +33 -0
- package/templates/generate/module/hexagonal/application/queries.go.hbs +25 -0
- package/templates/generate/module/hexagonal/application/service.crud.go.hbs +73 -0
- package/templates/generate/module/hexagonal/application/service.go.hbs +29 -0
- package/templates/generate/module/hexagonal/application/service_test.go.hbs +62 -0
- package/templates/generate/module/hexagonal/composition.go.hbs +27 -0
- package/templates/generate/module/hexagonal/domain/entity.go.hbs +20 -0
- package/templates/generate/module/hexagonal/domain/errors.go.hbs +11 -0
- package/templates/generate/module/hexagonal/ports/repository.go.hbs +38 -0
- package/templates/generate/module/migration.up.sql.hbs +1 -1
- package/dist/utils/method-patcher.js +0 -357
- package/templates/add/auth/docs/google-callback.yaml.hbs +0 -22
- package/templates/add/auth/docs/google-login.yaml.hbs +0 -7
- package/templates/add/auth/internal/app/user/dto.go.hbs +0 -77
- package/templates/add/auth/internal/app/user/errors.go.hbs +0 -43
- package/templates/add/auth/internal/app/user/handler.go.hbs +0 -276
- package/templates/add/auth/internal/app/user/jwt.go.hbs +0 -108
- package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +0 -39
- package/templates/add/auth/internal/app/user/model/identity.go.hbs +0 -31
- package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +0 -26
- package/templates/add/auth/internal/app/user/model/user.go.hbs +0 -30
- package/templates/add/auth/internal/app/user/repository.go.hbs +0 -137
- package/templates/add/auth/internal/app/user/service.go.hbs +0 -531
- package/templates/add/auth/internal/app/user/service_test.go.hbs +0 -316
- package/templates/add/auth/internal/app/user/tokenstore.go.hbs +0 -30
- package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +0 -144
- package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +0 -147
- package/templates/add/rbac/internal/app/role/dto.go.hbs +0 -45
- package/templates/add/rbac/internal/app/role/errors.go.hbs +0 -39
- package/templates/add/rbac/internal/app/role/handler.go.hbs +0 -104
- package/templates/add/rbac/internal/app/role/model/permission.go.hbs +0 -12
- package/templates/add/rbac/internal/app/role/model/role.go.hbs +0 -22
- package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +0 -11
- package/templates/add/rbac/internal/app/role/repository.go.hbs +0 -97
- package/templates/add/rbac/internal/app/role/service.go.hbs +0 -217
- package/templates/generate/module/dto.go.hbs +0 -36
- package/templates/generate/module/errors.go.hbs +0 -33
- package/templates/generate/module/handler.go.hbs +0 -134
- package/templates/generate/module/handler_test.go.hbs +0 -174
- package/templates/generate/module/minimal/dto.go.hbs +0 -28
- package/templates/generate/module/minimal/handler.go.hbs +0 -48
- package/templates/generate/module/minimal/handler_test.go.hbs +0 -10
- package/templates/generate/module/minimal/service.go.hbs +0 -45
- package/templates/generate/module/minimal/service_test.go.hbs +0 -77
- package/templates/generate/module/model/model.go.hbs +0 -36
- package/templates/generate/module/repository.go.hbs +0 -103
- package/templates/generate/module/service.go.hbs +0 -108
- package/templates/generate/module/service_test.go.hbs +0 -161
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
post:
|
|
2
|
+
summary: Exchange a provider authorization code for a local session
|
|
3
|
+
description: The frontend callback route sends the code, the same state, and the PKCE verifier to the API. The API consumes a one-time server-side state transaction bound to the provider, S256 challenge, and OIDC nonce, uses its exact provider-registered redirect URI, validates the provider response, resolves the local identity, sets an HttpOnly refresh cookie, and returns an access token. The request has no redirect_uri, return_to, or frontend destination; credentials are never placed in a URI.
|
|
4
|
+
operationId: providerExchange
|
|
5
|
+
tags: [auth]
|
|
6
|
+
security: []
|
|
7
|
+
parameters:
|
|
8
|
+
- name: provider
|
|
9
|
+
in: path
|
|
10
|
+
required: true
|
|
11
|
+
schema: { type: string, example: google }
|
|
12
|
+
requestBody:
|
|
13
|
+
required: true
|
|
14
|
+
content:
|
|
15
|
+
application/json:
|
|
16
|
+
schema:
|
|
17
|
+
$ref: "./schemas.yaml#/OAuthExchangeInput"
|
|
18
|
+
responses:
|
|
19
|
+
"200":
|
|
20
|
+
description: local session created; refresh token is set only as an HttpOnly cookie
|
|
21
|
+
headers:
|
|
22
|
+
Set-Cookie:
|
|
23
|
+
description: HttpOnly refresh cookie
|
|
24
|
+
schema: { type: string }
|
|
25
|
+
Cache-Control:
|
|
26
|
+
description: token responses are never cacheable
|
|
27
|
+
schema: { type: string, example: no-store }
|
|
28
|
+
Pragma:
|
|
29
|
+
description: legacy cache prevention for token responses
|
|
30
|
+
schema: { type: string, example: no-cache }
|
|
31
|
+
content:
|
|
32
|
+
application/json:
|
|
33
|
+
schema:
|
|
34
|
+
oneOf:
|
|
35
|
+
- { $ref: "./schemas.yaml#/AuthResponse" }
|
|
36
|
+
- { $ref: "./schemas.yaml#/MFAChallengeResponse" }
|
|
37
|
+
"400":
|
|
38
|
+
description: controlled OAuth error (oauth_denied, oauth_state_invalid, or oauth_failed)
|
|
39
|
+
"503":
|
|
40
|
+
description: provider is unavailable or not configured
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
get:
|
|
2
|
+
summary: Start an external OAuth login flow
|
|
3
|
+
description: Redirects the browser to the configured provider. The browser client supplies state and an S256 PKCE challenge; the server creates a one-time transaction bound to that state, provider, challenge, and OIDC nonce, then uses the exact provider-registered redirect URI. It never accepts a client redirect URI.
|
|
4
|
+
operationId: providerLogin
|
|
5
|
+
tags: [auth]
|
|
6
|
+
security: []
|
|
7
|
+
parameters:
|
|
8
|
+
- name: provider
|
|
9
|
+
in: path
|
|
10
|
+
required: true
|
|
11
|
+
schema: { type: string, example: google }
|
|
12
|
+
- name: state
|
|
13
|
+
in: query
|
|
14
|
+
required: true
|
|
15
|
+
schema: { type: string, minLength: 1 }
|
|
16
|
+
- name: code_challenge
|
|
17
|
+
in: query
|
|
18
|
+
required: true
|
|
19
|
+
schema:
|
|
20
|
+
type: string
|
|
21
|
+
minLength: 43
|
|
22
|
+
maxLength: 128
|
|
23
|
+
pattern: '^[A-Za-z0-9._~-]+$'
|
|
24
|
+
- name: code_challenge_method
|
|
25
|
+
in: query
|
|
26
|
+
required: true
|
|
27
|
+
schema: { type: string, enum: [S256] }
|
|
28
|
+
responses:
|
|
29
|
+
"302": { description: redirect to the provider's authorization endpoint }
|
|
30
|
+
"400": { description: missing or invalid state/PKCE parameters; response uses a controlled OAuth error code }
|
|
31
|
+
"503": { description: provider is unavailable or not configured }
|
|
@@ -9,6 +9,13 @@ post:
|
|
|
9
9
|
responses:
|
|
10
10
|
"200":
|
|
11
11
|
description: ok, a new refresh_token cookie is set
|
|
12
|
+
headers:
|
|
13
|
+
Cache-Control:
|
|
14
|
+
description: token responses are never cacheable
|
|
15
|
+
schema: { type: string, example: no-store }
|
|
16
|
+
Pragma:
|
|
17
|
+
description: legacy cache prevention for token responses
|
|
18
|
+
schema: { type: string, example: no-cache }
|
|
12
19
|
content:
|
|
13
20
|
application/json:
|
|
14
21
|
schema: { $ref: './schemas.yaml#/AuthResponse' }
|
|
@@ -11,6 +11,13 @@ post:
|
|
|
11
11
|
responses:
|
|
12
12
|
"201":
|
|
13
13
|
description: created, refresh_token set as an httpOnly cookie
|
|
14
|
+
headers:
|
|
15
|
+
Cache-Control:
|
|
16
|
+
description: token responses are never cacheable
|
|
17
|
+
schema: { type: string, example: no-store }
|
|
18
|
+
Pragma:
|
|
19
|
+
description: legacy cache prevention for token responses
|
|
20
|
+
schema: { type: string, example: no-cache }
|
|
14
21
|
content:
|
|
15
22
|
application/json:
|
|
16
23
|
schema: { $ref: './schemas.yaml#/AuthResponse' }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
post:
|
|
2
2
|
summary: Reset a password using a reset token
|
|
3
|
-
description: The token is
|
|
3
|
+
description: The token is consumed only after the password update and refresh-session revocation complete. If the session store is temporarily unavailable, the internal error response restores the token so the caller can retry the complete operation.
|
|
4
4
|
operationId: resetPassword
|
|
5
5
|
tags: [auth]
|
|
6
6
|
security: []
|
|
@@ -32,8 +32,66 @@ VerifyEmailInput:
|
|
|
32
32
|
properties:
|
|
33
33
|
token: { type: string }
|
|
34
34
|
|
|
35
|
+
MFAChallengeInput:
|
|
36
|
+
type: object
|
|
37
|
+
required: [challenge, code]
|
|
38
|
+
additionalProperties: false
|
|
39
|
+
properties:
|
|
40
|
+
challenge: { type: string, minLength: 1 }
|
|
41
|
+
code: { type: string, minLength: 6, maxLength: 16 }
|
|
42
|
+
|
|
43
|
+
MFACodeInput:
|
|
44
|
+
type: object
|
|
45
|
+
required: [code]
|
|
46
|
+
additionalProperties: false
|
|
47
|
+
properties:
|
|
48
|
+
code: { type: string, minLength: 6, maxLength: 16 }
|
|
49
|
+
|
|
50
|
+
MFAChallengeResponse:
|
|
51
|
+
type: object
|
|
52
|
+
required: [mfa_required, challenge]
|
|
53
|
+
properties:
|
|
54
|
+
mfa_required: { type: boolean, example: true }
|
|
55
|
+
challenge: { type: string }
|
|
56
|
+
|
|
57
|
+
MFAStatus:
|
|
58
|
+
type: object
|
|
59
|
+
required: [available, enabled]
|
|
60
|
+
properties:
|
|
61
|
+
available: { type: boolean }
|
|
62
|
+
enabled: { type: boolean }
|
|
63
|
+
|
|
64
|
+
MFASetupResponse:
|
|
65
|
+
type: object
|
|
66
|
+
required: [secret, otpauth_uri]
|
|
67
|
+
properties:
|
|
68
|
+
secret: { type: string, description: show once and add to an authenticator app }
|
|
69
|
+
otpauth_uri: { type: string, format: uri }
|
|
70
|
+
|
|
71
|
+
MFAConfirmResponse:
|
|
72
|
+
type: object
|
|
73
|
+
required: [recovery_codes]
|
|
74
|
+
properties:
|
|
75
|
+
recovery_codes:
|
|
76
|
+
type: array
|
|
77
|
+
items: { type: string }
|
|
78
|
+
|
|
79
|
+
OAuthExchangeInput:
|
|
80
|
+
type: object
|
|
81
|
+
required: [code, state, code_verifier]
|
|
82
|
+
additionalProperties: false
|
|
83
|
+
properties:
|
|
84
|
+
code: { type: string, minLength: 1 }
|
|
85
|
+
state: { type: string, minLength: 1 }
|
|
86
|
+
code_verifier:
|
|
87
|
+
type: string
|
|
88
|
+
minLength: 43
|
|
89
|
+
maxLength: 128
|
|
90
|
+
pattern: '^[A-Za-z0-9._~-]+$'
|
|
91
|
+
|
|
35
92
|
# access_token in the body; refresh_token is set as an httpOnly cookie, never
|
|
36
|
-
# echoed back — see toCookieResponse in internal/app/user/dto.go.
|
|
93
|
+
# echoed back — see toCookieResponse in internal/app/user/dto.go. All token
|
|
94
|
+
# responses carry Cache-Control: no-store and Pragma: no-cache.
|
|
37
95
|
AuthResponse:
|
|
38
96
|
type: object
|
|
39
97
|
properties:
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
post:
|
|
2
|
+
summary: Confirm MFA enrollment with a current TOTP code
|
|
3
|
+
operationId: confirmMyMFA
|
|
4
|
+
tags: [users]
|
|
5
|
+
security: [{ bearerAuth: [] }]
|
|
6
|
+
requestBody:
|
|
7
|
+
required: true
|
|
8
|
+
content:
|
|
9
|
+
application/json:
|
|
10
|
+
schema: { $ref: './schemas.yaml#/MFACodeInput' }
|
|
11
|
+
responses:
|
|
12
|
+
"200":
|
|
13
|
+
description: MFA enabled; recovery codes are returned once
|
|
14
|
+
content:
|
|
15
|
+
application/json:
|
|
16
|
+
schema: { $ref: './schemas.yaml#/MFAConfirmResponse' }
|
|
17
|
+
"400": { $ref: '../common/responses.yaml#/ValidationError' }
|
|
18
|
+
"401": { $ref: '../common/responses.yaml#/UnauthorizedError' }
|
|
19
|
+
"409": { $ref: '../common/responses.yaml#/ConflictError' }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
post:
|
|
2
|
+
summary: Disable MFA for the current user
|
|
3
|
+
operationId: disableMyMFA
|
|
4
|
+
tags: [users]
|
|
5
|
+
security: [{ bearerAuth: [] }]
|
|
6
|
+
requestBody:
|
|
7
|
+
required: true
|
|
8
|
+
content:
|
|
9
|
+
application/json:
|
|
10
|
+
schema: { $ref: './schemas.yaml#/MFACodeInput' }
|
|
11
|
+
responses:
|
|
12
|
+
"204": { description: MFA disabled }
|
|
13
|
+
"400": { $ref: '../common/responses.yaml#/ValidationError' }
|
|
14
|
+
"401": { $ref: '../common/responses.yaml#/UnauthorizedError' }
|
|
15
|
+
"409": { $ref: '../common/responses.yaml#/ConflictError' }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
post:
|
|
2
|
+
summary: Start MFA enrollment for the current user
|
|
3
|
+
operationId: setupMyMFA
|
|
4
|
+
tags: [users]
|
|
5
|
+
security: [{ bearerAuth: [] }]
|
|
6
|
+
responses:
|
|
7
|
+
"200":
|
|
8
|
+
description: secret and otpauth URI; enrollment remains disabled until confirmed
|
|
9
|
+
content:
|
|
10
|
+
application/json:
|
|
11
|
+
schema: { $ref: './schemas.yaml#/MFASetupResponse' }
|
|
12
|
+
"401": { $ref: '../common/responses.yaml#/UnauthorizedError' }
|
|
13
|
+
"409": { $ref: '../common/responses.yaml#/ConflictError' }
|
|
14
|
+
"503": { description: MFA is disabled by the operator }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
get:
|
|
2
|
+
summary: Get the current user's MFA status
|
|
3
|
+
operationId: getMyMFAStatus
|
|
4
|
+
tags: [users]
|
|
5
|
+
security: [{ bearerAuth: [] }]
|
|
6
|
+
responses:
|
|
7
|
+
"200":
|
|
8
|
+
description: MFA availability and enrollment status
|
|
9
|
+
content:
|
|
10
|
+
application/json:
|
|
11
|
+
schema: { $ref: './schemas.yaml#/MFAStatus' }
|
|
12
|
+
"401": { $ref: '../common/responses.yaml#/UnauthorizedError' }
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
package httpadapter
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"net/http"
|
|
6
|
+
"net/url"
|
|
7
|
+
"strings"
|
|
8
|
+
|
|
9
|
+
"{{goModule}}/internal/shared/apperror"
|
|
10
|
+
|
|
11
|
+
"github.com/gin-gonic/gin"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// requireBrowserOrigin is the CSRF guard for the cross-site cookie mode. CORS
|
|
15
|
+
// controls whether a caller can read the response; this exact origin check
|
|
16
|
+
// independently controls whether a state-changing request may use an ambient
|
|
17
|
+
// refresh cookie.
|
|
18
|
+
func (h *Handler) requireBrowserOrigin(c *gin.Context) bool {
|
|
19
|
+
if !strings.EqualFold(strings.TrimSpace(h.cookieSameSite), "none") {
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
origin := strings.TrimSpace(c.GetHeader("Origin"))
|
|
24
|
+
if origin == "" {
|
|
25
|
+
if referer := strings.TrimSpace(c.GetHeader("Referer")); referer != "" {
|
|
26
|
+
if parsed, err := url.Parse(referer); err == nil && (parsed.Scheme == "http" || parsed.Scheme == "https") && parsed.Host != "" && parsed.User == nil {
|
|
27
|
+
origin = parsed.Scheme + "://" + parsed.Host
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if !browserOriginAllowed(origin, h.allowedOrigins) {
|
|
32
|
+
c.Error(apperror.New(http.StatusForbidden, "CSRF_ORIGIN_INVALID", "request origin is not allowed"))
|
|
33
|
+
c.Abort()
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func browserOriginAllowed(origin string, allowed []string) bool {
|
|
40
|
+
origin = strings.TrimSpace(origin)
|
|
41
|
+
if _, ok := canonicalBrowserOrigin(origin); !ok {
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
for _, candidate := range allowed {
|
|
45
|
+
candidate = strings.TrimSpace(candidate)
|
|
46
|
+
if _, candidateOK := canonicalBrowserOrigin(candidate); candidateOK && candidate == origin {
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func canonicalBrowserOrigin(value string) (string, bool) {
|
|
54
|
+
parsed, err := url.Parse(strings.TrimSpace(value))
|
|
55
|
+
if err != nil || parsed.User != nil || parsed.Opaque != "" || parsed.Host == "" || parsed.Path != "" || parsed.RawQuery != "" || parsed.Fragment != "" {
|
|
56
|
+
return "", false
|
|
57
|
+
}
|
|
58
|
+
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
|
59
|
+
return "", false
|
|
60
|
+
}
|
|
61
|
+
return parsed.Scheme + "://" + parsed.Host, true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ValidateBrowserCookiePolicy ties deployment topology to the attributes used
|
|
65
|
+
// by /auth/refresh. Same-site is the safe local default; cross-site must opt
|
|
66
|
+
// into None + Secure, and production always requires Secure.
|
|
67
|
+
func ValidateBrowserCookiePolicy(topology, sameSite string, secure, production bool) error {
|
|
68
|
+
if production && !secure {
|
|
69
|
+
return fmt.Errorf("production browser cookies require COOKIE_SECURE=true")
|
|
70
|
+
}
|
|
71
|
+
switch strings.ToLower(strings.TrimSpace(topology)) {
|
|
72
|
+
case "same-origin", "same-site":
|
|
73
|
+
if strings.EqualFold(strings.TrimSpace(sameSite), "none") && !secure {
|
|
74
|
+
return fmt.Errorf("COOKIE_SAMESITE=none requires COOKIE_SECURE=true")
|
|
75
|
+
}
|
|
76
|
+
return nil
|
|
77
|
+
case "cross-site":
|
|
78
|
+
if !strings.EqualFold(strings.TrimSpace(sameSite), "none") || !secure {
|
|
79
|
+
return fmt.Errorf("cross-site browser topology requires COOKIE_SAMESITE=none and COOKIE_SECURE=true")
|
|
80
|
+
}
|
|
81
|
+
return nil
|
|
82
|
+
default:
|
|
83
|
+
return fmt.Errorf("unsupported browser topology %q", topology)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// sameSiteFrom maps COOKIE_SAMESITE to the net/http constant and defaults to
|
|
88
|
+
// the strictest option for anything unrecognised.
|
|
89
|
+
func sameSiteFrom(mode string) http.SameSite {
|
|
90
|
+
switch strings.ToLower(mode) {
|
|
91
|
+
case "none":
|
|
92
|
+
return http.SameSiteNoneMode
|
|
93
|
+
case "lax":
|
|
94
|
+
return http.SameSiteLaxMode
|
|
95
|
+
default:
|
|
96
|
+
return http.SameSiteStrictMode
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
package httpadapter
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"time"
|
|
5
|
+
|
|
6
|
+
userapp "{{goModule}}/internal/app/user/application"
|
|
7
|
+
userdomain "{{goModule}}/internal/app/user/domain"
|
|
8
|
+
|
|
9
|
+
"github.com/google/uuid"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// Request DTOs belong to the HTTP adapter. The application receives the
|
|
13
|
+
// transport-neutral inputs from handler mapping code.
|
|
14
|
+
type registerInput struct {
|
|
15
|
+
Email string `json:"email" binding:"required,email"`
|
|
16
|
+
Password string `json:"password" binding:"required,min=8"`
|
|
17
|
+
Name string `json:"name" binding:"required"`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type loginInput struct {
|
|
21
|
+
Email string `json:"email" binding:"required,email"`
|
|
22
|
+
Password string `json:"password" binding:"required"`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type loginExchangeInput struct {
|
|
26
|
+
Code string `json:"code"`
|
|
27
|
+
State string `json:"state"`
|
|
28
|
+
CodeVerifier string `json:"code_verifier"`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type loginStartInput struct {
|
|
32
|
+
State string
|
|
33
|
+
CodeChallenge string
|
|
34
|
+
CodeChallengeMethod string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func toRegisterInput(in registerInput) userapp.RegisterInput {
|
|
38
|
+
return userapp.RegisterInput{Email: in.Email, Password: in.Password, Name: in.Name}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func toLoginInput(in loginInput) userapp.LoginInput {
|
|
42
|
+
return userapp.LoginInput{Email: in.Email, Password: in.Password}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func toLoginStartInput(in loginStartInput) userapp.LoginStartInput {
|
|
46
|
+
return userapp.LoginStartInput{
|
|
47
|
+
State: in.State, CodeChallenge: in.CodeChallenge, CodeChallengeMethod: in.CodeChallengeMethod,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
func toLoginExchangeInput(in loginExchangeInput) userapp.LoginExchangeInput {
|
|
52
|
+
return userapp.LoginExchangeInput{Code: in.Code, State: in.State, CodeVerifier: in.CodeVerifier}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type forgotPasswordInput struct {
|
|
56
|
+
Email string `json:"email" binding:"required,email"`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type resetPasswordInput struct {
|
|
60
|
+
Token string `json:"token" binding:"required"`
|
|
61
|
+
NewPassword string `json:"new_password" binding:"required,min=8"`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type verifyEmailInput struct {
|
|
65
|
+
Token string `json:"token" binding:"required"`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type mfaCodeInput struct {
|
|
69
|
+
Code string `json:"code" binding:"required"`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type mfaChallengeInput struct {
|
|
73
|
+
Challenge string `json:"challenge" binding:"required"`
|
|
74
|
+
Code string `json:"code" binding:"required"`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type authCookieResponse struct {
|
|
78
|
+
AccessToken string `json:"access_token"`
|
|
79
|
+
TokenType string `json:"token_type"`
|
|
80
|
+
ExpiresIn int `json:"expires_in"`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type mfaChallengeResponse struct {
|
|
84
|
+
MFARequired bool `json:"mfa_required"`
|
|
85
|
+
Challenge string `json:"challenge"`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type mfaStatusResponse struct {
|
|
89
|
+
Available bool `json:"available"`
|
|
90
|
+
Enabled bool `json:"enabled"`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type mfaSetupResponse struct {
|
|
94
|
+
Secret string `json:"secret"`
|
|
95
|
+
OTPAuthURI string `json:"otpauth_uri"`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
type mfaConfirmResponse struct {
|
|
99
|
+
RecoveryCodes []string `json:"recovery_codes"`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func toCookieResponse(auth *userapp.AuthResponse) authCookieResponse {
|
|
103
|
+
return authCookieResponse{AccessToken: auth.AccessToken, TokenType: auth.TokenType, ExpiresIn: auth.ExpiresIn}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
type meResponse struct {
|
|
107
|
+
ID uuid.UUID `json:"id"`
|
|
108
|
+
Email string `json:"email"`
|
|
109
|
+
Name string `json:"name"`
|
|
110
|
+
AvatarURL string `json:"avatar_url"`
|
|
111
|
+
EmailVerified bool `json:"email_verified"`
|
|
112
|
+
Role string `json:"role"`
|
|
113
|
+
// go-scaffold:user-me-fields
|
|
114
|
+
CreatedAt time.Time `json:"created_at"`
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
func toMeResponse(user *userdomain.User) meResponse {
|
|
118
|
+
return meResponse{
|
|
119
|
+
ID: user.ID,
|
|
120
|
+
Email: user.Email,
|
|
121
|
+
Name: user.Name,
|
|
122
|
+
AvatarURL: user.AvatarURL,
|
|
123
|
+
EmailVerified: user.EmailVerified,
|
|
124
|
+
CreatedAt: user.CreatedAt,
|
|
125
|
+
Role: user.Role,
|
|
126
|
+
// go-scaffold:user-me-values
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// response is the HTTP representation used by generated methods. It is kept
|
|
131
|
+
// separate from application.Response so JSON naming remains an adapter detail.
|
|
132
|
+
//
|
|
133
|
+
//nolint:unused
|
|
134
|
+
type response struct {
|
|
135
|
+
ID uuid.UUID `json:"id"`
|
|
136
|
+
Email string `json:"email"`
|
|
137
|
+
Name string `json:"name"`
|
|
138
|
+
AvatarURL string `json:"avatar_url"`
|
|
139
|
+
EmailVerified bool `json:"email_verified"`
|
|
140
|
+
Role string `json:"role"`
|
|
141
|
+
CreatedAt time.Time `json:"created_at"`
|
|
142
|
+
UpdatedAt time.Time `json:"updated_at"`
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
//nolint:unused
|
|
146
|
+
func toResponse(out userapp.Response) response {
|
|
147
|
+
return response{
|
|
148
|
+
ID: out.ID, Email: out.Email, Name: out.Name, AvatarURL: out.AvatarURL,
|
|
149
|
+
EmailVerified: out.EmailVerified, Role: out.Role, CreatedAt: out.CreatedAt,
|
|
150
|
+
UpdatedAt: out.UpdatedAt,
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
type setRoleInput struct {
|
|
155
|
+
Role string `json:"role" binding:"required"`
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// go-scaffold:dto
|
|
159
|
+
// go-scaffold:user-dto
|