@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.
Files changed (192) hide show
  1. package/README.md +598 -306
  2. package/dist/commands/auth.js +65 -23
  3. package/dist/commands/check.js +281 -0
  4. package/dist/commands/config.js +50 -0
  5. package/dist/commands/create.js +33 -2
  6. package/dist/commands/generate.js +29 -3
  7. package/dist/commands/method.js +74 -63
  8. package/dist/commands/migration.js +2 -2
  9. package/dist/commands/observability.js +4 -53
  10. package/dist/commands/rbac.js +21 -10
  11. package/dist/commands/undo.js +11 -3
  12. package/dist/commands/worker.js +15 -5
  13. package/dist/index.js +198 -59
  14. package/dist/prompts/auth-wizard.js +40 -6
  15. package/dist/prompts/create-wizard.js +42 -1
  16. package/dist/prompts/generate-wizard.js +89 -9
  17. package/dist/templates/auth-manifest.js +50 -19
  18. package/dist/templates/create-manifest.js +8 -0
  19. package/dist/templates/module-manifest.js +84 -26
  20. package/dist/templates/rbac-manifest.js +16 -11
  21. package/dist/templates/worker-manifest.js +4 -1
  22. package/dist/types.js +8 -0
  23. package/dist/utils/auth-patcher.js +124 -33
  24. package/dist/utils/config.js +167 -4
  25. package/dist/utils/docs-patcher.js +68 -0
  26. package/dist/utils/hexagonal-method-patcher.js +334 -0
  27. package/dist/utils/main-patcher.js +32 -30
  28. package/dist/utils/marker-patch.js +7 -1
  29. package/dist/utils/module-location.js +17 -11
  30. package/dist/utils/module-profile.js +32 -0
  31. package/dist/utils/platform-patcher.js +56 -7
  32. package/dist/utils/rbac-patcher.js +89 -210
  33. package/package.json +7 -2
  34. package/templates/add/auth/cmd/seed/main.go.hbs +15 -3
  35. package/templates/add/auth/docs/login.yaml.hbs +11 -1
  36. package/templates/add/auth/docs/mfa-verify.yaml.hbs +19 -0
  37. package/templates/add/auth/docs/provider-exchange.yaml.hbs +40 -0
  38. package/templates/add/auth/docs/provider-login.yaml.hbs +31 -0
  39. package/templates/add/auth/docs/refresh.yaml.hbs +7 -0
  40. package/templates/add/auth/docs/register.yaml.hbs +7 -0
  41. package/templates/add/auth/docs/reset-password.yaml.hbs +1 -1
  42. package/templates/add/auth/docs/schemas.yaml.hbs +59 -1
  43. package/templates/add/auth/docs/users-me-mfa-confirm.yaml.hbs +19 -0
  44. package/templates/add/auth/docs/users-me-mfa-disable.yaml.hbs +15 -0
  45. package/templates/add/auth/docs/users-me-mfa-setup.yaml.hbs +14 -0
  46. package/templates/add/auth/docs/users-me-mfa.yaml.hbs +12 -0
  47. package/templates/add/auth/internal/app/user/adapters/inbound/http/browser_policy.go.hbs +98 -0
  48. package/templates/add/auth/internal/app/user/adapters/inbound/http/dto.go.hbs +159 -0
  49. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler.go.hbs +228 -0
  50. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_local.go.hbs +76 -0
  51. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_mfa.go.hbs +83 -0
  52. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_oauth.go.hbs +70 -0
  53. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_recovery.go.hbs +49 -0
  54. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_test.go.hbs +311 -0
  55. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_user.go.hbs +41 -0
  56. package/templates/add/auth/internal/app/user/adapters/inbound/http/session_cookie.go.hbs +35 -0
  57. package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt.go.hbs +35 -0
  58. package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt_test.go.hbs +20 -0
  59. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/mfa_store.go.hbs +129 -0
  60. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/mfa_store_test.go.hbs +174 -0
  61. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/model.go.hbs +84 -0
  62. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/repository.go.hbs +211 -0
  63. package/templates/add/auth/internal/app/user/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +18 -19
  64. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_pg.go.hbs +213 -0
  65. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_pg_test.go.hbs +103 -0
  66. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_recovery.go.hbs +84 -0
  67. package/templates/add/auth/internal/app/user/adapters/outbound/redis/tokenstore.go.hbs +228 -0
  68. package/templates/add/auth/internal/app/user/adapters/outbound/redis/tokenstore_test.go.hbs +196 -0
  69. package/templates/add/auth/internal/app/user/application/contracts.go.hbs +52 -0
  70. package/templates/add/auth/internal/app/user/application/dto.go.hbs +75 -0
  71. package/templates/add/auth/internal/app/user/application/errors.go.hbs +62 -0
  72. package/templates/add/auth/internal/app/user/application/external_login.go.hbs +198 -0
  73. package/templates/add/auth/internal/app/user/application/jwt.go.hbs +58 -0
  74. package/templates/add/auth/internal/app/user/application/local_auth.go.hbs +96 -0
  75. package/templates/add/auth/internal/app/user/application/mfa_service.go.hbs +449 -0
  76. package/templates/add/auth/internal/app/user/application/mfa_service_test.go.hbs +200 -0
  77. package/templates/add/auth/internal/app/user/application/oauth.go.hbs +132 -0
  78. package/templates/add/auth/internal/app/user/application/provider_test.go.hbs +285 -0
  79. package/templates/add/auth/internal/app/user/application/recovery.go.hbs +82 -0
  80. package/templates/add/auth/internal/app/user/application/recovery_service.go.hbs +112 -0
  81. package/templates/add/auth/internal/app/user/application/service.go.hbs +145 -0
  82. package/templates/add/auth/internal/app/user/application/service_test.go.hbs +891 -0
  83. package/templates/add/auth/internal/app/user/application/sessions.go.hbs +99 -0
  84. package/templates/add/auth/internal/app/user/application/tokenstore_ports.go.hbs +14 -0
  85. package/templates/add/auth/internal/app/user/application/user_query.go.hbs +65 -0
  86. package/templates/add/auth/internal/app/user/composition.go.hbs +168 -0
  87. package/templates/add/auth/internal/app/user/domain/entity.go.hbs +41 -0
  88. package/templates/add/auth/internal/app/user/domain/errors.go.hbs +32 -0
  89. package/templates/add/auth/internal/app/user/ports/password.go.hbs +9 -0
  90. package/templates/add/auth/internal/app/user/ports/repository.go.hbs +90 -0
  91. package/templates/add/auth/internal/platform/authprovider/google/google.go.hbs +389 -0
  92. package/templates/add/auth/internal/platform/authprovider/google/google_test.go.hbs +312 -0
  93. package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +10 -5
  94. package/templates/add/auth/migrations/create_identities.up.sql.hbs +1 -1
  95. package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +1 -1
  96. package/templates/add/auth/migrations/create_mfa.down.sql.hbs +3 -0
  97. package/templates/add/auth/migrations/create_mfa.up.sql.hbs +29 -0
  98. package/templates/add/auth/migrations/create_users.up.sql.hbs +4 -3
  99. package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler.go.hbs +142 -0
  100. package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler_test.go.hbs +19 -0
  101. package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/model.go.hbs +48 -0
  102. package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/repository.go.hbs +127 -0
  103. package/templates/add/rbac/internal/app/role/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +8 -8
  104. package/templates/add/rbac/internal/app/role/application/dto.go.hbs +47 -0
  105. package/templates/add/rbac/internal/app/role/application/errors.go.hbs +19 -0
  106. package/templates/add/rbac/internal/app/role/application/service.go.hbs +157 -0
  107. package/templates/add/rbac/internal/app/role/{service_test.go.hbs → application/service_test.go.hbs} +26 -19
  108. package/templates/add/rbac/internal/app/role/composition.go.hbs +48 -0
  109. package/templates/add/rbac/internal/app/role/domain/entity.go.hbs +23 -0
  110. package/templates/add/rbac/internal/app/role/domain/errors.go.hbs +26 -0
  111. package/templates/add/rbac/internal/app/role/ports/repository.go.hbs +25 -0
  112. package/templates/add/rbac/migrations/add_roles.down.sql.hbs +3 -11
  113. package/templates/add/rbac/migrations/add_roles.up.sql.hbs +17 -6
  114. package/templates/add/worker/internal/platform/queue/river_test.go.hbs +84 -0
  115. package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +358 -121
  116. package/templates/create/base/.env.example.hbs +0 -1
  117. package/templates/create/base/.golangci.yml.hbs +2 -2
  118. package/templates/create/base/AGENTS.md.hbs +279 -67
  119. package/templates/create/base/Makefile.hbs +2 -1
  120. package/templates/create/base/README.md.hbs +115 -32
  121. package/templates/create/base/cmd/api/wiring.go.hbs +13 -9
  122. package/templates/create/base/internal/composition/doc.go.hbs +7 -0
  123. package/templates/create/base/internal/platform/database/database.go.hbs +3 -3
  124. package/templates/create/base/internal/shared/apperror/apperror.go.hbs +15 -2
  125. package/templates/create/base/internal/shared/config/config.go.hbs +0 -8
  126. package/templates/create/base/internal/shared/middleware/cors_test.go.hbs +40 -0
  127. package/templates/create/base/internal/shared/middleware/error.go.hbs +15 -5
  128. package/templates/create/features/docs/architecture.md.hbs +92 -32
  129. package/templates/create/features/docs/patterns.md.hbs +137 -91
  130. package/templates/create/features/docs/techstack.md.hbs +18 -3
  131. package/templates/generate/module/hexagonal/adapters/inbound/http/dto.go.hbs +45 -0
  132. package/templates/generate/module/hexagonal/adapters/inbound/http/dto.minimal.go.hbs +28 -0
  133. package/templates/generate/module/hexagonal/adapters/inbound/http/handler.go.hbs +182 -0
  134. package/templates/generate/module/hexagonal/adapters/inbound/http/handler.minimal.go.hbs +83 -0
  135. package/templates/generate/module/hexagonal/adapters/inbound/http/handler_crud_test.go.hbs +18 -0
  136. package/templates/generate/module/hexagonal/adapters/inbound/http/handler_test.go.hbs +30 -0
  137. package/templates/generate/module/hexagonal/adapters/outbound/postgres/model.go.hbs +37 -0
  138. package/templates/generate/module/hexagonal/adapters/outbound/postgres/repository.go.hbs +95 -0
  139. package/templates/generate/module/{repository_test.go.hbs → hexagonal/adapters/outbound/postgres/repository_test.go.hbs} +8 -8
  140. package/templates/generate/module/hexagonal/application/commands.crud.go.hbs +54 -0
  141. package/templates/generate/module/hexagonal/application/commands.go.hbs +25 -0
  142. package/templates/generate/module/hexagonal/application/cqrs_test.go.hbs +66 -0
  143. package/templates/generate/module/hexagonal/application/dto.go.hbs +35 -0
  144. package/templates/generate/module/hexagonal/application/dto.minimal.go.hbs +25 -0
  145. package/templates/generate/module/hexagonal/application/queries.crud.go.hbs +33 -0
  146. package/templates/generate/module/hexagonal/application/queries.go.hbs +25 -0
  147. package/templates/generate/module/hexagonal/application/service.crud.go.hbs +73 -0
  148. package/templates/generate/module/hexagonal/application/service.go.hbs +29 -0
  149. package/templates/generate/module/hexagonal/application/service_test.go.hbs +62 -0
  150. package/templates/generate/module/hexagonal/composition.go.hbs +27 -0
  151. package/templates/generate/module/hexagonal/domain/entity.go.hbs +20 -0
  152. package/templates/generate/module/hexagonal/domain/errors.go.hbs +11 -0
  153. package/templates/generate/module/hexagonal/ports/repository.go.hbs +38 -0
  154. package/templates/generate/module/migration.up.sql.hbs +1 -1
  155. package/dist/utils/method-patcher.js +0 -357
  156. package/templates/add/auth/docs/google-callback.yaml.hbs +0 -22
  157. package/templates/add/auth/docs/google-login.yaml.hbs +0 -7
  158. package/templates/add/auth/internal/app/user/dto.go.hbs +0 -77
  159. package/templates/add/auth/internal/app/user/errors.go.hbs +0 -43
  160. package/templates/add/auth/internal/app/user/handler.go.hbs +0 -276
  161. package/templates/add/auth/internal/app/user/jwt.go.hbs +0 -108
  162. package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +0 -39
  163. package/templates/add/auth/internal/app/user/model/identity.go.hbs +0 -31
  164. package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +0 -26
  165. package/templates/add/auth/internal/app/user/model/user.go.hbs +0 -30
  166. package/templates/add/auth/internal/app/user/repository.go.hbs +0 -137
  167. package/templates/add/auth/internal/app/user/service.go.hbs +0 -531
  168. package/templates/add/auth/internal/app/user/service_test.go.hbs +0 -316
  169. package/templates/add/auth/internal/app/user/tokenstore.go.hbs +0 -30
  170. package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +0 -144
  171. package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +0 -147
  172. package/templates/add/rbac/internal/app/role/dto.go.hbs +0 -45
  173. package/templates/add/rbac/internal/app/role/errors.go.hbs +0 -39
  174. package/templates/add/rbac/internal/app/role/handler.go.hbs +0 -104
  175. package/templates/add/rbac/internal/app/role/model/permission.go.hbs +0 -12
  176. package/templates/add/rbac/internal/app/role/model/role.go.hbs +0 -22
  177. package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +0 -11
  178. package/templates/add/rbac/internal/app/role/repository.go.hbs +0 -97
  179. package/templates/add/rbac/internal/app/role/service.go.hbs +0 -217
  180. package/templates/generate/module/dto.go.hbs +0 -36
  181. package/templates/generate/module/errors.go.hbs +0 -33
  182. package/templates/generate/module/handler.go.hbs +0 -134
  183. package/templates/generate/module/handler_test.go.hbs +0 -174
  184. package/templates/generate/module/minimal/dto.go.hbs +0 -28
  185. package/templates/generate/module/minimal/handler.go.hbs +0 -48
  186. package/templates/generate/module/minimal/handler_test.go.hbs +0 -10
  187. package/templates/generate/module/minimal/service.go.hbs +0 -45
  188. package/templates/generate/module/minimal/service_test.go.hbs +0 -77
  189. package/templates/generate/module/model/model.go.hbs +0 -36
  190. package/templates/generate/module/repository.go.hbs +0 -103
  191. package/templates/generate/module/service.go.hbs +0 -108
  192. package/templates/generate/module/service_test.go.hbs +0 -161
