@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,228 @@
|
|
|
1
|
+
package httpadapter
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"errors"
|
|
5
|
+
"net/http"
|
|
6
|
+
"time"
|
|
7
|
+
|
|
8
|
+
"{{goModule}}/internal/app/user/application"
|
|
9
|
+
"{{goModule}}/internal/app/user/domain"
|
|
10
|
+
"{{goModule}}/internal/shared/apperror"
|
|
11
|
+
"{{goModule}}/internal/shared/httpx"
|
|
12
|
+
"{{goModule}}/internal/shared/middleware"
|
|
13
|
+
|
|
14
|
+
"{{goModule}}/internal/shared/pagination"
|
|
15
|
+
"github.com/gin-gonic/gin"
|
|
16
|
+
// go-scaffold:user-handler-imports
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
const (
|
|
20
|
+
refreshCookieName = "refresh_token"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
const PermUserManageRole = "user:manage-role"
|
|
24
|
+
const PermUserRead = "user:read"
|
|
25
|
+
|
|
26
|
+
// go-scaffold:user-handler-consts
|
|
27
|
+
|
|
28
|
+
// authorizer is the optional capability RBAC supplies to protect admin
|
|
29
|
+
// routes. Auth itself does not import the RBAC middleware implementation.
|
|
30
|
+
type authorizer interface {
|
|
31
|
+
Require(string) gin.HandlerFunc
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type Handler struct {
|
|
35
|
+
svc application.ServicePort
|
|
36
|
+
jwtSecret string
|
|
37
|
+
refreshTTL time.Duration
|
|
38
|
+
cookieSecure bool
|
|
39
|
+
cookieSameSite string
|
|
40
|
+
allowedOrigins []string
|
|
41
|
+
// limiter, not a *redis.Client: which backing store counts the requests is
|
|
42
|
+
// decided by `add auth --store`, and this file must not care.
|
|
43
|
+
limiter middleware.Limiter
|
|
44
|
+
authz authorizer
|
|
45
|
+
// go-scaffold:user-handler-fields
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func NewHandler(
|
|
49
|
+
svc application.ServicePort,
|
|
50
|
+
jwtSecret string,
|
|
51
|
+
refreshTTL time.Duration,
|
|
52
|
+
cookieSecure bool,
|
|
53
|
+
cookieSameSite string,
|
|
54
|
+
limiter middleware.Limiter,
|
|
55
|
+
authz ...authorizer,
|
|
56
|
+
// go-scaffold:user-handler-params
|
|
57
|
+
) *Handler {
|
|
58
|
+
return newHandler(svc, jwtSecret, refreshTTL, cookieSecure, cookieSameSite, nil, limiter, authz...)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// NewHandlerWithOrigins is the composition entry point when the application
|
|
62
|
+
// uses cookies across browser origins.
|
|
63
|
+
func NewHandlerWithOrigins(
|
|
64
|
+
svc application.ServicePort,
|
|
65
|
+
jwtSecret string,
|
|
66
|
+
refreshTTL time.Duration,
|
|
67
|
+
cookieSecure bool,
|
|
68
|
+
cookieSameSite string,
|
|
69
|
+
allowedOrigins []string,
|
|
70
|
+
limiter middleware.Limiter,
|
|
71
|
+
authz ...authorizer,
|
|
72
|
+
// go-scaffold:user-handler-with-origins-params
|
|
73
|
+
) *Handler {
|
|
74
|
+
return newHandler(svc, jwtSecret, refreshTTL, cookieSecure, cookieSameSite, allowedOrigins, limiter, authz...)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
func newHandler(
|
|
78
|
+
svc application.ServicePort,
|
|
79
|
+
jwtSecret string,
|
|
80
|
+
refreshTTL time.Duration,
|
|
81
|
+
cookieSecure bool,
|
|
82
|
+
cookieSameSite string,
|
|
83
|
+
allowedOrigins []string,
|
|
84
|
+
limiter middleware.Limiter,
|
|
85
|
+
authz ...authorizer,
|
|
86
|
+
) *Handler {
|
|
87
|
+
var authzMiddleware authorizer
|
|
88
|
+
if len(authz) > 0 {
|
|
89
|
+
authzMiddleware = authz[0]
|
|
90
|
+
}
|
|
91
|
+
return &Handler{
|
|
92
|
+
svc: svc,
|
|
93
|
+
jwtSecret: jwtSecret,
|
|
94
|
+
refreshTTL: refreshTTL,
|
|
95
|
+
cookieSecure: cookieSecure,
|
|
96
|
+
cookieSameSite: cookieSameSite,
|
|
97
|
+
allowedOrigins: append([]string(nil), allowedOrigins...),
|
|
98
|
+
limiter: limiter,
|
|
99
|
+
authz: authzMiddleware,
|
|
100
|
+
// go-scaffold:user-handler-init
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Register owns the public/protected route split for the user feature. The
|
|
105
|
+
// endpoint implementations live in focused handler files; this file remains
|
|
106
|
+
// the single route/composition surface that generator patches can target.
|
|
107
|
+
func (h *Handler) Register(rg gin.IRouter) {
|
|
108
|
+
loginLimit := middleware.RateLimit(h.limiter, "login", 10, time.Minute)
|
|
109
|
+
registerLimit := middleware.RateLimit(h.limiter, "register", 5, time.Minute)
|
|
110
|
+
forgotPasswordLimit := middleware.RateLimit(h.limiter, "forgot-password", 5, time.Minute)
|
|
111
|
+
resetPasswordLimit := middleware.RateLimit(h.limiter, "reset-password", 10, time.Minute)
|
|
112
|
+
verifyEmailLimit := middleware.RateLimit(h.limiter, "verify-email", 10, time.Minute)
|
|
113
|
+
resendVerificationLimit := middleware.RateLimit(h.limiter, "resend-verification", 5, time.Minute)
|
|
114
|
+
mfaVerifyLimit := middleware.RateLimit(h.limiter, "mfa-verify", 5, time.Minute)
|
|
115
|
+
mfaSetupLimit := middleware.RateLimit(h.limiter, "mfa-setup", 5, time.Minute)
|
|
116
|
+
mfaConfirmLimit := middleware.RateLimit(h.limiter, "mfa-confirm", 5, time.Minute)
|
|
117
|
+
mfaDisableLimit := middleware.RateLimit(h.limiter, "mfa-disable", 5, time.Minute)
|
|
118
|
+
|
|
119
|
+
authGroup := rg.Group("/auth")
|
|
120
|
+
authGroup.POST("/register", registerLimit, h.register)
|
|
121
|
+
authGroup.POST("/login", loginLimit, h.login)
|
|
122
|
+
authGroup.POST("/refresh", h.refresh)
|
|
123
|
+
authGroup.POST("/logout", h.logout)
|
|
124
|
+
authGroup.POST("/forgot-password", forgotPasswordLimit, h.forgotPassword)
|
|
125
|
+
authGroup.POST("/reset-password", resetPasswordLimit, h.resetPassword)
|
|
126
|
+
authGroup.POST("/verify-email", verifyEmailLimit, h.verifyEmail)
|
|
127
|
+
authGroup.POST("/mfa/verify", mfaVerifyLimit, h.verifyMFA)
|
|
128
|
+
authGroup.GET("/:provider/login", h.providerLogin)
|
|
129
|
+
authGroup.POST("/:provider/exchange", h.providerExchange)
|
|
130
|
+
|
|
131
|
+
usersGroup := rg.Group("/users", middleware.RequireAuth(h.jwtSecret))
|
|
132
|
+
usersGroup.GET("/me", h.me)
|
|
133
|
+
usersGroup.POST("/me/resend-verification", resendVerificationLimit, h.resendVerification)
|
|
134
|
+
usersGroup.POST("/me/logout-all", h.logoutAll)
|
|
135
|
+
usersGroup.GET("/me/mfa", h.mfaStatus)
|
|
136
|
+
usersGroup.POST("/me/mfa/setup", mfaSetupLimit, h.setupMFA)
|
|
137
|
+
usersGroup.POST("/me/mfa/confirm", mfaConfirmLimit, h.confirmMFA)
|
|
138
|
+
usersGroup.POST("/me/mfa/disable", mfaDisableLimit, h.disableMFA)
|
|
139
|
+
// Admin user routes are enabled only when the optional RBAC authorizer is
|
|
140
|
+
// supplied by the composition root. Auth-only projects keep the endpoint
|
|
141
|
+
// surface limited to authentication and the current-user operations.
|
|
142
|
+
if h.authz != nil {
|
|
143
|
+
usersGroup.GET("", h.authz.Require(PermUserRead), h.adminListUsers)
|
|
144
|
+
usersGroup.GET("/:id", h.authz.Require(PermUserRead), h.adminGetUser)
|
|
145
|
+
usersGroup.PATCH("/:id/set-role", h.authz.Require(PermUserManageRole), h.setRole)
|
|
146
|
+
}
|
|
147
|
+
// go-scaffold:handler-routes
|
|
148
|
+
// go-scaffold:user-routes
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
func (h *Handler) adminListUsers(c *gin.Context) {
|
|
152
|
+
p := pagination.Parse(c)
|
|
153
|
+
items, err := h.svc.List(c.Request.Context(), p.Limit, p.Offset)
|
|
154
|
+
if err != nil {
|
|
155
|
+
c.Error(toHTTPError(err))
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
out := make([]meResponse, len(items))
|
|
159
|
+
for i := range items {
|
|
160
|
+
out[i] = toMeResponse(&items[i])
|
|
161
|
+
}
|
|
162
|
+
c.JSON(http.StatusOK, p.Response(out))
|
|
163
|
+
}
|
|
164
|
+
func (h *Handler) adminGetUser(c *gin.Context) {
|
|
165
|
+
id, ok := httpx.ParseID(c)
|
|
166
|
+
if !ok {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
u, err := h.svc.Get(c.Request.Context(), id)
|
|
170
|
+
if err != nil {
|
|
171
|
+
c.Error(toHTTPError(err))
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
c.JSON(http.StatusOK, toMeResponse(u))
|
|
175
|
+
}
|
|
176
|
+
func (h *Handler) setRole(c *gin.Context) {
|
|
177
|
+
id, ok := httpx.ParseID(c)
|
|
178
|
+
if !ok {
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
var in setRoleInput
|
|
182
|
+
if err := c.ShouldBindJSON(&in); err != nil {
|
|
183
|
+
c.Error(httpx.BindErr(err))
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
u, err := h.svc.SetRole(c.Request.Context(), id, in.Role)
|
|
187
|
+
if err != nil {
|
|
188
|
+
c.Error(toHTTPError(err))
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
c.JSON(http.StatusOK, toMeResponse(u))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
func toHTTPError(err error) error {
|
|
195
|
+
if err == nil {
|
|
196
|
+
return nil
|
|
197
|
+
}
|
|
198
|
+
var appErr *apperror.AppError
|
|
199
|
+
if errors.As(err, &appErr) {
|
|
200
|
+
return err
|
|
201
|
+
}
|
|
202
|
+
var ruleErr *domain.RuleError
|
|
203
|
+
if errors.As(err, &ruleErr) {
|
|
204
|
+
status := http.StatusInternalServerError
|
|
205
|
+
switch ruleErr.Code {
|
|
206
|
+
case "USER_NOT_FOUND":
|
|
207
|
+
status = http.StatusNotFound
|
|
208
|
+
case "USER_EMAIL_TAKEN", "AUTH_ALREADY_VERIFIED", "AUTH_MFA_ALREADY_ENABLED", "AUTH_MFA_NOT_ENROLLED", "AUTH_MFA_SETUP_REQUIRED":
|
|
209
|
+
status = http.StatusConflict
|
|
210
|
+
case "AUTH_INVALID_CREDENTIALS", "AUTH_INVALID_TOKEN", "AUTH_MFA_INVALID":
|
|
211
|
+
status = http.StatusUnauthorized
|
|
212
|
+
case "AUTH_MFA_UNAVAILABLE":
|
|
213
|
+
status = http.StatusServiceUnavailable
|
|
214
|
+
case "AUTH_TOO_MANY_ATTEMPTS":
|
|
215
|
+
status = http.StatusTooManyRequests
|
|
216
|
+
case "USER_UNKNOWN_ROLE":
|
|
217
|
+
status = http.StatusUnprocessableEntity
|
|
218
|
+
}
|
|
219
|
+
return apperror.New(status, ruleErr.Code, ruleErr.Message)
|
|
220
|
+
}
|
|
221
|
+
if errors.Is(err, domain.ErrNotFound) {
|
|
222
|
+
return apperror.NewNotFound("user not found")
|
|
223
|
+
}
|
|
224
|
+
return apperror.NewInternal(err)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// go-scaffold:handler-funcs
|
|
228
|
+
// go-scaffold:user-handler-funcs
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
package httpadapter
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"net/http"
|
|
5
|
+
|
|
6
|
+
"{{goModule}}/internal/app/user/domain"
|
|
7
|
+
"{{goModule}}/internal/shared/httpx"
|
|
8
|
+
|
|
9
|
+
"github.com/gin-gonic/gin"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
func (h *Handler) register(c *gin.Context) {
|
|
13
|
+
setNoStoreHeaders(c)
|
|
14
|
+
if !h.requireBrowserOrigin(c) {
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
var in registerInput
|
|
18
|
+
if err := c.ShouldBindJSON(&in); err != nil {
|
|
19
|
+
c.Error(httpx.BindErr(err))
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
auth, err := h.svc.Register(c.Request.Context(), toRegisterInput(in))
|
|
23
|
+
if err != nil {
|
|
24
|
+
c.Error(toHTTPError(err))
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
h.writeAuthResult(c, http.StatusCreated, auth)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func (h *Handler) login(c *gin.Context) {
|
|
31
|
+
setNoStoreHeaders(c)
|
|
32
|
+
if !h.requireBrowserOrigin(c) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
var in loginInput
|
|
36
|
+
if err := c.ShouldBindJSON(&in); err != nil {
|
|
37
|
+
c.Error(httpx.BindErr(err))
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
auth, err := h.svc.Login(c.Request.Context(), toLoginInput(in))
|
|
41
|
+
if err != nil {
|
|
42
|
+
c.Error(toHTTPError(err))
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
h.writeAuthResult(c, http.StatusOK, auth)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func (h *Handler) refresh(c *gin.Context) {
|
|
49
|
+
setNoStoreHeaders(c)
|
|
50
|
+
if !h.requireBrowserOrigin(c) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
raw, err := c.Cookie(refreshCookieName)
|
|
54
|
+
if err != nil || raw == "" {
|
|
55
|
+
c.Error(toHTTPError(domain.Rule("AUTH_INVALID_TOKEN", "invalid or expired token", domain.ErrInvalidToken)))
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
auth, err := h.svc.Refresh(c.Request.Context(), raw)
|
|
59
|
+
if err != nil {
|
|
60
|
+
h.clearRefreshCookie(c)
|
|
61
|
+
c.Error(toHTTPError(err))
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
h.setRefreshCookie(c, auth.RefreshToken)
|
|
65
|
+
c.JSON(http.StatusOK, toCookieResponse(auth))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func (h *Handler) logout(c *gin.Context) {
|
|
69
|
+
if !h.requireBrowserOrigin(c) {
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
raw, _ := c.Cookie(refreshCookieName)
|
|
73
|
+
_ = h.svc.Logout(c.Request.Context(), raw)
|
|
74
|
+
h.clearRefreshCookie(c)
|
|
75
|
+
c.Status(http.StatusNoContent)
|
|
76
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
package httpadapter
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"net/http"
|
|
5
|
+
|
|
6
|
+
"{{goModule}}/internal/shared/httpx"
|
|
7
|
+
"{{goModule}}/internal/shared/middleware"
|
|
8
|
+
|
|
9
|
+
"github.com/gin-gonic/gin"
|
|
10
|
+
"github.com/google/uuid"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
func (h *Handler) verifyMFA(c *gin.Context) {
|
|
14
|
+
setNoStoreHeaders(c)
|
|
15
|
+
if !h.requireBrowserOrigin(c) {
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
var in mfaChallengeInput
|
|
19
|
+
if err := c.ShouldBindJSON(&in); err != nil {
|
|
20
|
+
c.Error(httpx.BindErr(err))
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
auth, err := h.svc.VerifyMFA(c.Request.Context(), in.Challenge, in.Code)
|
|
24
|
+
if err != nil {
|
|
25
|
+
c.Error(toHTTPError(err))
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
h.setRefreshCookie(c, auth.RefreshToken)
|
|
29
|
+
c.JSON(http.StatusOK, toCookieResponse(auth))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func (h *Handler) mfaStatus(c *gin.Context) {
|
|
33
|
+
setNoStoreHeaders(c)
|
|
34
|
+
status, err := h.svc.MFAStatus(c.Request.Context(), currentUserID(c))
|
|
35
|
+
if err != nil {
|
|
36
|
+
c.Error(toHTTPError(err))
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
c.JSON(http.StatusOK, mfaStatusResponse(status))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func (h *Handler) setupMFA(c *gin.Context) {
|
|
43
|
+
setNoStoreHeaders(c)
|
|
44
|
+
setup, err := h.svc.SetupMFA(c.Request.Context(), currentUserID(c))
|
|
45
|
+
if err != nil {
|
|
46
|
+
c.Error(toHTTPError(err))
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
c.JSON(http.StatusOK, mfaSetupResponse{Secret: setup.Secret, OTPAuthURI: setup.OTPAuthURI})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func (h *Handler) confirmMFA(c *gin.Context) {
|
|
53
|
+
setNoStoreHeaders(c)
|
|
54
|
+
var in mfaCodeInput
|
|
55
|
+
if err := c.ShouldBindJSON(&in); err != nil {
|
|
56
|
+
c.Error(httpx.BindErr(err))
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
codes, err := h.svc.ConfirmMFA(c.Request.Context(), currentUserID(c), in.Code)
|
|
60
|
+
if err != nil {
|
|
61
|
+
c.Error(toHTTPError(err))
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
c.JSON(http.StatusOK, mfaConfirmResponse{RecoveryCodes: codes})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func (h *Handler) disableMFA(c *gin.Context) {
|
|
68
|
+
setNoStoreHeaders(c)
|
|
69
|
+
var in mfaCodeInput
|
|
70
|
+
if err := c.ShouldBindJSON(&in); err != nil {
|
|
71
|
+
c.Error(httpx.BindErr(err))
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
if err := h.svc.DisableMFA(c.Request.Context(), currentUserID(c), in.Code); err != nil {
|
|
75
|
+
c.Error(toHTTPError(err))
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
c.Status(http.StatusNoContent)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func currentUserID(c *gin.Context) uuid.UUID {
|
|
82
|
+
return c.MustGet(middleware.UserIDKey).(uuid.UUID)
|
|
83
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
package httpadapter
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"errors"
|
|
5
|
+
"log/slog"
|
|
6
|
+
"net/http"
|
|
7
|
+
|
|
8
|
+
userapp "{{goModule}}/internal/app/user/application"
|
|
9
|
+
"{{goModule}}/internal/shared/apperror"
|
|
10
|
+
|
|
11
|
+
"github.com/gin-gonic/gin"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
func (h *Handler) providerLogin(c *gin.Context) {
|
|
15
|
+
start, err := h.svc.BeginLogin(c.Request.Context(), c.Param("provider"), toLoginStartInput(loginStartInput{
|
|
16
|
+
State: c.Query("state"),
|
|
17
|
+
CodeChallenge: c.Query("code_challenge"),
|
|
18
|
+
CodeChallengeMethod: c.Query("code_challenge_method"),
|
|
19
|
+
}))
|
|
20
|
+
if err != nil {
|
|
21
|
+
c.Error(h.oauthAppError(c.Param("provider"), err))
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
c.Redirect(http.StatusFound, start.URL)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func (h *Handler) providerExchange(c *gin.Context) {
|
|
28
|
+
setNoStoreHeaders(c)
|
|
29
|
+
if !h.requireBrowserOrigin(c) {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
var in loginExchangeInput
|
|
33
|
+
if err := c.ShouldBindJSON(&in); err != nil {
|
|
34
|
+
c.Error(h.oauthAppError(c.Param("provider"), userapp.NewOAuthError(userapp.OAuthFailed, err)))
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
auth, err := h.svc.ExchangeLogin(c.Request.Context(), c.Param("provider"), toLoginExchangeInput(in))
|
|
38
|
+
if err != nil {
|
|
39
|
+
c.Error(h.oauthAppError(c.Param("provider"), err))
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
h.writeAuthResult(c, http.StatusOK, auth)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func (h *Handler) oauthAppError(provider string, err error) *apperror.AppError {
|
|
46
|
+
code := userapp.OAuthFailed
|
|
47
|
+
status := http.StatusBadRequest
|
|
48
|
+
message := "external login failed"
|
|
49
|
+
var oauthErr *userapp.OAuthError
|
|
50
|
+
if errors.As(err, &oauthErr) {
|
|
51
|
+
switch oauthErr.Code {
|
|
52
|
+
case userapp.OAuthDenied:
|
|
53
|
+
code = userapp.OAuthDenied
|
|
54
|
+
message = "external login was denied"
|
|
55
|
+
case userapp.OAuthStateInvalid:
|
|
56
|
+
code = userapp.OAuthStateInvalid
|
|
57
|
+
message = "oauth state or PKCE verifier is invalid"
|
|
58
|
+
case userapp.OAuthProviderUnavailable:
|
|
59
|
+
code = userapp.OAuthProviderUnavailable
|
|
60
|
+
status = http.StatusServiceUnavailable
|
|
61
|
+
message = "external login provider is unavailable"
|
|
62
|
+
case userapp.OAuthFailed:
|
|
63
|
+
code = userapp.OAuthFailed
|
|
64
|
+
default:
|
|
65
|
+
code = userapp.OAuthFailed
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
slog.Warn("oauth request failed", "provider", provider, "code", code, "error", err)
|
|
69
|
+
return apperror.New(status, string(code), message)
|
|
70
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
package httpadapter
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"net/http"
|
|
5
|
+
|
|
6
|
+
"{{goModule}}/internal/shared/httpx"
|
|
7
|
+
|
|
8
|
+
"github.com/gin-gonic/gin"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
func (h *Handler) forgotPassword(c *gin.Context) {
|
|
12
|
+
var in forgotPasswordInput
|
|
13
|
+
if err := c.ShouldBindJSON(&in); err != nil {
|
|
14
|
+
c.Error(httpx.BindErr(err))
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
if err := h.svc.ForgotPassword(c.Request.Context(), in.Email); err != nil {
|
|
18
|
+
c.Error(toHTTPError(err))
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
// Always the same response, whether or not the email exists.
|
|
22
|
+
c.JSON(http.StatusOK, gin.H{"message": "if that email exists, a reset link has been sent"})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func (h *Handler) resetPassword(c *gin.Context) {
|
|
26
|
+
var in resetPasswordInput
|
|
27
|
+
if err := c.ShouldBindJSON(&in); err != nil {
|
|
28
|
+
c.Error(httpx.BindErr(err))
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
if err := h.svc.ResetPassword(c.Request.Context(), in.Token, in.NewPassword); err != nil {
|
|
32
|
+
c.Error(toHTTPError(err))
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
c.Status(http.StatusNoContent)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func (h *Handler) verifyEmail(c *gin.Context) {
|
|
39
|
+
var in verifyEmailInput
|
|
40
|
+
if err := c.ShouldBindJSON(&in); err != nil {
|
|
41
|
+
c.Error(httpx.BindErr(err))
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
if err := h.svc.VerifyEmail(c.Request.Context(), in.Token); err != nil {
|
|
45
|
+
c.Error(toHTTPError(err))
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
c.Status(http.StatusNoContent)
|
|
49
|
+
}
|