@nakedev/go-scaffold 0.1.2 → 0.1.3
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 +75 -0
- package/dist/commands/auth.js +129 -0
- package/dist/commands/create.js +3 -2
- package/dist/commands/generate.js +59 -1
- package/dist/commands/method.js +3 -0
- package/dist/commands/migration.js +34 -0
- package/dist/commands/rbac.js +103 -0
- package/dist/commands/remove.js +19 -2
- package/dist/commands/worker.js +75 -0
- package/dist/index.js +66 -3
- package/dist/prompts/create-wizard.js +6 -1
- package/dist/prompts/generate-wizard.js +8 -0
- package/dist/templates/auth-manifest.js +19 -0
- package/dist/templates/create-manifest.js +25 -0
- package/dist/templates/rbac-manifest.js +17 -0
- package/dist/templates/worker-manifest.js +12 -0
- package/dist/utils/auth-patcher.js +96 -0
- package/dist/utils/gocheck.js +65 -0
- package/dist/utils/main-patcher.js +8 -1
- package/dist/utils/migrations.js +30 -8
- package/dist/utils/openapi-patcher.js +16 -0
- package/dist/utils/platform-patcher.js +59 -0
- package/dist/utils/rbac-patcher.js +277 -0
- package/dist/utils/version.js +24 -0
- package/package.json +2 -2
- package/templates/add/auth/cmd/seed/main.go.hbs +76 -0
- package/templates/add/auth/docs/forgot-password.yaml.hbs +19 -0
- package/templates/add/auth/docs/google-callback.yaml.hbs +22 -0
- package/templates/add/auth/docs/google-login.yaml.hbs +7 -0
- package/templates/add/auth/docs/login.yaml.hbs +19 -0
- package/templates/add/auth/docs/logout.yaml.hbs +8 -0
- package/templates/add/auth/docs/refresh.yaml.hbs +15 -0
- package/templates/add/auth/docs/register.yaml.hbs +19 -0
- package/templates/add/auth/docs/reset-password.yaml.hbs +16 -0
- package/templates/add/auth/docs/schemas.yaml.hbs +58 -0
- package/templates/add/auth/docs/users-me-logout-all.yaml.hbs +9 -0
- package/templates/add/auth/docs/users-me-resend-verification.yaml.hbs +10 -0
- package/templates/add/auth/docs/users-me.yaml.hbs +12 -0
- package/templates/add/auth/docs/verify-email.yaml.hbs +16 -0
- package/templates/add/auth/internal/app/user/dto.go.hbs +77 -0
- package/templates/add/auth/internal/app/user/errors.go.hbs +36 -0
- package/templates/add/auth/internal/app/user/handler.go.hbs +235 -0
- package/templates/add/auth/internal/app/user/jwt.go.hbs +84 -0
- package/templates/add/auth/internal/app/user/model/identity.go.hbs +28 -0
- package/templates/add/auth/internal/app/user/model/user.go.hbs +22 -0
- package/templates/add/auth/internal/app/user/repository.go.hbs +84 -0
- package/templates/add/auth/internal/app/user/service.go.hbs +447 -0
- package/templates/add/auth/internal/app/user/service_test.go.hbs +237 -0
- package/templates/add/auth/internal/app/user/tokenstore.go.hbs +159 -0
- package/templates/add/auth/internal/shared/middleware/auth.go.hbs +64 -0
- package/templates/add/auth/internal/shared/middleware/ratelimit.go.hbs +44 -0
- package/templates/add/auth/migrations/create_identities.down.sql.hbs +1 -0
- package/templates/add/auth/migrations/create_identities.up.sql.hbs +11 -0
- package/templates/add/auth/migrations/create_users.down.sql.hbs +1 -0
- package/templates/add/auth/migrations/create_users.up.sql.hbs +9 -0
- package/templates/add/rbac/docs/permissions.yaml.hbs +24 -0
- package/templates/add/rbac/docs/role-permissions.yaml.hbs +31 -0
- package/templates/add/rbac/docs/role.yaml.hbs +17 -0
- package/templates/add/rbac/docs/roles.yaml.hbs +43 -0
- package/templates/add/rbac/docs/schemas.yaml.hbs +37 -0
- package/templates/add/rbac/docs/user-set-role.yaml.hbs +23 -0
- package/templates/add/rbac/docs/user.yaml.hbs +16 -0
- package/templates/add/rbac/docs/users.yaml.hbs +23 -0
- package/templates/add/rbac/internal/app/role/dto.go.hbs +40 -0
- package/templates/add/rbac/internal/app/role/errors.go.hbs +39 -0
- package/templates/add/rbac/internal/app/role/handler.go.hbs +101 -0
- package/templates/add/rbac/internal/app/role/model/permission.go.hbs +9 -0
- package/templates/add/rbac/internal/app/role/model/role.go.hbs +18 -0
- package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +8 -0
- package/templates/add/rbac/internal/app/role/repository.go.hbs +96 -0
- package/templates/add/rbac/internal/app/role/service.go.hbs +210 -0
- package/templates/add/rbac/internal/app/role/service_test.go.hbs +119 -0
- package/templates/add/rbac/internal/shared/middleware/authz.go.hbs +88 -0
- package/templates/add/rbac/internal/shared/middleware/authz_test.go.hbs +88 -0
- package/templates/add/rbac/migrations/add_roles.down.sql.hbs +15 -0
- package/templates/add/rbac/migrations/add_roles.up.sql.hbs +35 -0
- package/templates/add/worker/cmd/worker/main.go.hbs +77 -0
- package/templates/add/worker/internal/platform/cache/redis.go.hbs +18 -0
- package/templates/add/worker/internal/platform/mail/mail.go.hbs +48 -0
- package/templates/add/worker/internal/platform/mail/task.go.hbs +52 -0
- package/templates/add/worker/internal/platform/queue/client.go.hbs +31 -0
- package/templates/add/worker/internal/platform/queue/server.go.hbs +68 -0
- package/templates/create/base/.env.example.hbs +20 -0
- package/templates/create/base/.github/workflows/ci.yml.hbs +4 -2
- package/templates/create/base/.gitignore.hbs +2 -0
- package/templates/create/base/Makefile.hbs +30 -5
- package/templates/create/base/README.md.hbs +36 -8
- package/templates/create/base/cmd/api/main.go.hbs +26 -1
- package/templates/create/base/internal/platform/database/database.go.hbs +53 -0
- package/templates/create/base/internal/shared/config/config.go.hbs +43 -1
- package/templates/create/base/internal/shared/middleware/cors.go.hbs +29 -0
- package/templates/create/base/internal/shared/middleware/error.go.hbs +13 -1
- package/templates/create/base/migrations/embed.go.hbs +15 -0
- package/templates/create/features/docs/architecture.md.hbs +22 -0
- package/templates/create/features/docs/common/responses.yaml.hbs +15 -0
- package/templates/create/features/docs/observability/metrics.yaml.hbs +12 -0
- package/templates/create/features/docs/openapi.yaml.hbs +13 -0
- package/templates/create/features/docs/techstack.md.hbs +3 -0
- package/templates/create/features/observability/middleware/metrics.go.hbs +41 -0
- package/templates/create/features/observability/middleware/tracing.go.hbs +46 -0
- package/templates/create/features/observability/platform/telemetry/tracing.go.hbs +130 -0
- package/templates/generate/module/handler.go.hbs +20 -3
- package/templates/generate/module/handler_test.go.hbs +49 -6
- package/templates/generate/module/minimal/handler.go.hbs +21 -3
- package/templates/generate/module/minimal/handler_test.go.hbs +53 -6
- package/templates/generate/module/permission.down.sql.hbs +5 -0
- package/templates/generate/module/permission.up.sql.hbs +4 -0
- package/dist/utils/module-paths.js +0 -33
|
@@ -0,0 +1,277 @@
|
|
|
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.patchAuthDocsForRbac = patchAuthDocsForRbac;
|
|
7
|
+
exports.patchConfigForRbac = patchConfigForRbac;
|
|
8
|
+
exports.patchUserModelForRbac = patchUserModelForRbac;
|
|
9
|
+
exports.patchMiddlewareAuthForRbac = patchMiddlewareAuthForRbac;
|
|
10
|
+
exports.patchUserJWTForRbac = patchUserJWTForRbac;
|
|
11
|
+
exports.patchUserServiceForRbac = patchUserServiceForRbac;
|
|
12
|
+
exports.patchUserServiceTestForRbac = patchUserServiceTestForRbac;
|
|
13
|
+
exports.patchUserDTOForRbac = patchUserDTOForRbac;
|
|
14
|
+
exports.patchUserHandlerForRbac = patchUserHandlerForRbac;
|
|
15
|
+
exports.patchUserErrorsForRbac = patchUserErrorsForRbac;
|
|
16
|
+
exports.patchMainGoForRbac = patchMainGoForRbac;
|
|
17
|
+
exports.patchCmdSeedForRbac = patchCmdSeedForRbac;
|
|
18
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
19
|
+
const marker_patch_1 = require("./marker-patch");
|
|
20
|
+
const IMPORT_MARKER = "// go-scaffold:imports";
|
|
21
|
+
const MODEL_MARKER = "// go-scaffold:models";
|
|
22
|
+
const CONFIG_FIELDS_MARKER = "// go-scaffold:config-fields";
|
|
23
|
+
const CONFIG_LOAD_MARKER = "// go-scaffold:config-load";
|
|
24
|
+
// patchAuthDocsForRbac adds `role` to the hand-written MeResponse schema in
|
|
25
|
+
// docs/auth/schemas.yaml — same marker convention as the Go dto.go patch
|
|
26
|
+
// (patchUserDTOForRbac), since GET/PATCH /users(/me) all serialize the same
|
|
27
|
+
// model.User whether or not RBAC is installed.
|
|
28
|
+
function patchAuthDocsForRbac(authSchemasYamlPath) {
|
|
29
|
+
if (!fs_extra_1.default.existsSync(authSchemasYamlPath))
|
|
30
|
+
return; // openapi docs feature disabled
|
|
31
|
+
let content = fs_extra_1.default.readFileSync(authSchemasYamlPath, "utf8");
|
|
32
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "# go-scaffold:me-response-fields", "role: { type: string }", "role: { type: string }");
|
|
33
|
+
fs_extra_1.default.writeFileSync(authSchemasYamlPath, content);
|
|
34
|
+
}
|
|
35
|
+
// patchConfigForRbac adds the Authz permission-cache TTL — configurable
|
|
36
|
+
// instead of the hardcoded 1 minute it started as, same marker-based
|
|
37
|
+
// insertion patchConfigForAuth uses.
|
|
38
|
+
function patchConfigForRbac(configGoPath) {
|
|
39
|
+
let content = fs_extra_1.default.readFileSync(configGoPath, "utf8");
|
|
40
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_FIELDS_MARKER, "AuthzCacheTTL time.Duration", "AuthzCacheTTL time.Duration");
|
|
41
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_LOAD_MARKER, 'AuthzCacheTTL: time.Duration(envInt("AUTHZ_CACHE_TTL_MIN", 1)) * time.Minute,', "AuthzCacheTTL: time.Duration");
|
|
42
|
+
fs_extra_1.default.writeFileSync(configGoPath, content);
|
|
43
|
+
}
|
|
44
|
+
// patchUserModelForRbac adds the Role column to model.User — default 'staff'
|
|
45
|
+
// at the DB level (via the gorm tag), so every existing user-creation path
|
|
46
|
+
// (Register, Google, cmd/seed) gets a sane role without each one needing to
|
|
47
|
+
// set it explicitly.
|
|
48
|
+
function patchUserModelForRbac(userModelPath) {
|
|
49
|
+
let content = fs_extra_1.default.readFileSync(userModelPath, "utf8");
|
|
50
|
+
const field = `Role string \`json:"role" gorm:"type:varchar(20);not null;default:'staff'"\``;
|
|
51
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-fields", field, "Role string");
|
|
52
|
+
fs_extra_1.default.writeFileSync(userModelPath, content);
|
|
53
|
+
}
|
|
54
|
+
// patchMiddlewareAuthForRbac adds RoleKey + the Role claim to the shared
|
|
55
|
+
// middleware's own accessClaims copy (see auth.go's own comment on why it's
|
|
56
|
+
// duplicated, not imported, from the user package's claims type).
|
|
57
|
+
function patchMiddlewareAuthForRbac(middlewareAuthPath) {
|
|
58
|
+
let content = fs_extra_1.default.readFileSync(middlewareAuthPath, "utf8");
|
|
59
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:middleware-auth-keys", 'const RoleKey = "role"', 'RoleKey = "role"');
|
|
60
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:middleware-auth-claims", 'Role string `json:"role,omitempty"`', "Role string");
|
|
61
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:middleware-auth-context", "c.Set(RoleKey, claims.Role)", "c.Set(RoleKey");
|
|
62
|
+
fs_extra_1.default.writeFileSync(middlewareAuthPath, content);
|
|
63
|
+
}
|
|
64
|
+
// patchUserJWTForRbac adds the Role claim to the access token: the claim
|
|
65
|
+
// itself (accessClaims.Role), an extra issueAccessToken param, and the value
|
|
66
|
+
// wired into the claims literal — three separate marker points because the
|
|
67
|
+
// param list and the struct literal are formatted one-arg-per-line
|
|
68
|
+
// specifically so a marker patch can extend either without reformatting.
|
|
69
|
+
function patchUserJWTForRbac(jwtGoPath) {
|
|
70
|
+
let content = fs_extra_1.default.readFileSync(jwtGoPath, "utf8");
|
|
71
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:jwt-claims", 'Role string `json:"role,omitempty"`', "Role string");
|
|
72
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:issue-access-token-params", "role string,", "role string,");
|
|
73
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:jwt-claims-values", "Role: role,", "Role: role,");
|
|
74
|
+
fs_extra_1.default.writeFileSync(jwtGoPath, content);
|
|
75
|
+
}
|
|
76
|
+
// patchUserServiceForRbac wires a role.Service dependency into user.Service
|
|
77
|
+
// (via a consumer-side roleChecker interface, same convention as
|
|
78
|
+
// repository/mailer/tokenStore) and adds SetRole, the one place a user's
|
|
79
|
+
// role actually changes after creation.
|
|
80
|
+
function patchUserServiceForRbac(serviceGoPath) {
|
|
81
|
+
let content = fs_extra_1.default.readFileSync(serviceGoPath, "utf8");
|
|
82
|
+
const roleCheckerInterface = [
|
|
83
|
+
"// roleChecker = what the service needs from the role domain to validate a",
|
|
84
|
+
"// role assignment — declared consumer-side, same convention as repository/mailer.",
|
|
85
|
+
"type roleChecker interface {",
|
|
86
|
+
"\tCodeExists(ctx context.Context, code string) (bool, error)",
|
|
87
|
+
"}",
|
|
88
|
+
].join("\n");
|
|
89
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-interfaces", roleCheckerInterface, "type roleChecker interface");
|
|
90
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-service-fields", "roles roleChecker", "roles roleChecker");
|
|
91
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-service-params", "roles roleChecker,", "roles roleChecker,");
|
|
92
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-service-init", "roles: roles,", "roles: roles,");
|
|
93
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:issue-access-token-args", "u.Role,", "u.Role,");
|
|
94
|
+
const setRoleMethod = [
|
|
95
|
+
"// SetRole assigns userID a new role — validated against the role catalog",
|
|
96
|
+
"// (not just the DB's FK) so a typo'd code fails with a clear 422 here",
|
|
97
|
+
"// instead of surfacing as an opaque FK-violation, and so AutoMigrate-only",
|
|
98
|
+
"// test schemas (which never create the FK) still reject it correctly.",
|
|
99
|
+
"func (s *Service) SetRole(ctx context.Context, userID uuid.UUID, roleCode string) (*model.User, error) {",
|
|
100
|
+
"\tu, err := s.repo.FindByID(ctx, userID)",
|
|
101
|
+
"\tif err != nil {",
|
|
102
|
+
"\t\tif errors.Is(err, gorm.ErrRecordNotFound) {",
|
|
103
|
+
"\t\t\treturn nil, errNotFound()",
|
|
104
|
+
"\t\t}",
|
|
105
|
+
"\t\treturn nil, apperror.NewInternal()",
|
|
106
|
+
"\t}",
|
|
107
|
+
"\texists, err := s.roles.CodeExists(ctx, roleCode)",
|
|
108
|
+
"\tif err != nil {",
|
|
109
|
+
"\t\treturn nil, apperror.NewInternal()",
|
|
110
|
+
"\t}",
|
|
111
|
+
"\tif !exists {",
|
|
112
|
+
"\t\treturn nil, errUnknownRole()",
|
|
113
|
+
"\t}",
|
|
114
|
+
"\tu.Role = roleCode",
|
|
115
|
+
"\tif err := s.repo.UpdateUser(ctx, u); err != nil {",
|
|
116
|
+
"\t\treturn nil, apperror.NewInternal()",
|
|
117
|
+
"\t}",
|
|
118
|
+
"\treturn u, nil",
|
|
119
|
+
"}",
|
|
120
|
+
].join("\n");
|
|
121
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-service-methods", setRoleMethod, "func (s *Service) SetRole(");
|
|
122
|
+
fs_extra_1.default.writeFileSync(serviceGoPath, content);
|
|
123
|
+
}
|
|
124
|
+
// patchUserServiceTestForRbac keeps service_test.go's single NewService call
|
|
125
|
+
// site (newTestService) compiling once patchUserServiceForRbac adds the
|
|
126
|
+
// roleChecker param above — same drift risk the handoff notes called out for
|
|
127
|
+
// middleware.NewAuthz's call sites, just for this signature instead. The fake
|
|
128
|
+
// itself is added here too, not pre-declared unconditionally in the
|
|
129
|
+
// template: golangci-lint's unused check flags an unreferenced type+method
|
|
130
|
+
// pair in the auth-only (pre-rbac) state, since nothing there yet implements
|
|
131
|
+
// or needs a roleChecker.
|
|
132
|
+
function patchUserServiceTestForRbac(serviceTestGoPath) {
|
|
133
|
+
let content = fs_extra_1.default.readFileSync(serviceTestGoPath, "utf8");
|
|
134
|
+
const fakeRoles = [
|
|
135
|
+
"// fakeRoles satisfies role's roleChecker interface structurally.",
|
|
136
|
+
"type fakeRoles struct{}",
|
|
137
|
+
"",
|
|
138
|
+
"func (fakeRoles) CodeExists(context.Context, string) (bool, error) { return true, nil }",
|
|
139
|
+
].join("\n");
|
|
140
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-service-test-types", fakeRoles, "type fakeRoles struct{}");
|
|
141
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-service-test-args", "fakeRoles{},", "fakeRoles{},");
|
|
142
|
+
fs_extra_1.default.writeFileSync(serviceTestGoPath, content);
|
|
143
|
+
}
|
|
144
|
+
// patchUserDTOForRbac adds the request DTO for PATCH /users/:id/set-role and
|
|
145
|
+
// surfaces Role on the /me response — both gated behind RBAC since
|
|
146
|
+
// model.User has no Role field at all without it.
|
|
147
|
+
function patchUserDTOForRbac(dtoGoPath) {
|
|
148
|
+
let content = fs_extra_1.default.readFileSync(dtoGoPath, "utf8");
|
|
149
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-me-fields", 'Role string `json:"role"`', "Role string");
|
|
150
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-me-values", "Role: u.Role,", "Role: u.Role,");
|
|
151
|
+
const setRoleInput = ["type setRoleInput struct {", '\tRole string `json:"role" binding:"required"`', "}"].join("\n");
|
|
152
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-dto", setRoleInput, "type setRoleInput struct");
|
|
153
|
+
fs_extra_1.default.writeFileSync(dtoGoPath, content);
|
|
154
|
+
}
|
|
155
|
+
// patchUserHandlerForRbac wires an *middleware.Authz into Handler and adds
|
|
156
|
+
// the admin routes this PR ships: listing/viewing other users (PermUserRead)
|
|
157
|
+
// and changing a user's role (PermUserManageRole). Suspending a user is a
|
|
158
|
+
// separate concern, not RBAC's — see the role domain's own /roles,
|
|
159
|
+
// /permissions for the actual role/permission management API.
|
|
160
|
+
function patchUserHandlerForRbac(handlerGoPath, goModule) {
|
|
161
|
+
let content = fs_extra_1.default.readFileSync(handlerGoPath, "utf8");
|
|
162
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-handler-consts", 'const PermUserManageRole = "user:manage-role"', "PermUserManageRole");
|
|
163
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-handler-consts", 'const PermUserRead = "user:read"', "PermUserRead");
|
|
164
|
+
const paginationImport = `"${goModule}/internal/shared/pagination"`;
|
|
165
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-handler-imports", paginationImport, "internal/shared/pagination");
|
|
166
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-handler-fields", "authz *middleware.Authz", "authz *middleware.Authz");
|
|
167
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-handler-params", "authz *middleware.Authz,", "authz *middleware.Authz,");
|
|
168
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-handler-init", "authz: authz,", "authz: authz,");
|
|
169
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-routes", 'usersGroup.GET("", h.authz.Require(PermUserRead), h.adminListUsers)', 'usersGroup.GET("", h.authz.Require(PermUserRead)');
|
|
170
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-routes", 'usersGroup.GET("/:id", h.authz.Require(PermUserRead), h.adminGetUser)', 'usersGroup.GET("/:id", h.authz.Require(PermUserRead)');
|
|
171
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-routes", 'usersGroup.PATCH("/:id/set-role", h.authz.Require(PermUserManageRole), h.setRole)', 'usersGroup.PATCH("/:id/set-role"');
|
|
172
|
+
const adminListHandler = [
|
|
173
|
+
"func (h *Handler) adminListUsers(c *gin.Context) {",
|
|
174
|
+
"\tp := pagination.Parse(c)",
|
|
175
|
+
"\titems, err := h.svc.List(c.Request.Context(), p.Limit, p.Offset)",
|
|
176
|
+
"\tif err != nil {",
|
|
177
|
+
"\t\tc.Error(err)",
|
|
178
|
+
"\t\treturn",
|
|
179
|
+
"\t}",
|
|
180
|
+
"\tout := make([]meResponse, len(items))",
|
|
181
|
+
"\tfor i := range items {",
|
|
182
|
+
"\t\tout[i] = toMeResponse(&items[i])",
|
|
183
|
+
"\t}",
|
|
184
|
+
"\tc.JSON(http.StatusOK, p.Response(out))",
|
|
185
|
+
"}",
|
|
186
|
+
].join("\n");
|
|
187
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-handler-funcs", adminListHandler, "func (h *Handler) adminListUsers(");
|
|
188
|
+
const adminGetHandler = [
|
|
189
|
+
"func (h *Handler) adminGetUser(c *gin.Context) {",
|
|
190
|
+
"\tid, ok := httpx.ParseID(c)",
|
|
191
|
+
"\tif !ok {",
|
|
192
|
+
"\t\treturn",
|
|
193
|
+
"\t}",
|
|
194
|
+
"\tu, err := h.svc.Get(c.Request.Context(), id)",
|
|
195
|
+
"\tif err != nil {",
|
|
196
|
+
"\t\tc.Error(err)",
|
|
197
|
+
"\t\treturn",
|
|
198
|
+
"\t}",
|
|
199
|
+
"\tc.JSON(http.StatusOK, toMeResponse(u))",
|
|
200
|
+
"}",
|
|
201
|
+
].join("\n");
|
|
202
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-handler-funcs", adminGetHandler, "func (h *Handler) adminGetUser(");
|
|
203
|
+
const setRoleHandler = [
|
|
204
|
+
"func (h *Handler) setRole(c *gin.Context) {",
|
|
205
|
+
"\tid, ok := httpx.ParseID(c)",
|
|
206
|
+
"\tif !ok {",
|
|
207
|
+
"\t\treturn",
|
|
208
|
+
"\t}",
|
|
209
|
+
"\tvar in setRoleInput",
|
|
210
|
+
"\tif err := c.ShouldBindJSON(&in); err != nil {",
|
|
211
|
+
"\t\tc.Error(httpx.BindErr(err))",
|
|
212
|
+
"\t\treturn",
|
|
213
|
+
"\t}",
|
|
214
|
+
"\tu, err := h.svc.SetRole(c.Request.Context(), id, in.Role)",
|
|
215
|
+
"\tif err != nil {",
|
|
216
|
+
"\t\tc.Error(err)",
|
|
217
|
+
"\t\treturn",
|
|
218
|
+
"\t}",
|
|
219
|
+
"\tc.JSON(http.StatusOK, toMeResponse(u))",
|
|
220
|
+
"}",
|
|
221
|
+
].join("\n");
|
|
222
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-handler-funcs", setRoleHandler, "func (h *Handler) setRole(");
|
|
223
|
+
fs_extra_1.default.writeFileSync(handlerGoPath, content);
|
|
224
|
+
}
|
|
225
|
+
function patchUserErrorsForRbac(errorsGoPath) {
|
|
226
|
+
let content = fs_extra_1.default.readFileSync(errorsGoPath, "utf8");
|
|
227
|
+
const errFn = ["func errUnknownRole() *apperror.AppError {", '\treturn apperror.New(http.StatusUnprocessableEntity, "USER_UNKNOWN_ROLE", "unknown role code")', "}"].join("\n");
|
|
228
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:user-errors", errFn, "func errUnknownRole(");
|
|
229
|
+
fs_extra_1.default.writeFileSync(errorsGoPath, content);
|
|
230
|
+
}
|
|
231
|
+
// patchMainGoForRbac wires the role domain into cmd/api: its import, its
|
|
232
|
+
// three models in the AutoMigrate call, and — the one place a plain insert
|
|
233
|
+
// isn't enough — REPLACES the `add auth` PR's user.NewHandler(...) call with
|
|
234
|
+
// a version that also builds roleSvc/authz and passes them through, plus
|
|
235
|
+
// registers role's own routes right after it.
|
|
236
|
+
function patchMainGoForRbac(mainGoPath, goModule) {
|
|
237
|
+
let content = fs_extra_1.default.readFileSync(mainGoPath, "utf8");
|
|
238
|
+
const importLine = `"${goModule}/internal/app/role"`;
|
|
239
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, importLine, importLine);
|
|
240
|
+
const modelImportLine = `rolemodel "${goModule}/internal/app/role/model"`;
|
|
241
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, modelImportLine, modelImportLine);
|
|
242
|
+
const migrateLines = ["&rolemodel.Role{},", "&rolemodel.Permission{},", "&rolemodel.RolePermission{},"];
|
|
243
|
+
for (const line of migrateLines) {
|
|
244
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, MODEL_MARKER, line, line);
|
|
245
|
+
}
|
|
246
|
+
const oldRouteLine = "user.NewHandler(user.NewService(user.NewRepository(db), user.NewRedisTokenStore(rdb), mail.NewAsyncClient(q), cfg), cfg.JWTSecret, cfg.JWTRefreshTTL, cfg.CookieSecure, rdb).Register(api)";
|
|
247
|
+
if (content.includes(oldRouteLine)) {
|
|
248
|
+
const newRouteBlock = [
|
|
249
|
+
"roleSvc := role.NewService(role.NewRepository(db))",
|
|
250
|
+
"authz := middleware.NewAuthz(roleSvc.PermissionsOf, cfg.AuthzCacheTTL)",
|
|
251
|
+
"user.NewHandler(user.NewService(user.NewRepository(db), user.NewRedisTokenStore(rdb), mail.NewAsyncClient(q), cfg, roleSvc), cfg.JWTSecret, cfg.JWTRefreshTTL, cfg.CookieSecure, rdb, authz).Register(api)",
|
|
252
|
+
"role.NewHandler(roleSvc, cfg.JWTSecret, authz).Register(api)",
|
|
253
|
+
].join("\n");
|
|
254
|
+
content = content.replace(oldRouteLine, newRouteBlock);
|
|
255
|
+
}
|
|
256
|
+
fs_extra_1.default.writeFileSync(mainGoPath, content);
|
|
257
|
+
}
|
|
258
|
+
// patchCmdSeedForRbac makes the seeded admin actually an admin: wires a
|
|
259
|
+
// role.Service into cmd/seed's own user.Service, then calls SetRole right
|
|
260
|
+
// after EnsureUser creates/finds the SEED_ADMIN_EMAIL user (fixtures stay on
|
|
261
|
+
// the 'staff' DB default — only the operator-specified admin gets promoted).
|
|
262
|
+
function patchCmdSeedForRbac(seedMainGoPath, goModule) {
|
|
263
|
+
let content = fs_extra_1.default.readFileSync(seedMainGoPath, "utf8");
|
|
264
|
+
const importLine = `"${goModule}/internal/app/role"`;
|
|
265
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:seed-imports", importLine, importLine);
|
|
266
|
+
const roleSvcLine = "roleSvc := role.NewService(role.NewRepository(db))";
|
|
267
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:seed-services", roleSvcLine, roleSvcLine);
|
|
268
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:seed-user-service-args", "roleSvc,", "roleSvc,");
|
|
269
|
+
const setRoleCall = [
|
|
270
|
+
'if _, err := svc.SetRole(ctx, u.ID, "admin"); err != nil {',
|
|
271
|
+
'\tlogger.Error("promote seed admin to admin role", "error", err)',
|
|
272
|
+
"\tos.Exit(1)",
|
|
273
|
+
"}",
|
|
274
|
+
].join("\n");
|
|
275
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, "// go-scaffold:seed-admin-role", setRoleCall, "promote seed admin to admin role");
|
|
276
|
+
fs_extra_1.default.writeFileSync(seedMainGoPath, content);
|
|
277
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
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.cliVersion = cliVersion;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
10
|
+
// cliVersion reads this CLI's own version out of its package.json — one source
|
|
11
|
+
// of truth for `--version`, for the stamp written into a new project's
|
|
12
|
+
// go-scaffold.config.json, and for the drift diagnostic that compares the two.
|
|
13
|
+
// Same two-candidate lookup as getTemplatesRoot (dist/ when installed, src/
|
|
14
|
+
// when running from a checkout).
|
|
15
|
+
function cliVersion() {
|
|
16
|
+
const candidates = [
|
|
17
|
+
path_1.default.join(__dirname, "..", "..", "package.json"),
|
|
18
|
+
path_1.default.join(__dirname, "..", "..", "..", "package.json"),
|
|
19
|
+
];
|
|
20
|
+
const resolved = candidates.find((candidate) => fs_1.default.existsSync(candidate));
|
|
21
|
+
if (!resolved)
|
|
22
|
+
throw new Error("unable to locate package.json");
|
|
23
|
+
return fs_extra_1.default.readJsonSync(resolved).version;
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nakedev/go-scaffold",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Scaffold Gin + GORM + Postgres Go backend projects with a consistent domain-module standard",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/NakePranob/go-scaffold.git"
|
|
8
8
|
},
|
|
9
9
|
"bin": {
|
|
10
|
-
"go-scaffold": "
|
|
10
|
+
"go-scaffold": "bin/go-scaffold.js"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"dist",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"flag"
|
|
6
|
+
"log/slog"
|
|
7
|
+
"os"
|
|
8
|
+
|
|
9
|
+
"{{goModule}}/internal/app/user"
|
|
10
|
+
"{{goModule}}/internal/platform/database"
|
|
11
|
+
"{{goModule}}/internal/shared/config"
|
|
12
|
+
// go-scaffold:seed-imports
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// cmd/seed is a one-off operational task, like `migrate` — not part of the
|
|
16
|
+
// API server's runtime, so it's a separate binary rather than a flag on
|
|
17
|
+
// cmd/api. tokens/mailer are nil: EnsureUser never touches them (no login
|
|
18
|
+
// or email flow runs during seeding).
|
|
19
|
+
func main() {
|
|
20
|
+
withFixtures := flag.Bool("fixtures", false, "also seed sample dev users — do not use against a real environment")
|
|
21
|
+
flag.Parse()
|
|
22
|
+
|
|
23
|
+
cfg := config.Load()
|
|
24
|
+
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
|
25
|
+
|
|
26
|
+
db, err := database.Open(cfg)
|
|
27
|
+
if err != nil {
|
|
28
|
+
logger.Error("open db", "error", err)
|
|
29
|
+
os.Exit(1)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// go-scaffold:seed-services
|
|
33
|
+
svc := user.NewService(
|
|
34
|
+
user.NewRepository(db), nil, nil, cfg,
|
|
35
|
+
// go-scaffold:seed-user-service-args
|
|
36
|
+
)
|
|
37
|
+
ctx := context.Background()
|
|
38
|
+
|
|
39
|
+
if email := os.Getenv("SEED_ADMIN_EMAIL"); email != "" {
|
|
40
|
+
password := os.Getenv("SEED_ADMIN_PASSWORD")
|
|
41
|
+
if password == "" {
|
|
42
|
+
logger.Error("SEED_ADMIN_PASSWORD is required when SEED_ADMIN_EMAIL is set")
|
|
43
|
+
os.Exit(1)
|
|
44
|
+
}
|
|
45
|
+
name := os.Getenv("SEED_ADMIN_NAME")
|
|
46
|
+
if name == "" {
|
|
47
|
+
name = "Admin"
|
|
48
|
+
}
|
|
49
|
+
u, err := svc.EnsureUser(ctx, email, password, name)
|
|
50
|
+
if err != nil {
|
|
51
|
+
logger.Error("seed admin user", "error", err)
|
|
52
|
+
os.Exit(1)
|
|
53
|
+
}
|
|
54
|
+
logger.Info("admin user ready", "email", u.Email, "id", u.ID)
|
|
55
|
+
// go-scaffold:seed-admin-role
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if *withFixtures {
|
|
59
|
+
seedFixtures(ctx, svc, logger)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
func seedFixtures(ctx context.Context, svc *user.Service, logger *slog.Logger) {
|
|
64
|
+
fixtures := []struct{ email, name string }{
|
|
65
|
+
{email: "dev.one@example.com", name: "Dev One"},
|
|
66
|
+
{email: "dev.two@example.com", name: "Dev Two"},
|
|
67
|
+
}
|
|
68
|
+
for _, f := range fixtures {
|
|
69
|
+
u, err := svc.EnsureUser(ctx, f.email, "password123", f.name)
|
|
70
|
+
if err != nil {
|
|
71
|
+
logger.Error("seed fixture user", "email", f.email, "error", err)
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
logger.Info("fixture user ready", "email", u.Email, "id", u.ID)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
post:
|
|
2
|
+
summary: Request a password reset link
|
|
3
|
+
description: Always returns the same message whether or not the email exists, to avoid leaking which emails are registered.
|
|
4
|
+
operationId: forgotPassword
|
|
5
|
+
tags: [auth]
|
|
6
|
+
security: []
|
|
7
|
+
requestBody:
|
|
8
|
+
required: true
|
|
9
|
+
content:
|
|
10
|
+
application/json:
|
|
11
|
+
schema: { $ref: './schemas.yaml#/ForgotPasswordInput' }
|
|
12
|
+
responses:
|
|
13
|
+
"200":
|
|
14
|
+
description: ok
|
|
15
|
+
content:
|
|
16
|
+
application/json:
|
|
17
|
+
schema: { $ref: './schemas.yaml#/MessageResponse' }
|
|
18
|
+
"400": { $ref: '../common/responses.yaml#/ValidationError' }
|
|
19
|
+
"429": { $ref: '../common/responses.yaml#/TooManyRequestsError' }
|
|
@@ -0,0 +1,22 @@
|
|
|
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' }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
post:
|
|
2
|
+
summary: Log in with email and password
|
|
3
|
+
operationId: loginUser
|
|
4
|
+
tags: [auth]
|
|
5
|
+
security: []
|
|
6
|
+
requestBody:
|
|
7
|
+
required: true
|
|
8
|
+
content:
|
|
9
|
+
application/json:
|
|
10
|
+
schema: { $ref: './schemas.yaml#/LoginInput' }
|
|
11
|
+
responses:
|
|
12
|
+
"200":
|
|
13
|
+
description: ok, refresh_token set as an httpOnly cookie
|
|
14
|
+
content:
|
|
15
|
+
application/json:
|
|
16
|
+
schema: { $ref: './schemas.yaml#/AuthResponse' }
|
|
17
|
+
"400": { $ref: '../common/responses.yaml#/ValidationError' }
|
|
18
|
+
"401": { $ref: '../common/responses.yaml#/UnauthorizedError' }
|
|
19
|
+
"429": { $ref: '../common/responses.yaml#/TooManyRequestsError' }
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
post:
|
|
2
|
+
summary: Log out the current session
|
|
3
|
+
description: Revokes the refresh_token cookie's session and clears the cookie. Always succeeds, even with no cookie.
|
|
4
|
+
operationId: logoutUser
|
|
5
|
+
tags: [auth]
|
|
6
|
+
security: []
|
|
7
|
+
responses:
|
|
8
|
+
"204": { description: logged out }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
post:
|
|
2
|
+
summary: Rotate the refresh token cookie for a new access token
|
|
3
|
+
description: >-
|
|
4
|
+
Reads refresh_token from the httpOnly cookie (no request body). Reusing an
|
|
5
|
+
already-rotated-out token revokes the whole session family.
|
|
6
|
+
operationId: refreshToken
|
|
7
|
+
tags: [auth]
|
|
8
|
+
security: []
|
|
9
|
+
responses:
|
|
10
|
+
"200":
|
|
11
|
+
description: ok, a new refresh_token cookie is set
|
|
12
|
+
content:
|
|
13
|
+
application/json:
|
|
14
|
+
schema: { $ref: './schemas.yaml#/AuthResponse' }
|
|
15
|
+
"401": { $ref: '../common/responses.yaml#/UnauthorizedError' }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
post:
|
|
2
|
+
summary: Register a new account
|
|
3
|
+
operationId: registerUser
|
|
4
|
+
tags: [auth]
|
|
5
|
+
security: []
|
|
6
|
+
requestBody:
|
|
7
|
+
required: true
|
|
8
|
+
content:
|
|
9
|
+
application/json:
|
|
10
|
+
schema: { $ref: './schemas.yaml#/RegisterInput' }
|
|
11
|
+
responses:
|
|
12
|
+
"201":
|
|
13
|
+
description: created, refresh_token set as an httpOnly cookie
|
|
14
|
+
content:
|
|
15
|
+
application/json:
|
|
16
|
+
schema: { $ref: './schemas.yaml#/AuthResponse' }
|
|
17
|
+
"400": { $ref: '../common/responses.yaml#/ValidationError' }
|
|
18
|
+
"409": { $ref: '../common/responses.yaml#/ConflictError' }
|
|
19
|
+
"429": { $ref: '../common/responses.yaml#/TooManyRequestsError' }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
post:
|
|
2
|
+
summary: Reset a password using a reset token
|
|
3
|
+
description: The token is one-time use (consumed on success) and also revokes every existing refresh token for the account.
|
|
4
|
+
operationId: resetPassword
|
|
5
|
+
tags: [auth]
|
|
6
|
+
security: []
|
|
7
|
+
requestBody:
|
|
8
|
+
required: true
|
|
9
|
+
content:
|
|
10
|
+
application/json:
|
|
11
|
+
schema: { $ref: './schemas.yaml#/ResetPasswordInput' }
|
|
12
|
+
responses:
|
|
13
|
+
"204": { description: password reset }
|
|
14
|
+
"400": { $ref: '../common/responses.yaml#/ValidationError' }
|
|
15
|
+
"401": { $ref: '../common/responses.yaml#/UnauthorizedError' }
|
|
16
|
+
"429": { $ref: '../common/responses.yaml#/TooManyRequestsError' }
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
RegisterInput:
|
|
2
|
+
type: object
|
|
3
|
+
required: [email, password, name]
|
|
4
|
+
properties:
|
|
5
|
+
email: { type: string, format: email }
|
|
6
|
+
password: { type: string, format: password, minLength: 8 }
|
|
7
|
+
name: { type: string }
|
|
8
|
+
|
|
9
|
+
LoginInput:
|
|
10
|
+
type: object
|
|
11
|
+
required: [email, password]
|
|
12
|
+
properties:
|
|
13
|
+
email: { type: string, format: email }
|
|
14
|
+
password: { type: string, format: password }
|
|
15
|
+
|
|
16
|
+
ForgotPasswordInput:
|
|
17
|
+
type: object
|
|
18
|
+
required: [email]
|
|
19
|
+
properties:
|
|
20
|
+
email: { type: string, format: email }
|
|
21
|
+
|
|
22
|
+
ResetPasswordInput:
|
|
23
|
+
type: object
|
|
24
|
+
required: [token, new_password]
|
|
25
|
+
properties:
|
|
26
|
+
token: { type: string }
|
|
27
|
+
new_password: { type: string, format: password, minLength: 8 }
|
|
28
|
+
|
|
29
|
+
VerifyEmailInput:
|
|
30
|
+
type: object
|
|
31
|
+
required: [token]
|
|
32
|
+
properties:
|
|
33
|
+
token: { type: string }
|
|
34
|
+
|
|
35
|
+
# 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.
|
|
37
|
+
AuthResponse:
|
|
38
|
+
type: object
|
|
39
|
+
properties:
|
|
40
|
+
access_token: { type: string }
|
|
41
|
+
token_type: { type: string, example: Bearer }
|
|
42
|
+
expires_in: { type: integer, description: seconds until access_token expires }
|
|
43
|
+
|
|
44
|
+
MessageResponse:
|
|
45
|
+
type: object
|
|
46
|
+
properties:
|
|
47
|
+
message: { type: string }
|
|
48
|
+
|
|
49
|
+
MeResponse:
|
|
50
|
+
type: object
|
|
51
|
+
properties:
|
|
52
|
+
id: { type: string, format: uuid }
|
|
53
|
+
email: { type: string, format: email }
|
|
54
|
+
name: { type: string }
|
|
55
|
+
avatar_url: { type: string }
|
|
56
|
+
email_verified: { type: boolean }
|
|
57
|
+
# go-scaffold:me-response-fields
|
|
58
|
+
created_at: { type: string, format: date-time }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
post:
|
|
2
|
+
summary: Log out every session for the current user
|
|
3
|
+
description: Revokes every refresh token for the caller, not just the current one, and clears this request's cookie too.
|
|
4
|
+
operationId: logoutAllSessions
|
|
5
|
+
tags: [users]
|
|
6
|
+
security: [{ bearerAuth: [] }]
|
|
7
|
+
responses:
|
|
8
|
+
"204": { description: all sessions logged out }
|
|
9
|
+
"401": { $ref: '../common/responses.yaml#/UnauthorizedError' }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
post:
|
|
2
|
+
summary: Resend the email verification link
|
|
3
|
+
operationId: resendVerification
|
|
4
|
+
tags: [users]
|
|
5
|
+
security: [{ bearerAuth: [] }]
|
|
6
|
+
responses:
|
|
7
|
+
"204": { description: verification email sent }
|
|
8
|
+
"401": { $ref: '../common/responses.yaml#/UnauthorizedError' }
|
|
9
|
+
"409": { $ref: '../common/responses.yaml#/ConflictError' }
|
|
10
|
+
"429": { $ref: '../common/responses.yaml#/TooManyRequestsError' }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
get:
|
|
2
|
+
summary: Get the current user
|
|
3
|
+
operationId: getMe
|
|
4
|
+
tags: [users]
|
|
5
|
+
security: [{ bearerAuth: [] }]
|
|
6
|
+
responses:
|
|
7
|
+
"200":
|
|
8
|
+
description: ok
|
|
9
|
+
content:
|
|
10
|
+
application/json:
|
|
11
|
+
schema: { $ref: './schemas.yaml#/MeResponse' }
|
|
12
|
+
"401": { $ref: '../common/responses.yaml#/UnauthorizedError' }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
post:
|
|
2
|
+
summary: Verify an email address using a verification token
|
|
3
|
+
description: The token is one-time use (GETDEL), reusing an already-consumed token fails.
|
|
4
|
+
operationId: verifyEmail
|
|
5
|
+
tags: [auth]
|
|
6
|
+
security: []
|
|
7
|
+
requestBody:
|
|
8
|
+
required: true
|
|
9
|
+
content:
|
|
10
|
+
application/json:
|
|
11
|
+
schema: { $ref: './schemas.yaml#/VerifyEmailInput' }
|
|
12
|
+
responses:
|
|
13
|
+
"204": { description: email verified }
|
|
14
|
+
"400": { $ref: '../common/responses.yaml#/ValidationError' }
|
|
15
|
+
"401": { $ref: '../common/responses.yaml#/UnauthorizedError' }
|
|
16
|
+
"429": { $ref: '../common/responses.yaml#/TooManyRequestsError' }
|