@@ -0,0 +1,29 @@
1
+ package application
2
+
3
+ import (
4
+ "context"
5
+
6
+ "{{goModule}}/internal/app/{{modulePath}}/domain"
7
+ "{{goModule}}/internal/app/{{modulePath}}/ports"
8
+ )
9
+
10
+ // ServicePort is the inbound application port used by the HTTP adapter.
11
+ type ServicePort interface {
12
+ // go-scaffold:service-interface
13
+ }
14
+
15
+ // Service orchestrates {{pkg}} use cases and depends only on ports.
16
+ type Service struct {
17
+ repo ports.Repository
18
+ }
19
+
20
+ func NewService(repo ports.Repository) *Service {
21
+ return &Service{repo: repo}
22
+ }
23
+
24
+ // Keep these imports and the port visible in a lean module before its first
25
+ // method is generated. Methods are added to this file by `generate method`.
26
+ var _ context.Context
27
+ var _ *domain.{{pascalName}}
28
+
29
+ // go-scaffold:service-methods
@@ -0,0 +1,62 @@
1
+ package application
2
+
3
+ import (
4
+ "context"
5
+ "testing"
6
+
7
+ "{{goModule}}/internal/app/{{modulePath}}/domain"
8
+
9
+ "github.com/google/uuid"
10
+ )
11
+
12
+ // repositoryStub exposes one function per dependency operation. Focused
13
+ // application tests can configure only the calls they expect; an unexpected
14
+ // call panics instead of silently returning a zero value that hides broken
15
+ // orchestration.
16
+ type repositoryStub struct {
17
+ createFn func(context.Context, *domain.{{pascalName}}) error
18
+ findAllFn func(context.Context, int, int) ([]domain.{{pascalName}}, error)
19
+ findByIDFn func(context.Context, uuid.UUID) (*domain.{{pascalName}}, error)
20
+ updateFn func(context.Context, *domain.{{pascalName}}) error
21
+ deleteFn func(context.Context, uuid.UUID) error
22
+ // go-scaffold:repository-stub-fields
23
+ }
24
+
25
+ func (s *repositoryStub) Create(ctx context.Context, m *domain.{{pascalName}}) error {
26
+ if s.createFn == nil {
27
+ panic("unexpected repository.Create call")
28
+ }
29
+ return s.createFn(ctx, m)
30
+ }
31
+ func (s *repositoryStub) FindAll(ctx context.Context, limit, offset int) ([]domain.{{pascalName}}, error) {
32
+ if s.findAllFn == nil {
33
+ panic("unexpected repository.FindAll call")
34
+ }
35
+ return s.findAllFn(ctx, limit, offset)
36
+ }
37
+ func (s *repositoryStub) FindByID(ctx context.Context, id uuid.UUID) (*domain.{{pascalName}}, error) {
38
+ if s.findByIDFn == nil {
39
+ panic("unexpected repository.FindByID call")
40
+ }
41
+ return s.findByIDFn(ctx, id)
42
+ }
43
+ func (s *repositoryStub) Update(ctx context.Context, m *domain.{{pascalName}}) error {
44
+ if s.updateFn == nil {
45
+ panic("unexpected repository.Update call")
46
+ }
47
+ return s.updateFn(ctx, m)
48
+ }
49
+ func (s *repositoryStub) Delete(ctx context.Context, id uuid.UUID) error {
50
+ if s.deleteFn == nil {
51
+ panic("unexpected repository.Delete call")
52
+ }
53
+ return s.deleteFn(ctx, id)
54
+ }
55
+
56
+ func TestService_ComposesWithRepositoryPort(t *testing.T) {
57
+ if NewService(&repositoryStub{}) == nil {
58
+ t.Fatal("expected service")
59
+ }
60
+ }
61
+
62
+ // go-scaffold:repository-stub-methods
@@ -0,0 +1,27 @@
1
+ package {{pkg}}
2
+
3
+ import (
4
+ httpadapter "{{goModule}}/internal/app/{{modulePath}}/adapters/inbound/http"
5
+ "{{goModule}}/internal/app/{{modulePath}}/adapters/outbound/postgres"
6
+ "{{goModule}}/internal/app/{{modulePath}}/application"
7
+ {{#unless cqrs}}
8
+ "{{goModule}}/internal/app/{{modulePath}}/ports"
9
+ {{/unless}}
10
+ "gorm.io/gorm"
11
+ {{#if permission}} "{{goModule}}/internal/shared/middleware"
12
+ {{/if}}
13
+ )
14
+
15
+ // NewHandlerFromDB is this module's composition root. The process root owns
16
+ // the database handle; this package owns the feature's adapter graph.
17
+ func NewHandlerFromDB(db *gorm.DB{{#if auth}}, jwtSecret string{{/if}}{{#if permission}}, authz *middleware.Authz{{/if}}) *httpadapter.Handler {
18
+ repo := postgres.NewRepository(db)
19
+ {{#if cqrs}}
20
+ commands := application.NewCommandHandler(repo)
21
+ queries := application.NewQueryHandler(repo)
22
+ return httpadapter.NewHandler(commands, queries{{#if auth}}, jwtSecret{{/if}}{{#if permission}}, authz{{/if}})
23
+ {{else}}
24
+ var service ports.Repository = repo
25
+ return httpadapter.NewHandler(application.NewService(service){{#if auth}}, jwtSecret{{/if}}{{#if permission}}, authz{{/if}})
26
+ {{/if}}
27
+ }
@@ -0,0 +1,20 @@
1
+ // Package domain contains {{pkg}}'s business state and invariants. It does
2
+ // not import Gin, GORM, SQL drivers, or HTTP concerns.
3
+ package domain
4
+
5
+ import (
6
+ "time"
7
+
8
+ "github.com/google/uuid"
9
+ )
10
+
11
+ // {{pascalName}} is the domain entity. Persistence and transport models are
12
+ // deliberately separate so a database column or JSON field cannot silently
13
+ // become part of the business API.
14
+ type {{pascalName}} struct {
15
+ ID uuid.UUID
16
+ CreatedAt time.Time
17
+ UpdatedAt time.Time
18
+ Version int
19
+ // TODO: add business fields and invariants here.
20
+ }
@@ -0,0 +1,11 @@
1
+ package domain
2
+
3
+ import "errors"
4
+
5
+ var (
6
+ ErrNotFound = errors.New("{{pkg}} not found")
7
+ ErrConflict = errors.New("{{pkg}} conflicts with an existing record")
8
+ ErrStaleVersion = errors.New("{{pkg}} was modified by someone else")
9
+ ErrHasReferences = errors.New("{{pkg}} still has related records")
10
+ ErrNotImplemented = errors.New("{{pkg}} operation is not implemented")
11
+ )
@@ -0,0 +1,38 @@
1
+ // Package ports declares the dependencies owned by the {{pkg}} application.
2
+ // Adapters implement these interfaces; application code never imports a
3
+ // database driver.
4
+ package ports
5
+
6
+ import (
7
+ "context"
8
+
9
+ "{{goModule}}/internal/app/{{modulePath}}/domain"
10
+
11
+ "github.com/google/uuid"
12
+ )
13
+
14
+ // Repository is the complete persistence port for service-style modules.
15
+ type Repository interface {
16
+ Create(context.Context, *domain.{{pascalName}}) error
17
+ FindAll(context.Context, int, int) ([]domain.{{pascalName}}, error)
18
+ FindByID(context.Context, uuid.UUID) (*domain.{{pascalName}}, error)
19
+ Update(context.Context, *domain.{{pascalName}}) error
20
+ Delete(context.Context, uuid.UUID) error
21
+ // go-scaffold:repository-interface
22
+ }
23
+
24
+ // CommandRepository and QueryRepository keep CQRS handlers honest: a write
25
+ // use case cannot grow a read dependency by accident, and vice versa.
26
+ type CommandRepository interface {
27
+ Create(context.Context, *domain.{{pascalName}}) error
28
+ FindByID(context.Context, uuid.UUID) (*domain.{{pascalName}}, error)
29
+ Update(context.Context, *domain.{{pascalName}}) error
30
+ Delete(context.Context, uuid.UUID) error
31
+ // go-scaffold:command-repository-interface
32
+ }
33
+
34
+ type QueryRepository interface {
35
+ FindAll(context.Context, int, int) ([]domain.{{pascalName}}, error)
36
+ FindByID(context.Context, uuid.UUID) (*domain.{{pascalName}}, error)
37
+ // go-scaffold:query-repository-interface
38
+ }
@@ -1,4 +1,4 @@
1
- -- one schema per domain — see model.go's TableName for why
1
+ -- one schema per domain — see adapters/outbound/postgres/model.go's TableName for why
2
2
  CREATE SCHEMA IF NOT EXISTS {{schemaName}};
3
3
 
4
4
  CREATE TABLE {{schemaName}}.{{tableName}} (
@@ -1,357 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.assertMethodAbsent = assertMethodAbsent;
7
- exports.patchMethod = patchMethod;
8
- exports.markersPresent = markersPresent;
9
- const fs_extra_1 = __importDefault(require("fs-extra"));
10
- const marker_patch_1 = require("./marker-patch");
11
- const naming_1 = require("./naming");
12
- const DTO_MARKER = "// go-scaffold:dto";
13
- const REPO_INTERFACE_MARKER = "// go-scaffold:repository-interface";
14
- const REPO_IMPL_MARKER = "// go-scaffold:repository-methods";
15
- const SERVICE_MARKER = "// go-scaffold:service-methods";
16
- const SERVICE_INTERFACE_MARKER = "// go-scaffold:service-interface";
17
- const HANDLER_ROUTES_MARKER = "// go-scaffold:handler-routes";
18
- const HANDLER_FUNCS_MARKER = "// go-scaffold:handler-funcs";
19
- const REPOSITORY_STUB_FIELDS_MARKER = "// go-scaffold:repository-stub-fields";
20
- const REPOSITORY_STUB_METHODS_MARKER = "// go-scaffold:repository-stub-methods";
21
- const LEGACY_FAKE_REPO_METHODS_MARKER = "// go-scaffold:fake-repo-methods";
22
- const UNUSED_G_LINE = "\t_ = g\n";
23
- // writeHandler ensures whatever packages the new handler code references are
24
- // imported (a minimal module starts with only "gin" imported) and drops the
25
- // `_ = g` placeholder once a real route makes it unnecessary — a no-op on a
26
- // full module, which already imports everything and never has that line.
27
- function writeHandler(handlerPath, content, goModule, needs) {
28
- for (const pkg of needs) {
29
- const importPath = pkg.startsWith("net/") ? pkg : `${goModule}/internal/shared/${pkg}`;
30
- content = (0, marker_patch_1.ensureImport)(content, importPath);
31
- }
32
- fs_extra_1.default.writeFileSync(handlerPath, content.replace(UNUSED_G_LINE, ""));
33
- }
34
- function assertNotDuplicate(content, needle, what) {
35
- if (content.includes(needle)) {
36
- throw new Error(`${what} already exists — pick a different method name`);
37
- }
38
- }
39
- function routeCall(type) {
40
- return { get: "GET", post: "POST", put: "PUT", patch: "PATCH", delete: "DELETE" }[type];
41
- }
42
- // assertMethodAbsent is the same check patchMethod runs, exported so callers
43
- // can reach it before their own pre-flight checks. A name that already exists
44
- // is the cause the caller needs to hear about, so it has to be reported ahead
45
- // of anything downstream of it (the OpenAPI document that name would collide
46
- // with, for one) rather than after.
47
- function assertMethodAbsent(paths, method) {
48
- const handlerSig = `func (h *Handler) ${method.handlerName}(`;
49
- const serviceSig = `func (s *Service) ${method.pascalName}(`;
50
- assertNotDuplicate(fs_extra_1.default.readFileSync(paths.handlerPath, "utf8"), handlerSig, `handler method "${method.handlerName}"`);
51
- assertNotDuplicate(fs_extra_1.default.readFileSync(paths.servicePath, "utf8"), serviceSig, `service method "${method.pascalName}"`);
52
- }
53
- function patchMethod(paths, naming, method, opts, goModule) {
54
- assertMethodAbsent(paths, method);
55
- if (opts.type === "get" && opts.getMode === "all") {
56
- patchGetAll(paths, naming, method, goModule);
57
- }
58
- else if (opts.type === "get") {
59
- if (!opts.field)
60
- throw new Error("--field is required for --type get --get-mode one");
61
- if (opts.field.toLowerCase() === "id") {
62
- throw new Error('--field cannot be "id" — GET /:id already exists as the default lookup');
63
- }
64
- patchGetOne(paths, naming, method, opts.field, goModule);
65
- }
66
- else if (opts.type === "post") {
67
- patchPost(paths, naming, method, goModule);
68
- }
69
- else if (opts.type === "put" || opts.type === "patch") {
70
- patchResourceAction(paths, naming, method, opts.type, goModule);
71
- }
72
- else {
73
- patchDelete(paths, method, goModule);
74
- }
75
- patchHandlerServiceInterface(paths.handlerPath, naming, method, opts, goModule);
76
- }
77
- function patchHandlerServiceInterface(handlerPath, naming, method, opts, goModule) {
78
- let signature;
79
- let needsModel = true;
80
- let needsUUID = false;
81
- if (opts.type === "get" && opts.getMode === "all") {
82
- signature = `${method.pascalName}(context.Context, int, int) ([]model.${naming.pascalName}, error)`;
83
- }
84
- else if (opts.type === "get") {
85
- signature = `${method.pascalName}(context.Context, string) (*model.${naming.pascalName}, error)`;
86
- }
87
- else if (opts.type === "post") {
88
- signature = `${method.pascalName}(context.Context, ${method.pascalName}Input) (*model.${naming.pascalName}, error)`;
89
- }
90
- else if (opts.type === "put" || opts.type === "patch") {
91
- signature = `${method.pascalName}(context.Context, uuid.UUID) (*model.${naming.pascalName}, error)`;
92
- needsUUID = true;
93
- }
94
- else {
95
- signature = `${method.pascalName}(context.Context, uuid.UUID) error`;
96
- needsModel = false;
97
- needsUUID = true;
98
- }
99
- let handler = fs_extra_1.default.readFileSync(handlerPath, "utf8");
100
- if (!(0, marker_patch_1.hasMarker)(handler, SERVICE_INTERFACE_MARKER)) {
101
- // Projects scaffolded before the narrow service interface stored *Service
102
- // directly. The concrete type already exposes generated methods, so there
103
- // is no interface declaration to patch and no migration is required.
104
- if (handler.includes("svc *Service"))
105
- return;
106
- throw new Error(`marker "${SERVICE_INTERFACE_MARKER}" not found and Handler does not use legacy *Service wiring`);
107
- }
108
- handler = (0, marker_patch_1.insertBeforeMarker)(handler, SERVICE_INTERFACE_MARKER, signature);
109
- handler = (0, marker_patch_1.ensureImport)(handler, "context");
110
- if (needsModel) {
111
- handler = (0, marker_patch_1.ensureImport)(handler, `${goModule}/internal/app/${naming.pkg}/model`);
112
- }
113
- if (needsUUID) {
114
- handler = (0, marker_patch_1.ensureImport)(handler, "github.com/google/uuid");
115
- }
116
- fs_extra_1.default.writeFileSync(handlerPath, handler);
117
- }
118
- function patchGetAll(paths, naming, method, goModule) {
119
- let handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
120
- handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_ROUTES_MARKER, `g.GET("/${method.pathSegment}", h.${method.handlerName})`);
121
- handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_FUNCS_MARKER, [
122
- `func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
123
- `\tp := pagination.Parse(c)`,
124
- `\titems, err := h.svc.${method.pascalName}(c.Request.Context(), p.Limit, p.Offset)`,
125
- `\tif err != nil {`,
126
- `\t\tc.Error(err)`,
127
- `\t\treturn`,
128
- `\t}`,
129
- `\tout := make([]response, len(items))`,
130
- `\tfor i := range items {`,
131
- `\t\tout[i] = toResponse(&items[i])`,
132
- `\t}`,
133
- `\tc.JSON(http.StatusOK, p.Response(out))`,
134
- `}`,
135
- ``,
136
- ].join("\n"));
137
- writeHandler(paths.handlerPath, handler, goModule, ["net/http", "pagination"]);
138
- let service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
139
- service = (0, marker_patch_1.insertBeforeMarker)(service, SERVICE_MARKER, [
140
- `func (s *Service) ${method.pascalName}(ctx context.Context, limit, offset int) ([]model.${naming.pascalName}, error) {`,
141
- `\t// TODO: add real filtering for "${method.name}" — currently reuses FindAll`,
142
- `\titems, err := s.repo.FindAll(ctx, limit, offset)`,
143
- `\tif err != nil {`,
144
- `\t\treturn nil, apperror.NewInternal()`,
145
- `\t}`,
146
- `\treturn items, nil`,
147
- `}`,
148
- ``,
149
- ].join("\n"));
150
- fs_extra_1.default.writeFileSync(paths.servicePath, service);
151
- }
152
- function patchGetOne(paths, naming, method, rawField, goModule) {
153
- const fieldParam = (0, naming_1.toCamelCase)(rawField);
154
- const fieldPascal = (0, naming_1.toPascalCase)(rawField);
155
- const fieldColumn = (0, naming_1.toDbName)(rawField);
156
- let repo = fs_extra_1.default.readFileSync(paths.repositoryPath, "utf8");
157
- assertNotDuplicate(repo, `FindBy${fieldPascal}(`, `repository method "FindBy${fieldPascal}"`);
158
- repo = (0, marker_patch_1.insertBeforeMarker)(repo, REPO_IMPL_MARKER, [
159
- `func (r *Repository) FindBy${fieldPascal}(ctx context.Context, ${fieldParam} string) (*model.${naming.pascalName}, error) {`,
160
- `\tvar m model.${naming.pascalName}`,
161
- `\t// TODO: add the "${fieldColumn}" column and an index on it — this query`,
162
- `\t// compiles against any table and only fails when it runs.`,
163
- `\t//`,
164
- `\t// tx.From, like every other method here: a lookup that reached for r.db`,
165
- `\t// directly would read outside a transaction its caller had opened, and`,
166
- `\t// so miss that transaction's own uncommitted writes.`,
167
- `\tif err := tx.From(ctx, r.db).WithContext(ctx).First(&m, "${fieldColumn} = ?", ${fieldParam}).Error; err != nil {`,
168
- `\t\treturn nil, err`,
169
- `\t}`,
170
- `\treturn &m, nil`,
171
- `}`,
172
- ``,
173
- ].join("\n"));
174
- fs_extra_1.default.writeFileSync(paths.repositoryPath, repo);
175
- let service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
176
- service = (0, marker_patch_1.insertBeforeMarker)(service, REPO_INTERFACE_MARKER, `FindBy${fieldPascal}(ctx context.Context, ${fieldParam} string) (*model.${naming.pascalName}, error)`);
177
- // The repository interface just grew, so the test double needs a matching
178
- // method or focused service tests stop compiling. New projects use a
179
- // function-backed stub; projects scaffolded before that refactor retain the
180
- // old fakeRepo marker and error behavior.
181
- let serviceTest = fs_extra_1.default.readFileSync(paths.serviceTestPath, "utf8");
182
- if ((0, marker_patch_1.hasMarker)(serviceTest, REPOSITORY_STUB_FIELDS_MARKER) &&
183
- (0, marker_patch_1.hasMarker)(serviceTest, REPOSITORY_STUB_METHODS_MARKER)) {
184
- serviceTest = (0, marker_patch_1.insertBeforeMarker)(serviceTest, REPOSITORY_STUB_FIELDS_MARKER, `findBy${fieldPascal}Fn func(context.Context, string) (*model.${naming.pascalName}, error)`);
185
- serviceTest = (0, marker_patch_1.insertBeforeMarker)(serviceTest, REPOSITORY_STUB_METHODS_MARKER, [
186
- `//nolint:unused`,
187
- `func (s *repositoryStub) FindBy${fieldPascal}(ctx context.Context, value string) (*model.${naming.pascalName}, error) {`,
188
- `\tif s.findBy${fieldPascal}Fn == nil {`,
189
- `\t\tpanic("unexpected repository.FindBy${fieldPascal} call")`,
190
- `\t}`,
191
- `\treturn s.findBy${fieldPascal}Fn(ctx, value)`,
192
- `}`,
193
- ``,
194
- ].join("\n"));
195
- }
196
- else if ((0, marker_patch_1.hasMarker)(serviceTest, LEGACY_FAKE_REPO_METHODS_MARKER)) {
197
- serviceTest = (0, marker_patch_1.insertBeforeMarker)(serviceTest, LEGACY_FAKE_REPO_METHODS_MARKER, [
198
- `//nolint:unused`,
199
- `func (f *fakeRepo) FindBy${fieldPascal}(context.Context, string) (*model.${naming.pascalName}, error) {`,
200
- `\tif f.err != nil {`,
201
- `\t\treturn nil, f.err`,
202
- `\t}`,
203
- `\treturn f.m, nil`,
204
- `}`,
205
- ``,
206
- ].join("\n"));
207
- }
208
- else {
209
- throw new Error(`neither current repository stub markers nor legacy "${LEGACY_FAKE_REPO_METHODS_MARKER}" found in ${paths.serviceTestPath}`);
210
- }
211
- fs_extra_1.default.writeFileSync(paths.serviceTestPath, serviceTest);
212
- service = (0, marker_patch_1.insertBeforeMarker)(service, SERVICE_MARKER, [
213
- `func (s *Service) ${method.pascalName}(ctx context.Context, ${fieldParam} string) (*model.${naming.pascalName}, error) {`,
214
- `\tm, err := s.repo.FindBy${fieldPascal}(ctx, ${fieldParam})`,
215
- `\tif err != nil {`,
216
- `\t\treturn nil, wrapFindErr(err)`,
217
- `\t}`,
218
- `\treturn m, nil`,
219
- `}`,
220
- ``,
221
- ].join("\n"));
222
- fs_extra_1.default.writeFileSync(paths.servicePath, service);
223
- let handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
224
- handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_ROUTES_MARKER, `g.GET("/${fieldColumn}/:${fieldParam}", h.${method.handlerName})`);
225
- handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_FUNCS_MARKER, [
226
- `func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
227
- `\t${fieldParam} := c.Param("${fieldParam}")`,
228
- `\tm, err := h.svc.${method.pascalName}(c.Request.Context(), ${fieldParam})`,
229
- `\tif err != nil {`,
230
- `\t\tc.Error(err)`,
231
- `\t\treturn`,
232
- `\t}`,
233
- `\tc.JSON(http.StatusOK, toResponse(m))`,
234
- `}`,
235
- ``,
236
- ].join("\n"));
237
- writeHandler(paths.handlerPath, handler, goModule, ["net/http"]);
238
- }
239
- function patchPost(paths, naming, method, goModule) {
240
- const inputName = `${method.pascalName}Input`;
241
- let dto = fs_extra_1.default.readFileSync(paths.dtoPath, "utf8");
242
- assertNotDuplicate(dto, `type ${inputName} struct`, `DTO "${inputName}"`);
243
- dto = (0, marker_patch_1.insertBeforeMarker)(dto, DTO_MARKER, [`type ${inputName} struct {`, `\t// TODO: add request fields`, `}`, ``].join("\n"));
244
- fs_extra_1.default.writeFileSync(paths.dtoPath, dto);
245
- let handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
246
- handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_ROUTES_MARKER, `g.POST("/${method.pathSegment}", h.${method.handlerName})`);
247
- handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_FUNCS_MARKER, [
248
- `func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
249
- `\tvar in ${inputName}`,
250
- `\tif err := c.ShouldBindJSON(&in); err != nil {`,
251
- `\t\tc.Error(httpx.BindErr(err))`,
252
- `\t\treturn`,
253
- `\t}`,
254
- `\tm, err := h.svc.${method.pascalName}(c.Request.Context(), in)`,
255
- `\tif err != nil {`,
256
- `\t\tc.Error(err)`,
257
- `\t\treturn`,
258
- `\t}`,
259
- `\tc.JSON(http.StatusCreated, toResponse(m))`,
260
- `}`,
261
- ``,
262
- ].join("\n"));
263
- writeHandler(paths.handlerPath, handler, goModule, ["net/http", "httpx"]);
264
- let service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
265
- service = (0, marker_patch_1.insertBeforeMarker)(service, SERVICE_MARKER, [
266
- `func (s *Service) ${method.pascalName}(ctx context.Context, in ${inputName}) (*model.${naming.pascalName}, error) {`,
267
- `\t// TODO: implement "${method.name}" — this stub does nothing yet`,
268
- `\t_ = in`,
269
- `\treturn nil, apperror.NewInternal()`,
270
- `}`,
271
- ``,
272
- ].join("\n"));
273
- fs_extra_1.default.writeFileSync(paths.servicePath, service);
274
- }
275
- function patchResourceAction(paths, naming, method, type, goModule) {
276
- let handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
277
- handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_ROUTES_MARKER, `g.${routeCall(type)}("/:id/${method.pathSegment}", h.${method.handlerName})`);
278
- handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_FUNCS_MARKER, [
279
- `func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
280
- `\tid, ok := httpx.ParseID(c)`,
281
- `\tif !ok {`,
282
- `\t\treturn`,
283
- `\t}`,
284
- `\tm, err := h.svc.${method.pascalName}(c.Request.Context(), id)`,
285
- `\tif err != nil {`,
286
- `\t\tc.Error(err)`,
287
- `\t\treturn`,
288
- `\t}`,
289
- `\tc.JSON(http.StatusOK, toResponse(m))`,
290
- `}`,
291
- ``,
292
- ].join("\n"));
293
- writeHandler(paths.handlerPath, handler, goModule, ["net/http", "httpx"]);
294
- let service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
295
- service = (0, marker_patch_1.insertBeforeMarker)(service, SERVICE_MARKER, [
296
- `func (s *Service) ${method.pascalName}(ctx context.Context, id uuid.UUID) (*model.${naming.pascalName}, error) {`,
297
- `\tm, err := s.repo.FindByID(ctx, id)`,
298
- `\tif err != nil {`,
299
- `\t\treturn nil, wrapFindErr(err)`,
300
- `\t}`,
301
- `\t// TODO: implement "${method.name}" — currently a no-op save`,
302
- `\t// The version compared here is the one just read, so this only`,
303
- `\t// catches a writer that lands between the read above and this write.`,
304
- `\t// It is NOT the check Update does: that one compares against the version`,
305
- `\t// the *client* last saw, which is what catches someone submitting a`,
306
- `\t// form built from a copy that has since gone stale. If this endpoint`,
307
- `\t// needs that, give it an input struct carrying Version and assign it`,
308
- `\t// to m.Version here, the way service.go's Update does.`,
309
- `\tif err := s.repo.Update(ctx, m); err != nil {`,
310
- `\t\tif errors.Is(err, ErrStaleVersion) {`,
311
- `\t\t\treturn nil, errStale()`,
312
- `\t\t}`,
313
- `\t\treturn nil, apperror.NewInternal()`,
314
- `\t}`,
315
- `\treturn m, nil`,
316
- `}`,
317
- ``,
318
- ].join("\n"));
319
- fs_extra_1.default.writeFileSync(paths.servicePath, service);
320
- }
321
- function patchDelete(paths, method, goModule) {
322
- let handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
323
- handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_ROUTES_MARKER, `g.DELETE("/:id/${method.pathSegment}", h.${method.handlerName})`);
324
- handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_FUNCS_MARKER, [
325
- `func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
326
- `\tid, ok := httpx.ParseID(c)`,
327
- `\tif !ok {`,
328
- `\t\treturn`,
329
- `\t}`,
330
- `\tif err := h.svc.${method.pascalName}(c.Request.Context(), id); err != nil {`,
331
- `\t\tc.Error(err)`,
332
- `\t\treturn`,
333
- `\t}`,
334
- `\tc.Status(http.StatusNoContent)`,
335
- `}`,
336
- ``,
337
- ].join("\n"));
338
- writeHandler(paths.handlerPath, handler, goModule, ["net/http", "httpx"]);
339
- let service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
340
- service = (0, marker_patch_1.insertBeforeMarker)(service, SERVICE_MARKER, [
341
- `func (s *Service) ${method.pascalName}(ctx context.Context, id uuid.UUID) error {`,
342
- `\t// TODO: implement "${method.name}" — this stub does nothing yet`,
343
- `\treturn apperror.NewInternal()`,
344
- `}`,
345
- ``,
346
- ].join("\n"));
347
- fs_extra_1.default.writeFileSync(paths.servicePath, service);
348
- }
349
- function markersPresent(handlerPath, servicePath) {
350
- const handler = fs_extra_1.default.readFileSync(handlerPath, "utf8");
351
- const service = fs_extra_1.default.readFileSync(servicePath, "utf8");
352
- return ((0, marker_patch_1.hasMarker)(handler, HANDLER_ROUTES_MARKER) &&
353
- (0, marker_patch_1.hasMarker)(handler, HANDLER_FUNCS_MARKER) &&
354
- ((0, marker_patch_1.hasMarker)(handler, SERVICE_INTERFACE_MARKER) || handler.includes("svc *Service")) &&
355
- (0, marker_patch_1.hasMarker)(service, SERVICE_MARKER) &&
356
- (0, marker_patch_1.hasMarker)(service, REPO_INTERFACE_MARKER));
357
- }
@@ -1,22 +0,0 @@
1
- get:
2
- summary: Google OAuth callback
3
- description: Redirect target Google sends the browser back to after consent — not called directly by API clients.
4
- operationId: googleCallback
5
- tags: [auth]
6
- security: []
7
- parameters:
8
- - name: code
9
- in: query
10
- required: true
11
- schema: { type: string }
12
- - name: state
13
- in: query
14
- required: true
15
- schema: { type: string }
16
- responses:
17
- "200":
18
- description: ok, refresh_token set as an httpOnly cookie
19
- content:
20
- application/json:
21
- schema: { $ref: './schemas.yaml#/AuthResponse' }
22
- "401": { $ref: '../common/responses.yaml#/UnauthorizedError' }
@@ -1,7 +0,0 @@
1
- get:
2
- summary: Start the Google OAuth login flow
3
- operationId: googleLogin
4
- tags: [auth]
5
- security: []
6
- responses:
7
- "302": { description: redirect to Google's consent screen }
@@ -1,77 +0,0 @@
1
- package user
2
-
3
- import (
4
- "time"
5
-
6
- "{{goModule}}/internal/app/user/model"
7
-
8
- "github.com/google/uuid"
9
- )
10
-
11
- type registerInput struct {
12
- Email string `json:"email" binding:"required,email"`
13
- Password string `json:"password" binding:"required,min=8"`
14
- Name string `json:"name" binding:"required"`
15
- }
16
-
17
- type loginInput struct {
18
- Email string `json:"email" binding:"required,email"`
19
- Password string `json:"password" binding:"required"`
20
- }
21
-
22
- type forgotPasswordInput struct {
23
- Email string `json:"email" binding:"required,email"`
24
- }
25
-
26
- type resetPasswordInput struct {
27
- Token string `json:"token" binding:"required"`
28
- NewPassword string `json:"new_password" binding:"required,min=8"`
29
- }
30
-
31
- type verifyEmailInput struct {
32
- Token string `json:"token" binding:"required"`
33
- }
34
-
35
- // authResponse is what the service returns internally (both tokens); the
36
- // handler sends the refresh token as an httpOnly cookie instead of echoing
37
- // it in the body, so meResponse never carries it — see toCookieResponse.
38
- type authResponse struct {
39
- AccessToken string `json:"access_token"`
40
- RefreshToken string `json:"-"`
41
- TokenType string `json:"token_type"`
42
- ExpiresIn int `json:"expires_in"`
43
- }
44
-
45
- type authCookieResponse struct {
46
- AccessToken string `json:"access_token"`
47
- TokenType string `json:"token_type"`
48
- ExpiresIn int `json:"expires_in"`
49
- }
50
-
51
- func toCookieResponse(a *authResponse) authCookieResponse {
52
- return authCookieResponse{AccessToken: a.AccessToken, TokenType: a.TokenType, ExpiresIn: a.ExpiresIn}
53
- }
54
-
55
- type meResponse struct {
56
- ID uuid.UUID `json:"id"`
57
- Email string `json:"email"`
58
- Name string `json:"name"`
59
- AvatarURL string `json:"avatar_url"`
60
- EmailVerified bool `json:"email_verified"`
61
- // go-scaffold:user-me-fields
62
- CreatedAt time.Time `json:"created_at"`
63
- }
64
-
65
- func toMeResponse(u *model.User) meResponse {
66
- return meResponse{
67
- ID: u.ID,
68
- Email: u.Email,
69
- Name: u.Name,
70
- AvatarURL: u.AvatarURL,
71
- EmailVerified: u.EmailVerified,
72
- CreatedAt: u.CreatedAt,
73
- // go-scaffold:user-me-values
74
- }
75
- }
76
-
77
- // go-scaffold:user-dto