@nakedev/go-scaffold 0.4.3 → 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 +567 -513
- package/dist/commands/auth.js +12 -1
- package/dist/commands/check.js +281 -0
- package/dist/commands/create.js +2 -1
- package/dist/commands/generate.js +5 -2
- package/dist/commands/method.js +66 -70
- package/dist/commands/observability.js +4 -53
- package/dist/commands/rbac.js +19 -8
- package/dist/commands/undo.js +6 -3
- package/dist/commands/worker.js +14 -4
- package/dist/index.js +13 -1
- package/dist/templates/auth-manifest.js +46 -45
- package/dist/templates/create-manifest.js +4 -0
- package/dist/templates/module-manifest.js +82 -60
- package/dist/templates/rbac-manifest.js +15 -11
- package/dist/templates/worker-manifest.js +4 -1
- package/dist/types.js +2 -0
- package/dist/utils/auth-patcher.js +22 -22
- package/dist/utils/config.js +24 -5
- package/dist/utils/docs-patcher.js +68 -0
- package/dist/utils/hexagonal-method-patcher.js +334 -0
- package/dist/utils/main-patcher.js +3 -3
- package/dist/utils/module-location.js +17 -11
- package/dist/utils/platform-patcher.js +27 -0
- package/dist/utils/rbac-patcher.js +73 -216
- package/package.json +1 -1
- package/templates/add/auth/cmd/seed/main.go.hbs +2 -0
- package/templates/add/auth/internal/app/user/{browser_policy.go.hbs → adapters/inbound/http/browser_policy.go.hbs} +3 -3
- package/templates/add/auth/internal/app/user/adapters/inbound/http/dto.go.hbs +159 -0
- package/templates/add/auth/internal/app/user/{handler.go.hbs → adapters/inbound/http/handler.go.hbs} +103 -8
- package/templates/add/auth/internal/app/user/{handler_local.go.hbs → adapters/inbound/http/handler_local.go.hbs} +8 -7
- package/templates/add/auth/internal/app/user/{handler_mfa.go.hbs → adapters/inbound/http/handler_mfa.go.hbs} +6 -6
- package/templates/add/auth/internal/app/user/{handler_oauth.go.hbs → adapters/inbound/http/handler_oauth.go.hbs} +17 -17
- package/templates/add/auth/internal/app/user/{handler_recovery.go.hbs → adapters/inbound/http/handler_recovery.go.hbs} +4 -4
- package/templates/add/auth/internal/app/user/{handler_test.go.hbs → adapters/inbound/http/handler_test.go.hbs} +52 -31
- package/templates/add/auth/internal/app/user/{handler_user.go.hbs → adapters/inbound/http/handler_user.go.hbs} +4 -4
- package/templates/add/auth/internal/app/user/{session_cookie.go.hbs → adapters/inbound/http/session_cookie.go.hbs} +5 -3
- 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/{mfa_store.go.hbs → adapters/outbound/postgres/mfa_store.go.hbs} +22 -20
- package/templates/add/auth/internal/app/user/{mfa_store_test.go.hbs → adapters/outbound/postgres/mfa_store_test.go.hbs} +7 -7
- 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/{tokenstore_pg.go.hbs → adapters/outbound/postgres/tokenstore_pg.go.hbs} +36 -32
- package/templates/add/auth/internal/app/user/{tokenstore_pg_test.go.hbs → adapters/outbound/postgres/tokenstore_pg_test.go.hbs} +10 -3
- package/templates/add/auth/internal/app/user/{tokenstore_recovery.go.hbs → adapters/outbound/postgres/tokenstore_recovery.go.hbs} +29 -3
- package/templates/add/auth/internal/app/user/{tokenstore_redis.go.hbs → adapters/outbound/redis/tokenstore.go.hbs} +43 -36
- package/templates/add/auth/internal/app/user/{tokenstore_redis_test.go.hbs → adapters/outbound/redis/tokenstore_test.go.hbs} +14 -3
- 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/{external_login.go.hbs → application/external_login.go.hbs} +30 -40
- package/templates/add/auth/internal/app/user/{jwt.go.hbs → application/jwt.go.hbs} +6 -3
- package/templates/add/auth/internal/app/user/{local_auth.go.hbs → application/local_auth.go.hbs} +17 -19
- package/templates/add/auth/internal/app/user/{mfa_service.go.hbs → application/mfa_service.go.hbs} +27 -28
- package/templates/add/auth/internal/app/user/{mfa_service_test.go.hbs → application/mfa_service_test.go.hbs} +12 -11
- package/templates/add/auth/internal/app/user/application/oauth.go.hbs +3 -3
- package/templates/add/auth/internal/app/user/{provider_test.go.hbs → application/provider_test.go.hbs} +55 -56
- package/templates/add/auth/internal/app/user/application/recovery.go.hbs +16 -47
- package/templates/add/auth/internal/app/user/{recovery_service.go.hbs → application/recovery_service.go.hbs} +13 -15
- package/templates/add/auth/internal/app/user/application/service.go.hbs +145 -0
- package/templates/add/auth/internal/app/user/{service_test.go.hbs → application/service_test.go.hbs} +94 -75
- package/templates/add/auth/internal/app/user/{sessions.go.hbs → application/sessions.go.hbs} +14 -14
- package/templates/add/auth/internal/app/user/application/tokenstore_ports.go.hbs +14 -0
- package/templates/add/auth/internal/app/user/{user_query.go.hbs → application/user_query.go.hbs} +16 -16
- package/templates/add/auth/internal/app/user/composition.go.hbs +89 -86
- 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/migrations/create_auth_tokens.up.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_users.up.sql.hbs +2 -1
- 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 +23 -10
- 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 +54 -36
- package/templates/create/base/.golangci.yml.hbs +2 -2
- package/templates/create/base/AGENTS.md.hbs +55 -31
- package/templates/create/base/README.md.hbs +77 -22
- package/templates/create/base/cmd/api/wiring.go.hbs +3 -2
- package/templates/create/base/internal/composition/doc.go.hbs +7 -0
- package/templates/create/features/docs/architecture.md.hbs +64 -26
- package/templates/create/features/docs/patterns.md.hbs +117 -90
- package/templates/create/features/docs/techstack.md.hbs +17 -2
- 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 -537
- package/templates/add/auth/internal/app/user/contracts.go.hbs +0 -88
- package/templates/add/auth/internal/app/user/dto.go.hbs +0 -134
- package/templates/add/auth/internal/app/user/errors.go.hbs +0 -68
- package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +0 -45
- package/templates/add/auth/internal/app/user/model/identity.go.hbs +0 -32
- package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +0 -26
- package/templates/add/auth/internal/app/user/model/mfa_challenge.go.hbs +0 -17
- package/templates/add/auth/internal/app/user/model/mfa_enrollment.go.hbs +0 -20
- package/templates/add/auth/internal/app/user/model/mfa_recovery_code.go.hbs +0 -17
- package/templates/add/auth/internal/app/user/model/user.go.hbs +0 -31
- package/templates/add/auth/internal/app/user/repository.go.hbs +0 -139
- package/templates/add/auth/internal/app/user/service.go.hbs +0 -135
- package/templates/add/auth/internal/app/user/tokenstore.go.hbs +0 -58
- 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/commands.go.hbs +0 -95
- package/templates/generate/module/composition.go.hbs +0 -23
- package/templates/generate/module/cqrs_test.go.hbs +0 -7
- 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 -179
- package/templates/generate/module/handler_test.go.hbs +0 -174
- package/templates/generate/module/minimal/commands.go.hbs +0 -34
- package/templates/generate/module/minimal/dto.go.hbs +0 -28
- package/templates/generate/module/minimal/handler.go.hbs +0 -82
- package/templates/generate/module/minimal/handler_test.go.hbs +0 -10
- package/templates/generate/module/minimal/queries.go.hbs +0 -45
- package/templates/generate/module/minimal/service.go.hbs +0 -71
- 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/queries.go.hbs +0 -62
- package/templates/generate/module/repository.go.hbs +0 -103
- package/templates/generate/module/service.go.hbs +0 -164
- package/templates/generate/module/service_test.go.hbs +0 -161
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
package user
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"context"
|
|
5
|
-
"time"
|
|
6
|
-
|
|
7
|
-
"{{goModule}}/internal/app/user/application"
|
|
8
|
-
|
|
9
|
-
"github.com/google/uuid"
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
// refreshTokenStore is the session surface needed by the refresh/logout use
|
|
13
|
-
// cases. Keeping this port narrow means a store for OAuth state or recovery
|
|
14
|
-
// tokens cannot accidentally become a dependency of session logic.
|
|
15
|
-
type RefreshTokenStore interface {
|
|
16
|
-
SetRefreshToken(ctx context.Context, tokenHash string, token refreshTokenRecord) error
|
|
17
|
-
GetRefreshToken(ctx context.Context, tokenHash string) (uuid.UUID, bool, error)
|
|
18
|
-
ConsumeRefreshToken(ctx context.Context, tokenHash string) (refreshTokenRecord, bool, error)
|
|
19
|
-
DeleteRefreshToken(ctx context.Context, tokenHash string, userID uuid.UUID) error
|
|
20
|
-
RevokeAllRefreshTokens(ctx context.Context, userID uuid.UUID) error
|
|
21
|
-
IsRefreshTokenUsed(ctx context.Context, tokenHash string) (uuid.UUID, bool, error)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// oauthTransactionStore is the one-time authorization attempt surface. It is
|
|
25
|
-
// intentionally separate from refresh sessions: the two have different TTL,
|
|
26
|
-
// replay, and operational characteristics.
|
|
27
|
-
type OAuthTransactionStore interface {
|
|
28
|
-
SetLoginTransaction(ctx context.Context, stateHash string, transaction loginTransaction) error
|
|
29
|
-
ConsumeLoginTransaction(ctx context.Context, stateHash string) (loginTransaction, bool, error)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// recoveryTokenPort is the durable one-time-token surface. Recovery owns the
|
|
33
|
-
// transaction boundary that consumes a token and updates the user, while the
|
|
34
|
-
// outer service also needs the two write methods to issue links.
|
|
35
|
-
type RecoveryTokenStore interface {
|
|
36
|
-
application.RecoveryTokens
|
|
37
|
-
SetPasswordResetToken(ctx context.Context, tokenHash string, userID uuid.UUID, ttl time.Duration) error
|
|
38
|
-
SetEmailVerifyToken(ctx context.Context, tokenHash string, userID uuid.UUID, ttl time.Duration) error
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// refreshTokenRecord carries both the sliding inactivity expiry and the fixed
|
|
42
|
-
// absolute expiry. A rotation may move ExpiresAt forward, but it must never
|
|
43
|
-
// move AbsoluteExpiresAt forward.
|
|
44
|
-
type refreshTokenRecord struct {
|
|
45
|
-
UserID uuid.UUID `json:"user_id"`
|
|
46
|
-
ExpiresAt time.Time `json:"expires_at"`
|
|
47
|
-
AbsoluteExpiresAt time.Time `json:"absolute_expires_at"`
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// loginTransaction is the server-owned binding for a browser authorization
|
|
51
|
-
// attempt. State itself is only hashed before storage; the provider receives
|
|
52
|
-
// the original value so the frontend callback can compare it unchanged.
|
|
53
|
-
type loginTransaction struct {
|
|
54
|
-
Provider string
|
|
55
|
-
CodeChallenge string
|
|
56
|
-
Nonce string
|
|
57
|
-
ExpiresAt time.Time
|
|
58
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
package role
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"time"
|
|
5
|
-
|
|
6
|
-
"{{goModule}}/internal/app/role/model"
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
// response = the DTO sent out (kept separate from the model so a later DB column doesn't leak automatically)
|
|
10
|
-
type response struct {
|
|
11
|
-
Code string `json:"code"`
|
|
12
|
-
Name string `json:"name"`
|
|
13
|
-
IsSystem bool `json:"is_system"`
|
|
14
|
-
Permissions []string `json:"permissions"`
|
|
15
|
-
CreatedAt time.Time `json:"created_at"`
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
func toResponse(m *model.Role, perms []string) response {
|
|
19
|
-
return response{Code: m.Code, Name: m.Name, IsSystem: m.IsSystem, Permissions: perms, CreatedAt: m.CreatedAt}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
type CreateInput struct {
|
|
23
|
-
Code string `json:"code" binding:"required,max=20"`
|
|
24
|
-
Name string `json:"name" binding:"required"`
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// SetPermissionsInput.PermissionCodes is a *pointer* so that "field absent"
|
|
28
|
-
// and "field present but empty" stay different things. An empty list is a
|
|
29
|
-
// valid request — it revokes every permission the role had — but with a plain
|
|
30
|
-
// []string a body that simply misspells the key (say `{"permissions": [...]}`)
|
|
31
|
-
// binds to nil, reads as that same "revoke everything", and answers 200 having
|
|
32
|
-
// silently stripped the role. `required` on the pointer rejects the absent
|
|
33
|
-
// case with a 400 while still allowing an explicit [].
|
|
34
|
-
type SetPermissionsInput struct {
|
|
35
|
-
PermissionCodes *[]string `json:"permission_codes" binding:"required"`
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
type permissionResponse struct {
|
|
39
|
-
Code string `json:"code"`
|
|
40
|
-
Description string `json:"description"`
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
func toPermissionResponse(m *model.Permission) permissionResponse {
|
|
44
|
-
return permissionResponse{Code: m.Code, Description: m.Description}
|
|
45
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
package role
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"net/http"
|
|
5
|
-
|
|
6
|
-
"{{goModule}}/internal/shared/apperror"
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
// error catalog specific to role — codes carry more meaning than the generic apperror ones
|
|
10
|
-
// functions, not vars: the error middleware writes RequestID onto the returned pointer directly
|
|
11
|
-
// (a shared instance would race across concurrent requests).
|
|
12
|
-
|
|
13
|
-
func errNotFound() *apperror.AppError {
|
|
14
|
-
return apperror.New(http.StatusNotFound, "ROLE_NOT_FOUND", "role not found")
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
func errConflict() *apperror.AppError {
|
|
18
|
-
return apperror.New(http.StatusConflict, "ROLE_CONFLICT", "role code already exists")
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
func errHasReferences() *apperror.AppError {
|
|
22
|
-
return apperror.New(http.StatusConflict, "ROLE_HAS_REFERENCES", "role still has users assigned to it")
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
func errInvalidCode() *apperror.AppError {
|
|
26
|
-
return apperror.New(http.StatusUnprocessableEntity, "ROLE_INVALID_CODE", "code must start with a lowercase letter and contain only lowercase letters, digits, or underscores")
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
func errSystemRole() *apperror.AppError {
|
|
30
|
-
return apperror.New(http.StatusConflict, "ROLE_IS_SYSTEM", "staff/admin are system roles and cannot be deleted")
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
func errUnknownPermission() *apperror.AppError {
|
|
34
|
-
return apperror.New(http.StatusUnprocessableEntity, "ROLE_UNKNOWN_PERMISSION", "one or more permission codes do not exist")
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
func errLastRoleManager() *apperror.AppError {
|
|
38
|
-
return apperror.New(http.StatusConflict, "ROLE_LAST_MANAGER", "this is the only role that can manage roles/permissions — grant "+PermRoleManage+" to another role first")
|
|
39
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
package role
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"net/http"
|
|
5
|
-
|
|
6
|
-
"{{goModule}}/internal/shared/httpx"
|
|
7
|
-
"{{goModule}}/internal/shared/middleware"
|
|
8
|
-
"{{goModule}}/internal/shared/pagination"
|
|
9
|
-
|
|
10
|
-
"github.com/gin-gonic/gin"
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
// PermRoleManage gates every endpoint in this file — role/permission
|
|
14
|
-
// management is a single admin capability, not split per-action.
|
|
15
|
-
const PermRoleManage = "role:manage"
|
|
16
|
-
|
|
17
|
-
type Handler struct {
|
|
18
|
-
svc *Service
|
|
19
|
-
jwtSecret string
|
|
20
|
-
authz *middleware.Authz
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
func NewHandler(svc *Service, jwtSecret string, authz *middleware.Authz) *Handler {
|
|
24
|
-
return &Handler{svc: svc, jwtSecret: jwtSecret, authz: authz}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Register wires role's routes onto the router group (takes an IRouter so it can be nested under /v1)
|
|
28
|
-
func (h *Handler) Register(rg gin.IRouter) {
|
|
29
|
-
roles := rg.Group("/roles", middleware.RequireAuth(h.jwtSecret), h.authz.Require(PermRoleManage))
|
|
30
|
-
roles.GET("", h.list)
|
|
31
|
-
roles.POST("", h.create)
|
|
32
|
-
roles.PATCH("/:code/permissions", h.setPermissions)
|
|
33
|
-
roles.DELETE("/:code", h.delete)
|
|
34
|
-
|
|
35
|
-
perms := rg.Group("/permissions", middleware.RequireAuth(h.jwtSecret), h.authz.Require(PermRoleManage))
|
|
36
|
-
perms.GET("", h.listPermissions)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
func (h *Handler) list(c *gin.Context) {
|
|
40
|
-
p := pagination.Parse(c)
|
|
41
|
-
items, err := h.svc.List(c.Request.Context(), p.Limit, p.Offset)
|
|
42
|
-
if err != nil {
|
|
43
|
-
c.Error(err)
|
|
44
|
-
return
|
|
45
|
-
}
|
|
46
|
-
out := make([]response, len(items))
|
|
47
|
-
for i := range items {
|
|
48
|
-
out[i] = toResponse(&items[i].Role, items[i].Permissions)
|
|
49
|
-
}
|
|
50
|
-
c.JSON(http.StatusOK, p.Response(out))
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
func (h *Handler) create(c *gin.Context) {
|
|
54
|
-
var in CreateInput
|
|
55
|
-
if err := c.ShouldBindJSON(&in); err != nil {
|
|
56
|
-
c.Error(httpx.BindErr(err))
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
item, err := h.svc.Create(c.Request.Context(), in)
|
|
60
|
-
if err != nil {
|
|
61
|
-
c.Error(err)
|
|
62
|
-
return
|
|
63
|
-
}
|
|
64
|
-
c.JSON(http.StatusCreated, toResponse(&item.Role, item.Permissions))
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
func (h *Handler) setPermissions(c *gin.Context) {
|
|
68
|
-
var in SetPermissionsInput
|
|
69
|
-
if err := c.ShouldBindJSON(&in); err != nil {
|
|
70
|
-
c.Error(httpx.BindErr(err))
|
|
71
|
-
return
|
|
72
|
-
}
|
|
73
|
-
item, err := h.svc.SetPermissions(c.Request.Context(), c.Param("code"), *in.PermissionCodes)
|
|
74
|
-
if err != nil {
|
|
75
|
-
c.Error(err)
|
|
76
|
-
return
|
|
77
|
-
}
|
|
78
|
-
// the grant is useless until the cache that answers Require() knows about it
|
|
79
|
-
h.authz.Invalidate(item.Code)
|
|
80
|
-
c.JSON(http.StatusOK, toResponse(&item.Role, item.Permissions))
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
func (h *Handler) delete(c *gin.Context) {
|
|
84
|
-
if err := h.svc.Delete(c.Request.Context(), c.Param("code")); err != nil {
|
|
85
|
-
c.Error(err)
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
h.authz.Invalidate(c.Param("code"))
|
|
89
|
-
c.Status(http.StatusNoContent)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
func (h *Handler) listPermissions(c *gin.Context) {
|
|
93
|
-
p := pagination.Parse(c)
|
|
94
|
-
items, err := h.svc.ListPermissions(c.Request.Context(), p.Limit, p.Offset)
|
|
95
|
-
if err != nil {
|
|
96
|
-
c.Error(err)
|
|
97
|
-
return
|
|
98
|
-
}
|
|
99
|
-
out := make([]permissionResponse, len(items))
|
|
100
|
-
for i := range items {
|
|
101
|
-
out[i] = toPermissionResponse(&items[i])
|
|
102
|
-
}
|
|
103
|
-
c.JSON(http.StatusOK, p.Response(out))
|
|
104
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
package model
|
|
2
|
-
|
|
3
|
-
// Permission = an action middleware.Authz.Require checks for. Rows are
|
|
4
|
-
// seeded by migration, not admin-created — a permission with no matching
|
|
5
|
-
// Require() call in the code would do nothing.
|
|
6
|
-
type Permission struct {
|
|
7
|
-
Code string `json:"code" gorm:"type:varchar(50);primaryKey"`
|
|
8
|
-
Description string `json:"description" gorm:"not null"`
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// TableName — see Role.TableName; same schema, same reasoning.
|
|
12
|
-
func (Permission) TableName() string { return "role_svc.permissions" }
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
// Package model holds this domain's GORM tables — a folder (not a single
|
|
2
|
-
// file) so a domain with more than one table can add a file per table
|
|
3
|
-
// without cluttering the domain package's top level.
|
|
4
|
-
package model
|
|
5
|
-
|
|
6
|
-
import "time"
|
|
7
|
-
|
|
8
|
-
// Role = an assignable permission level, backed by a real table instead of a
|
|
9
|
-
// hardcoded enum — adding one is a row insert, not a migration + redeploy.
|
|
10
|
-
// Code is the primary key (not a generated UUID): users.role already stores
|
|
11
|
-
// this string, so using it as the join key needs zero data migration.
|
|
12
|
-
type Role struct {
|
|
13
|
-
Code string `json:"code" gorm:"type:varchar(20);primaryKey"`
|
|
14
|
-
Name string `json:"name" gorm:"not null"`
|
|
15
|
-
IsSystem bool `json:"is_system" gorm:"not null;default:false"`
|
|
16
|
-
CreatedAt time.Time `json:"created_at"`
|
|
17
|
-
UpdatedAt time.Time `json:"updated_at"`
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// TableName pins this to role_svc rather than GORM's default inflection —
|
|
21
|
-
// every domain gets its own schema, see docs/architect/patterns.md.
|
|
22
|
-
func (Role) TableName() string { return "role_svc.roles" }
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
package model
|
|
2
|
-
|
|
3
|
-
// RolePermission = the grant between Role and Permission — the one thing
|
|
4
|
-
// admins actually edit; Role and Permission rows themselves stay fixed.
|
|
5
|
-
type RolePermission struct {
|
|
6
|
-
RoleCode string `json:"role_code" gorm:"primaryKey;type:varchar(20)"`
|
|
7
|
-
PermissionCode string `json:"permission_code" gorm:"primaryKey;type:varchar(50)"`
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
// TableName — see Role.TableName; same schema, same reasoning.
|
|
11
|
-
func (RolePermission) TableName() string { return "role_svc.role_permissions" }
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
package role
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"context"
|
|
5
|
-
|
|
6
|
-
"{{goModule}}/internal/app/role/model"
|
|
7
|
-
"{{goModule}}/internal/shared/tx"
|
|
8
|
-
|
|
9
|
-
"gorm.io/gorm"
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
// Repository = data access for role (the only place that touches the DB for this domain)
|
|
13
|
-
// every method takes ctx, so a cancelled request cancels the query too
|
|
14
|
-
type Repository struct {
|
|
15
|
-
db *gorm.DB
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
func NewRepository(db *gorm.DB) *Repository {
|
|
19
|
-
return &Repository{db: db}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// PermissionCodes returns the permission codes granted to roleCode.
|
|
23
|
-
func (r *Repository) PermissionCodes(ctx context.Context, roleCode string) ([]string, error) {
|
|
24
|
-
var codes []string
|
|
25
|
-
err := tx.From(ctx, r.db).WithContext(ctx).
|
|
26
|
-
Model(&model.RolePermission{}).
|
|
27
|
-
Where("role_code = ?", roleCode).
|
|
28
|
-
Pluck("permission_code", &codes).Error
|
|
29
|
-
return codes, err
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
func (r *Repository) FindAll(ctx context.Context, limit, offset int) ([]model.Role, error) {
|
|
33
|
-
var items []model.Role
|
|
34
|
-
err := tx.From(ctx, r.db).WithContext(ctx).Order("code").Limit(limit).Offset(offset).Find(&items).Error
|
|
35
|
-
return items, err
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
func (r *Repository) FindByCode(ctx context.Context, code string) (*model.Role, error) {
|
|
39
|
-
var m model.Role
|
|
40
|
-
if err := tx.From(ctx, r.db).WithContext(ctx).First(&m, "code = ?", code).Error; err != nil {
|
|
41
|
-
return nil, err
|
|
42
|
-
}
|
|
43
|
-
return &m, nil
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
func (r *Repository) Create(ctx context.Context, m *model.Role) error {
|
|
47
|
-
return tx.From(ctx, r.db).WithContext(ctx).Create(m).Error
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// SetPermissions replaces roleCode's entire permission grant set in one
|
|
51
|
-
// transaction — simpler for a checkbox-style admin UI than diffing
|
|
52
|
-
// add/remove, and cheap at this scale (a handful of rows per role).
|
|
53
|
-
func (r *Repository) SetPermissions(ctx context.Context, roleCode string, permissionCodes []string) error {
|
|
54
|
-
return tx.From(ctx, r.db).WithContext(ctx).Transaction(func(t *gorm.DB) error {
|
|
55
|
-
if err := t.Where("role_code = ?", roleCode).Delete(&model.RolePermission{}).Error; err != nil {
|
|
56
|
-
return err
|
|
57
|
-
}
|
|
58
|
-
if len(permissionCodes) == 0 {
|
|
59
|
-
return nil
|
|
60
|
-
}
|
|
61
|
-
grants := make([]model.RolePermission, len(permissionCodes))
|
|
62
|
-
for i, code := range permissionCodes {
|
|
63
|
-
grants[i] = model.RolePermission{RoleCode: roleCode, PermissionCode: code}
|
|
64
|
-
}
|
|
65
|
-
return t.Create(&grants).Error
|
|
66
|
-
})
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
func (r *Repository) Delete(ctx context.Context, code string) error {
|
|
70
|
-
return tx.From(ctx, r.db).WithContext(ctx).Delete(&model.Role{}, "code = ?", code).Error
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
func (r *Repository) FindAllPermissions(ctx context.Context, limit, offset int) ([]model.Permission, error) {
|
|
74
|
-
var items []model.Permission
|
|
75
|
-
err := tx.From(ctx, r.db).WithContext(ctx).Order("code").Limit(limit).Offset(offset).Find(&items).Error
|
|
76
|
-
return items, err
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// AllPermissionCodes returns every permission code that exists, unpaginated
|
|
80
|
-
// — the catalog is a handful of rows, cheap to fetch whole for validating a
|
|
81
|
-
// SetPermissions request.
|
|
82
|
-
func (r *Repository) AllPermissionCodes(ctx context.Context) ([]string, error) {
|
|
83
|
-
var codes []string
|
|
84
|
-
err := tx.From(ctx, r.db).WithContext(ctx).Model(&model.Permission{}).Pluck("code", &codes).Error
|
|
85
|
-
return codes, err
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// CountRolesWithPermission counts roles other than excludeCode that grant
|
|
89
|
-
// permissionCode — used to check whether editing or deleting excludeCode
|
|
90
|
-
// would leave no role holding permissionCode at all.
|
|
91
|
-
func (r *Repository) CountRolesWithPermission(ctx context.Context, permissionCode, excludeCode string) (int64, error) {
|
|
92
|
-
var count int64
|
|
93
|
-
err := tx.From(ctx, r.db).WithContext(ctx).Model(&model.RolePermission{}).
|
|
94
|
-
Where("permission_code = ? AND role_code <> ?", permissionCode, excludeCode).
|
|
95
|
-
Count(&count).Error
|
|
96
|
-
return count, err
|
|
97
|
-
}
|
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
package role
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"context"
|
|
5
|
-
"errors"
|
|
6
|
-
"regexp"
|
|
7
|
-
"slices"
|
|
8
|
-
|
|
9
|
-
"{{goModule}}/internal/app/role/model"
|
|
10
|
-
"{{goModule}}/internal/shared/apperror"
|
|
11
|
-
"{{goModule}}/internal/shared/dberr"
|
|
12
|
-
|
|
13
|
-
"gorm.io/gorm"
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
// repository = what the service needs from the data layer (declared on the
|
|
17
|
-
// consumer side, so it can be faked in tests)
|
|
18
|
-
type repository interface {
|
|
19
|
-
PermissionCodes(ctx context.Context, roleCode string) ([]string, error)
|
|
20
|
-
FindAll(ctx context.Context, limit, offset int) ([]model.Role, error)
|
|
21
|
-
FindByCode(ctx context.Context, code string) (*model.Role, error)
|
|
22
|
-
Create(ctx context.Context, m *model.Role) error
|
|
23
|
-
SetPermissions(ctx context.Context, roleCode string, permissionCodes []string) error
|
|
24
|
-
Delete(ctx context.Context, code string) error
|
|
25
|
-
FindAllPermissions(ctx context.Context, limit, offset int) ([]model.Permission, error)
|
|
26
|
-
AllPermissionCodes(ctx context.Context) ([]string, error)
|
|
27
|
-
CountRolesWithPermission(ctx context.Context, permissionCode, excludeCode string) (int64, error)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Service = business logic for role (rules live here, it knows nothing about HTTP)
|
|
31
|
-
type Service struct {
|
|
32
|
-
repo repository
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
func NewService(repo repository) *Service {
|
|
36
|
-
return &Service{repo: repo}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// PermissionsOf resolves roleCode's permission set. This is the func value
|
|
40
|
-
// injected into middleware.Authz from main.go — shared/ can't import a
|
|
41
|
-
// domain package, so it calls this through a func, not a direct dependency.
|
|
42
|
-
func (s *Service) PermissionsOf(ctx context.Context, roleCode string) (map[string]struct{}, error) {
|
|
43
|
-
codes, err := s.repo.PermissionCodes(ctx, roleCode)
|
|
44
|
-
if err != nil {
|
|
45
|
-
return nil, err
|
|
46
|
-
}
|
|
47
|
-
set := make(map[string]struct{}, len(codes))
|
|
48
|
-
for _, c := range codes {
|
|
49
|
-
set[c] = struct{}{}
|
|
50
|
-
}
|
|
51
|
-
return set, nil
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// CodeExists reports whether code names a real role — used by user.Service's
|
|
55
|
-
// SetRole to validate a role assignment: development-only test schemas never
|
|
56
|
-
// create the users_role_fkey FK, so that alone can't be relied on to reject
|
|
57
|
-
// an unknown code.
|
|
58
|
-
func (s *Service) CodeExists(ctx context.Context, code string) (bool, error) {
|
|
59
|
-
if _, err := s.repo.FindByCode(ctx, code); err != nil {
|
|
60
|
-
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
61
|
-
return false, nil
|
|
62
|
-
}
|
|
63
|
-
return false, err
|
|
64
|
-
}
|
|
65
|
-
return true, nil
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// roleListItem pairs a role with its granted permission codes — every
|
|
69
|
-
// role-shaped response (list, create, set-permissions) carries both.
|
|
70
|
-
type roleListItem struct {
|
|
71
|
-
model.Role
|
|
72
|
-
Permissions []string
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
func (s *Service) List(ctx context.Context, limit, offset int) ([]roleListItem, error) {
|
|
76
|
-
roles, err := s.repo.FindAll(ctx, limit, offset)
|
|
77
|
-
if err != nil {
|
|
78
|
-
return nil, apperror.NewInternal(err)
|
|
79
|
-
}
|
|
80
|
-
// ponytail: one query per role, not a join. A system has a handful of
|
|
81
|
-
// roles, so this is a handful of round trips on an admin screen nobody
|
|
82
|
-
// loads in a loop — the join costs more to read than it saves.
|
|
83
|
-
//
|
|
84
|
-
// Do not copy this shape into a domain whose list is user-sized. There the
|
|
85
|
-
// upgrade is one query with a join (or an IN over the page's ids) and a
|
|
86
|
-
// group-by in Go; here it would be ceremony.
|
|
87
|
-
out := make([]roleListItem, len(roles))
|
|
88
|
-
for i, r := range roles {
|
|
89
|
-
perms, err := s.repo.PermissionCodes(ctx, r.Code)
|
|
90
|
-
if err != nil {
|
|
91
|
-
return nil, apperror.NewInternal(err)
|
|
92
|
-
}
|
|
93
|
-
out[i] = roleListItem{Role: r, Permissions: perms}
|
|
94
|
-
}
|
|
95
|
-
return out, nil
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// roleCodePattern mirrors the seeded system codes' shape (staff, admin) —
|
|
99
|
-
// lowercase, no spaces, safe to use as a URL path segment and a JWT claim.
|
|
100
|
-
var roleCodePattern = regexp.MustCompile(`^[a-z][a-z0-9_]*$`)
|
|
101
|
-
|
|
102
|
-
func (s *Service) Create(ctx context.Context, in CreateInput) (*roleListItem, error) {
|
|
103
|
-
if !roleCodePattern.MatchString(in.Code) {
|
|
104
|
-
return nil, errInvalidCode()
|
|
105
|
-
}
|
|
106
|
-
m := &model.Role{Code: in.Code, Name: in.Name}
|
|
107
|
-
if err := s.repo.Create(ctx, m); err != nil {
|
|
108
|
-
if dberr.IsDuplicate(err) {
|
|
109
|
-
return nil, errConflict()
|
|
110
|
-
}
|
|
111
|
-
return nil, apperror.NewInternal(err)
|
|
112
|
-
}
|
|
113
|
-
return &roleListItem{Role: *m, Permissions: []string{}}, nil
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// SetPermissions replaces code's entire permission grant set with
|
|
117
|
-
// permissionCodes — a full-replace, not add/remove, matches a checkbox-style
|
|
118
|
-
// admin UI ("here's the new full set") better than diffing.
|
|
119
|
-
func (s *Service) SetPermissions(ctx context.Context, code string, permissionCodes []string) (*roleListItem, error) {
|
|
120
|
-
m, err := s.repo.FindByCode(ctx, code)
|
|
121
|
-
if err != nil {
|
|
122
|
-
return nil, wrapFindErr(err)
|
|
123
|
-
}
|
|
124
|
-
if err := s.validatePermissionCodes(ctx, permissionCodes); err != nil {
|
|
125
|
-
return nil, err
|
|
126
|
-
}
|
|
127
|
-
if !slices.Contains(permissionCodes, PermRoleManage) {
|
|
128
|
-
isLast, err := s.isLastRoleManager(ctx, code)
|
|
129
|
-
if err != nil {
|
|
130
|
-
return nil, apperror.NewInternal(err)
|
|
131
|
-
}
|
|
132
|
-
if isLast {
|
|
133
|
-
return nil, errLastRoleManager()
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
if err := s.repo.SetPermissions(ctx, code, permissionCodes); err != nil {
|
|
137
|
-
// the migration's FK is a backstop for this same check — reachable
|
|
138
|
-
// only under a race with a concurrent permission deletion.
|
|
139
|
-
if dberr.IsForeignKey(err) {
|
|
140
|
-
return nil, errUnknownPermission()
|
|
141
|
-
}
|
|
142
|
-
return nil, apperror.NewInternal(err)
|
|
143
|
-
}
|
|
144
|
-
return &roleListItem{Role: *m, Permissions: permissionCodes}, nil
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// validatePermissionCodes checks every code against the actual permission
|
|
148
|
-
// catalog — the DB's FK alone isn't enough (the development bootstrap, used in dev/tests,
|
|
149
|
-
// never creates it, only the raw migration SQL does).
|
|
150
|
-
func (s *Service) validatePermissionCodes(ctx context.Context, codes []string) error {
|
|
151
|
-
all, err := s.repo.AllPermissionCodes(ctx)
|
|
152
|
-
if err != nil {
|
|
153
|
-
return apperror.NewInternal(err)
|
|
154
|
-
}
|
|
155
|
-
valid := make(map[string]struct{}, len(all))
|
|
156
|
-
for _, c := range all {
|
|
157
|
-
valid[c] = struct{}{}
|
|
158
|
-
}
|
|
159
|
-
for _, c := range codes {
|
|
160
|
-
if _, ok := valid[c]; !ok {
|
|
161
|
-
return errUnknownPermission()
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return nil
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
func (s *Service) Delete(ctx context.Context, code string) error {
|
|
168
|
-
m, err := s.repo.FindByCode(ctx, code)
|
|
169
|
-
if err != nil {
|
|
170
|
-
return wrapFindErr(err)
|
|
171
|
-
}
|
|
172
|
-
if m.IsSystem {
|
|
173
|
-
return errSystemRole()
|
|
174
|
-
}
|
|
175
|
-
isLast, err := s.isLastRoleManager(ctx, code)
|
|
176
|
-
if err != nil {
|
|
177
|
-
return apperror.NewInternal(err)
|
|
178
|
-
}
|
|
179
|
-
if isLast {
|
|
180
|
-
return errLastRoleManager()
|
|
181
|
-
}
|
|
182
|
-
if err := s.repo.Delete(ctx, code); err != nil {
|
|
183
|
-
if dberr.IsForeignKey(err) {
|
|
184
|
-
return errHasReferences()
|
|
185
|
-
}
|
|
186
|
-
return apperror.NewInternal(err)
|
|
187
|
-
}
|
|
188
|
-
return nil
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// isLastRoleManager reports whether code is the only role left granting
|
|
192
|
-
// PermRoleManage — blocks a self-lockout no one could fix back through this
|
|
193
|
-
// same API. Best-effort: reachable only under a race with a concurrent edit
|
|
194
|
-
// to the same permission, not worth a serializable transaction for an
|
|
195
|
-
// admin-only, low-traffic action.
|
|
196
|
-
func (s *Service) isLastRoleManager(ctx context.Context, code string) (bool, error) {
|
|
197
|
-
others, err := s.repo.CountRolesWithPermission(ctx, PermRoleManage, code)
|
|
198
|
-
if err != nil {
|
|
199
|
-
return false, err
|
|
200
|
-
}
|
|
201
|
-
return others == 0, nil
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
func (s *Service) ListPermissions(ctx context.Context, limit, offset int) ([]model.Permission, error) {
|
|
205
|
-
items, err := s.repo.FindAllPermissions(ctx, limit, offset)
|
|
206
|
-
if err != nil {
|
|
207
|
-
return nil, apperror.NewInternal(err)
|
|
208
|
-
}
|
|
209
|
-
return items, nil
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
func wrapFindErr(err error) error {
|
|
213
|
-
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
214
|
-
return errNotFound()
|
|
215
|
-
}
|
|
216
|
-
return apperror.NewInternal(err)
|
|
217
|
-
}
|