@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
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
package postgres
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"errors"
|
|
6
|
+
|
|
7
|
+
"{{goModule}}/internal/app/role/domain"
|
|
8
|
+
"{{goModule}}/internal/app/role/ports"
|
|
9
|
+
"{{goModule}}/internal/shared/dberr"
|
|
10
|
+
"{{goModule}}/internal/shared/tx"
|
|
11
|
+
|
|
12
|
+
"gorm.io/gorm"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
var _ ports.Repository = (*Repository)(nil)
|
|
16
|
+
|
|
17
|
+
type Repository struct {
|
|
18
|
+
db *gorm.DB
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} }
|
|
22
|
+
|
|
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(&RolePermission{}).Where("role_code = ?", roleCode).Pluck("permission_code", &codes).Error
|
|
27
|
+
return codes, mapDatabaseError(err)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func (r *Repository) FindAll(ctx context.Context, limit, offset int) ([]domain.Role, error) {
|
|
31
|
+
var rows []Role
|
|
32
|
+
err := tx.From(ctx, r.db).WithContext(ctx).Order("code").Limit(limit).Offset(offset).Find(&rows).Error
|
|
33
|
+
if err != nil {
|
|
34
|
+
return nil, mapDatabaseError(err)
|
|
35
|
+
}
|
|
36
|
+
items := make([]domain.Role, len(rows))
|
|
37
|
+
for i := range rows {
|
|
38
|
+
items[i] = toDomainRole(&rows[i])
|
|
39
|
+
}
|
|
40
|
+
return items, nil
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func (r *Repository) FindByCode(ctx context.Context, code string) (*domain.Role, error) {
|
|
44
|
+
var row Role
|
|
45
|
+
if err := tx.From(ctx, r.db).WithContext(ctx).First(&row, "code = ?", code).Error; err != nil {
|
|
46
|
+
return nil, mapDatabaseError(err)
|
|
47
|
+
}
|
|
48
|
+
value := toDomainRole(&row)
|
|
49
|
+
return &value, nil
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func (r *Repository) Create(ctx context.Context, role *domain.Role) error {
|
|
53
|
+
return mapDatabaseError(tx.From(ctx, r.db).WithContext(ctx).Create(fromDomainRole(role)).Error)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func (r *Repository) SetPermissions(ctx context.Context, roleCode string, permissionCodes []string) error {
|
|
57
|
+
return tx.Do(ctx, r.db, func(txctx context.Context) error {
|
|
58
|
+
db := tx.From(txctx, r.db).WithContext(txctx)
|
|
59
|
+
if err := db.Where("role_code = ?", roleCode).Delete(&RolePermission{}).Error; err != nil {
|
|
60
|
+
return mapDatabaseError(err)
|
|
61
|
+
}
|
|
62
|
+
if len(permissionCodes) == 0 {
|
|
63
|
+
return nil
|
|
64
|
+
}
|
|
65
|
+
grants := make([]RolePermission, len(permissionCodes))
|
|
66
|
+
for i, code := range permissionCodes {
|
|
67
|
+
grants[i] = RolePermission{RoleCode: roleCode, PermissionCode: code}
|
|
68
|
+
}
|
|
69
|
+
return mapDatabaseError(db.Create(&grants).Error)
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
func (r *Repository) Delete(ctx context.Context, code string) error {
|
|
74
|
+
result := tx.From(ctx, r.db).WithContext(ctx).Where("code = ?", code).Delete(&Role{})
|
|
75
|
+
if result.Error != nil {
|
|
76
|
+
if dberr.IsForeignKey(result.Error) {
|
|
77
|
+
return domain.ErrHasReferences
|
|
78
|
+
}
|
|
79
|
+
return mapDatabaseError(result.Error)
|
|
80
|
+
}
|
|
81
|
+
if result.RowsAffected == 0 {
|
|
82
|
+
return domain.ErrNotFound
|
|
83
|
+
}
|
|
84
|
+
return nil
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func (r *Repository) FindAllPermissions(ctx context.Context, limit, offset int) ([]domain.Permission, error) {
|
|
88
|
+
var rows []Permission
|
|
89
|
+
err := tx.From(ctx, r.db).WithContext(ctx).Order("code").Limit(limit).Offset(offset).Find(&rows).Error
|
|
90
|
+
if err != nil {
|
|
91
|
+
return nil, mapDatabaseError(err)
|
|
92
|
+
}
|
|
93
|
+
items := make([]domain.Permission, len(rows))
|
|
94
|
+
for i := range rows {
|
|
95
|
+
items[i] = toDomainPermission(&rows[i])
|
|
96
|
+
}
|
|
97
|
+
return items, nil
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
func (r *Repository) AllPermissionCodes(ctx context.Context) ([]string, error) {
|
|
101
|
+
var codes []string
|
|
102
|
+
err := tx.From(ctx, r.db).WithContext(ctx).Model(&Permission{}).Pluck("code", &codes).Error
|
|
103
|
+
return codes, mapDatabaseError(err)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
func (r *Repository) CountRolesWithPermission(ctx context.Context, permissionCode, excludeCode string) (int64, error) {
|
|
107
|
+
var count int64
|
|
108
|
+
err := tx.From(ctx, r.db).WithContext(ctx).Model(&RolePermission{}).
|
|
109
|
+
Where("permission_code = ? AND role_code <> ?", permissionCode, excludeCode).Count(&count).Error
|
|
110
|
+
return count, mapDatabaseError(err)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
func mapDatabaseError(err error) error {
|
|
114
|
+
if err == nil {
|
|
115
|
+
return nil
|
|
116
|
+
}
|
|
117
|
+
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
118
|
+
return domain.ErrNotFound
|
|
119
|
+
}
|
|
120
|
+
if dberr.IsDuplicate(err) {
|
|
121
|
+
return domain.ErrConflict
|
|
122
|
+
}
|
|
123
|
+
if dberr.IsForeignKey(err) {
|
|
124
|
+
return domain.ErrUnknownPermission
|
|
125
|
+
}
|
|
126
|
+
return err
|
|
127
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
package
|
|
1
|
+
package postgres
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
5
|
"os"
|
|
6
6
|
"testing"
|
|
7
7
|
|
|
8
|
-
"{{goModule}}/internal/app/role/
|
|
8
|
+
"{{goModule}}/internal/app/role/domain"
|
|
9
9
|
|
|
10
10
|
"github.com/google/uuid"
|
|
11
11
|
"gorm.io/driver/postgres"
|
|
@@ -67,11 +67,11 @@ func testCode(t *testing.T) string {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
// seedPermission inserts directly rather than through Repository, which has
|
|
70
|
-
// no create method for Permission on purpose (see model
|
|
70
|
+
// no create method for Permission on purpose (see adapters/outbound/postgres/model.go:
|
|
71
71
|
// "seeded by migration, not admin-created"). Tests still need rows to grant.
|
|
72
72
|
func seedPermission(t *testing.T, tx *gorm.DB, code string) {
|
|
73
73
|
t.Helper()
|
|
74
|
-
if err := tx.WithContext(context.Background()).Create(&
|
|
74
|
+
if err := tx.WithContext(context.Background()).Create(&Permission{Code: code, Description: code}).Error; err != nil {
|
|
75
75
|
t.Fatalf("seed permission %s: %v", code, err)
|
|
76
76
|
}
|
|
77
77
|
}
|
|
@@ -87,7 +87,7 @@ func TestRepository_SetPermissions_ReplacesEntireSet(t *testing.T) {
|
|
|
87
87
|
|
|
88
88
|
roleCode := testCode(t)
|
|
89
89
|
permA, permB, permC := testCode(t), testCode(t), testCode(t)
|
|
90
|
-
if err := repo.Create(ctx, &
|
|
90
|
+
if err := repo.Create(ctx, &domain.Role{Code: roleCode, Name: roleCode}); err != nil {
|
|
91
91
|
t.Fatalf("seed role: %v", err)
|
|
92
92
|
}
|
|
93
93
|
seedPermission(t, tx, permA)
|
|
@@ -121,7 +121,7 @@ func TestRepository_Delete_CascadesRolePermissions(t *testing.T) {
|
|
|
121
121
|
|
|
122
122
|
roleCode := testCode(t)
|
|
123
123
|
permCode := testCode(t)
|
|
124
|
-
if err := repo.Create(ctx, &
|
|
124
|
+
if err := repo.Create(ctx, &domain.Role{Code: roleCode, Name: roleCode}); err != nil {
|
|
125
125
|
t.Fatalf("seed role: %v", err)
|
|
126
126
|
}
|
|
127
127
|
seedPermission(t, tx, permCode)
|
|
@@ -152,10 +152,10 @@ func TestRepository_CountRolesWithPermission_ExcludesGivenRole(t *testing.T) {
|
|
|
152
152
|
|
|
153
153
|
roleA, roleB := testCode(t), testCode(t)
|
|
154
154
|
permCode := testCode(t)
|
|
155
|
-
if err := repo.Create(ctx, &
|
|
155
|
+
if err := repo.Create(ctx, &domain.Role{Code: roleA, Name: roleA}); err != nil {
|
|
156
156
|
t.Fatalf("seed role a: %v", err)
|
|
157
157
|
}
|
|
158
|
-
if err := repo.Create(ctx, &
|
|
158
|
+
if err := repo.Create(ctx, &domain.Role{Code: roleB, Name: roleB}); err != nil {
|
|
159
159
|
t.Fatalf("seed role b: %v", err)
|
|
160
160
|
}
|
|
161
161
|
seedPermission(t, tx, permCode)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package application
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"time"
|
|
5
|
+
|
|
6
|
+
"{{goModule}}/internal/app/role/domain"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
const PermRoleManage = "role:manage"
|
|
10
|
+
|
|
11
|
+
type CreateInput struct {
|
|
12
|
+
Code string `json:"code"`
|
|
13
|
+
Name string `json:"name"`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type SetPermissionsInput struct {
|
|
17
|
+
PermissionCodes *[]string `json:"permission_codes"`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type RoleListItem struct {
|
|
21
|
+
Role domain.Role
|
|
22
|
+
Permissions []string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type RoleResponse struct {
|
|
26
|
+
Code string `json:"code"`
|
|
27
|
+
Name string `json:"name"`
|
|
28
|
+
IsSystem bool `json:"is_system"`
|
|
29
|
+
Permissions []string `json:"permissions"`
|
|
30
|
+
CreatedAt time.Time `json:"created_at"`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type PermissionResponse struct {
|
|
34
|
+
Code string `json:"code"`
|
|
35
|
+
Description string `json:"description"`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func ToRoleResponse(item RoleListItem) RoleResponse {
|
|
39
|
+
return RoleResponse{
|
|
40
|
+
Code: item.Role.Code, Name: item.Role.Name, IsSystem: item.Role.IsSystem,
|
|
41
|
+
Permissions: item.Permissions, CreatedAt: item.Role.CreatedAt,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func ToPermissionResponse(item domain.Permission) PermissionResponse {
|
|
46
|
+
return PermissionResponse{Code: item.Code, Description: item.Description}
|
|
47
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
package application
|
|
2
|
+
|
|
3
|
+
import "{{goModule}}/internal/app/role/domain"
|
|
4
|
+
|
|
5
|
+
func errInvalidCode() error {
|
|
6
|
+
return domain.Rule("ROLE_INVALID_CODE", "code must start with a lowercase letter and contain only lowercase letters, digits, or underscores", domain.ErrInvalidCode)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
func errSystemRole() error {
|
|
10
|
+
return domain.Rule("ROLE_IS_SYSTEM", "staff/admin are system roles and cannot be deleted", domain.ErrSystemRole)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
func errUnknownPermission() error {
|
|
14
|
+
return domain.Rule("ROLE_UNKNOWN_PERMISSION", "one or more permission codes do not exist", domain.ErrUnknownPermission)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func errLastRoleManager() error {
|
|
18
|
+
return domain.Rule("ROLE_LAST_MANAGER", "this is the only role that can manage roles/permissions — grant "+PermRoleManage+" to another role first", domain.ErrLastRoleManager)
|
|
19
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Package application contains role management use cases. It owns the rules
|
|
2
|
+
// for permission grants and never imports GORM, Gin, or HTTP error types.
|
|
3
|
+
package application
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"context"
|
|
7
|
+
"errors"
|
|
8
|
+
"regexp"
|
|
9
|
+
"slices"
|
|
10
|
+
|
|
11
|
+
"{{goModule}}/internal/app/role/domain"
|
|
12
|
+
"{{goModule}}/internal/app/role/ports"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
type ServicePort interface {
|
|
16
|
+
PermissionsOf(context.Context, string) (map[string]struct{}, error)
|
|
17
|
+
CodeExists(context.Context, string) (bool, error)
|
|
18
|
+
List(context.Context, int, int) ([]RoleListItem, error)
|
|
19
|
+
Create(context.Context, CreateInput) (*RoleListItem, error)
|
|
20
|
+
SetPermissions(context.Context, string, []string) (*RoleListItem, error)
|
|
21
|
+
Delete(context.Context, string) error
|
|
22
|
+
ListPermissions(context.Context, int, int) ([]domain.Permission, error)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type Service struct {
|
|
26
|
+
repo ports.Repository
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func NewService(repo ports.Repository) *Service { return &Service{repo: repo} }
|
|
30
|
+
|
|
31
|
+
func (s *Service) PermissionsOf(ctx context.Context, roleCode string) (map[string]struct{}, error) {
|
|
32
|
+
codes, err := s.repo.PermissionCodes(ctx, roleCode)
|
|
33
|
+
if err != nil {
|
|
34
|
+
return nil, err
|
|
35
|
+
}
|
|
36
|
+
set := make(map[string]struct{}, len(codes))
|
|
37
|
+
for _, code := range codes {
|
|
38
|
+
set[code] = struct{}{}
|
|
39
|
+
}
|
|
40
|
+
return set, nil
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func (s *Service) CodeExists(ctx context.Context, code string) (bool, error) {
|
|
44
|
+
if _, err := s.repo.FindByCode(ctx, code); err != nil {
|
|
45
|
+
if errors.Is(err, domain.ErrNotFound) {
|
|
46
|
+
return false, nil
|
|
47
|
+
}
|
|
48
|
+
return false, err
|
|
49
|
+
}
|
|
50
|
+
return true, nil
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func (s *Service) List(ctx context.Context, limit, offset int) ([]RoleListItem, error) {
|
|
54
|
+
roles, err := s.repo.FindAll(ctx, limit, offset)
|
|
55
|
+
if err != nil {
|
|
56
|
+
return nil, err
|
|
57
|
+
}
|
|
58
|
+
out := make([]RoleListItem, len(roles))
|
|
59
|
+
for i, role := range roles {
|
|
60
|
+
perms, err := s.repo.PermissionCodes(ctx, role.Code)
|
|
61
|
+
if err != nil {
|
|
62
|
+
return nil, err
|
|
63
|
+
}
|
|
64
|
+
out[i] = RoleListItem{Role: role, Permissions: perms}
|
|
65
|
+
}
|
|
66
|
+
return out, nil
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
var roleCodePattern = regexp.MustCompile(`^[a-z][a-z0-9_]*$`)
|
|
70
|
+
|
|
71
|
+
func (s *Service) Create(ctx context.Context, in CreateInput) (*RoleListItem, error) {
|
|
72
|
+
if !roleCodePattern.MatchString(in.Code) {
|
|
73
|
+
return nil, errInvalidCode()
|
|
74
|
+
}
|
|
75
|
+
role := &domain.Role{Code: in.Code, Name: in.Name}
|
|
76
|
+
if err := s.repo.Create(ctx, role); err != nil {
|
|
77
|
+
return nil, err
|
|
78
|
+
}
|
|
79
|
+
return &RoleListItem{Role: *role, Permissions: []string{}}, nil
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func (s *Service) SetPermissions(ctx context.Context, code string, permissionCodes []string) (*RoleListItem, error) {
|
|
83
|
+
role, err := s.repo.FindByCode(ctx, code)
|
|
84
|
+
if err != nil {
|
|
85
|
+
return nil, wrapFindErr(err)
|
|
86
|
+
}
|
|
87
|
+
if err := s.validatePermissionCodes(ctx, permissionCodes); err != nil {
|
|
88
|
+
return nil, err
|
|
89
|
+
}
|
|
90
|
+
if !slices.Contains(permissionCodes, PermRoleManage) {
|
|
91
|
+
isLast, err := s.isLastRoleManager(ctx, code)
|
|
92
|
+
if err != nil {
|
|
93
|
+
return nil, err
|
|
94
|
+
}
|
|
95
|
+
if isLast {
|
|
96
|
+
return nil, errLastRoleManager()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if err := s.repo.SetPermissions(ctx, code, permissionCodes); err != nil {
|
|
100
|
+
return nil, err
|
|
101
|
+
}
|
|
102
|
+
return &RoleListItem{Role: *role, Permissions: permissionCodes}, nil
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func (s *Service) validatePermissionCodes(ctx context.Context, codes []string) error {
|
|
106
|
+
all, err := s.repo.AllPermissionCodes(ctx)
|
|
107
|
+
if err != nil {
|
|
108
|
+
return err
|
|
109
|
+
}
|
|
110
|
+
valid := make(map[string]struct{}, len(all))
|
|
111
|
+
for _, code := range all {
|
|
112
|
+
valid[code] = struct{}{}
|
|
113
|
+
}
|
|
114
|
+
for _, code := range codes {
|
|
115
|
+
if _, ok := valid[code]; !ok {
|
|
116
|
+
return errUnknownPermission()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return nil
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
func (s *Service) Delete(ctx context.Context, code string) error {
|
|
123
|
+
role, err := s.repo.FindByCode(ctx, code)
|
|
124
|
+
if err != nil {
|
|
125
|
+
return wrapFindErr(err)
|
|
126
|
+
}
|
|
127
|
+
if role.IsSystem {
|
|
128
|
+
return errSystemRole()
|
|
129
|
+
}
|
|
130
|
+
isLast, err := s.isLastRoleManager(ctx, code)
|
|
131
|
+
if err != nil {
|
|
132
|
+
return err
|
|
133
|
+
}
|
|
134
|
+
if isLast {
|
|
135
|
+
return errLastRoleManager()
|
|
136
|
+
}
|
|
137
|
+
return s.repo.Delete(ctx, code)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
func (s *Service) isLastRoleManager(ctx context.Context, code string) (bool, error) {
|
|
141
|
+
others, err := s.repo.CountRolesWithPermission(ctx, PermRoleManage, code)
|
|
142
|
+
if err != nil {
|
|
143
|
+
return false, err
|
|
144
|
+
}
|
|
145
|
+
return others == 0, nil
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
func (s *Service) ListPermissions(ctx context.Context, limit, offset int) ([]domain.Permission, error) {
|
|
149
|
+
return s.repo.FindAllPermissions(ctx, limit, offset)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
func wrapFindErr(err error) error {
|
|
153
|
+
if errors.Is(err, domain.ErrNotFound) {
|
|
154
|
+
return domain.ErrNotFound
|
|
155
|
+
}
|
|
156
|
+
return err
|
|
157
|
+
}
|
package/templates/add/rbac/internal/app/role/{service_test.go.hbs → application/service_test.go.hbs}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
package
|
|
1
|
+
package application
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
@@ -6,8 +6,7 @@ import (
|
|
|
6
6
|
"net/http"
|
|
7
7
|
"testing"
|
|
8
8
|
|
|
9
|
-
"{{goModule}}/internal/app/role/
|
|
10
|
-
"{{goModule}}/internal/shared/apperror"
|
|
9
|
+
"{{goModule}}/internal/app/role/domain"
|
|
11
10
|
)
|
|
12
11
|
|
|
13
12
|
// fakeRepo = mock of the repository interface, so the service can be tested
|
|
@@ -15,7 +14,7 @@ import (
|
|
|
15
14
|
// than the one being edited currently grant PermRoleManage", exactly what
|
|
16
15
|
// CountRolesWithPermission's excludeCode param means.
|
|
17
16
|
type fakeRepo struct {
|
|
18
|
-
role *
|
|
17
|
+
role *domain.Role
|
|
19
18
|
findErr error
|
|
20
19
|
allPermissionCodes []string
|
|
21
20
|
countWithPermission int64
|
|
@@ -24,38 +23,46 @@ type fakeRepo struct {
|
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
func (f *fakeRepo) PermissionCodes(context.Context, string) ([]string, error) { return nil, nil }
|
|
27
|
-
func (f *fakeRepo) FindAll(context.Context, int, int) ([]
|
|
28
|
-
func (f *fakeRepo) FindByCode(context.Context, string) (*
|
|
26
|
+
func (f *fakeRepo) FindAll(context.Context, int, int) ([]domain.Role, error) { return nil, nil }
|
|
27
|
+
func (f *fakeRepo) FindByCode(context.Context, string) (*domain.Role, error) {
|
|
29
28
|
if f.findErr != nil {
|
|
30
29
|
return nil, f.findErr
|
|
31
30
|
}
|
|
32
31
|
return f.role, nil
|
|
33
32
|
}
|
|
34
|
-
func (f *fakeRepo) Create(context.Context, *
|
|
33
|
+
func (f *fakeRepo) Create(context.Context, *domain.Role) error { return nil }
|
|
35
34
|
func (f *fakeRepo) SetPermissions(context.Context, string, []string) error {
|
|
36
35
|
return f.setPermissionsErr
|
|
37
36
|
}
|
|
38
37
|
func (f *fakeRepo) Delete(context.Context, string) error { return f.deleteErr }
|
|
39
|
-
func (f *fakeRepo) FindAllPermissions(context.Context, int, int) ([]
|
|
38
|
+
func (f *fakeRepo) FindAllPermissions(context.Context, int, int) ([]domain.Permission, error) {
|
|
40
39
|
return nil, nil
|
|
41
40
|
}
|
|
42
|
-
func (f *fakeRepo) AllPermissionCodes(context.Context) ([]string, error) {
|
|
41
|
+
func (f *fakeRepo) AllPermissionCodes(context.Context) ([]string, error) {
|
|
42
|
+
return f.allPermissionCodes, nil
|
|
43
|
+
}
|
|
43
44
|
func (f *fakeRepo) CountRolesWithPermission(context.Context, string, string) (int64, error) {
|
|
44
45
|
return f.countWithPermission, nil
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
func status(t *testing.T, err error) int {
|
|
48
49
|
t.Helper()
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
if errors.Is(err, domain.ErrConflict) || errors.Is(err, domain.ErrSystemRole) || errors.Is(err, domain.ErrLastRoleManager) {
|
|
51
|
+
return http.StatusConflict
|
|
52
|
+
}
|
|
53
|
+
if errors.Is(err, domain.ErrInvalidCode) || errors.Is(err, domain.ErrUnknownPermission) {
|
|
54
|
+
return http.StatusUnprocessableEntity
|
|
55
|
+
}
|
|
56
|
+
if errors.Is(err, domain.ErrNotFound) {
|
|
57
|
+
return http.StatusNotFound
|
|
52
58
|
}
|
|
53
|
-
|
|
59
|
+
t.Fatalf("unexpected role error %T: %v", err, err)
|
|
60
|
+
return 0
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
func TestService_SetPermissions_LastRoleManagerLockout(t *testing.T) {
|
|
57
64
|
repo := &fakeRepo{
|
|
58
|
-
role: &
|
|
65
|
+
role: &domain.Role{Code: "ops", Name: "Ops"},
|
|
59
66
|
allPermissionCodes: []string{PermRoleManage, "user:read"},
|
|
60
67
|
countWithPermission: 0, // no OTHER role grants PermRoleManage
|
|
61
68
|
}
|
|
@@ -69,7 +76,7 @@ func TestService_SetPermissions_LastRoleManagerLockout(t *testing.T) {
|
|
|
69
76
|
|
|
70
77
|
func TestService_SetPermissions_AllowedWhenAnotherRoleStillManages(t *testing.T) {
|
|
71
78
|
repo := &fakeRepo{
|
|
72
|
-
role: &
|
|
79
|
+
role: &domain.Role{Code: "ops", Name: "Ops"},
|
|
73
80
|
allPermissionCodes: []string{PermRoleManage, "user:read"},
|
|
74
81
|
countWithPermission: 1, // another role still grants PermRoleManage
|
|
75
82
|
}
|
|
@@ -81,7 +88,7 @@ func TestService_SetPermissions_AllowedWhenAnotherRoleStillManages(t *testing.T)
|
|
|
81
88
|
|
|
82
89
|
func TestService_SetPermissions_KeepingRoleManageNeverLocksOut(t *testing.T) {
|
|
83
90
|
repo := &fakeRepo{
|
|
84
|
-
role: &
|
|
91
|
+
role: &domain.Role{Code: "ops", Name: "Ops"},
|
|
85
92
|
allPermissionCodes: []string{PermRoleManage},
|
|
86
93
|
countWithPermission: 0,
|
|
87
94
|
}
|
|
@@ -93,7 +100,7 @@ func TestService_SetPermissions_KeepingRoleManageNeverLocksOut(t *testing.T) {
|
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
func TestService_Delete_LastRoleManagerLockout(t *testing.T) {
|
|
96
|
-
repo := &fakeRepo{role: &
|
|
103
|
+
repo := &fakeRepo{role: &domain.Role{Code: "ops"}, countWithPermission: 0}
|
|
97
104
|
svc := NewService(repo)
|
|
98
105
|
err := svc.Delete(context.Background(), "ops")
|
|
99
106
|
if got := status(t, err); got != http.StatusConflict {
|
|
@@ -102,7 +109,7 @@ func TestService_Delete_LastRoleManagerLockout(t *testing.T) {
|
|
|
102
109
|
}
|
|
103
110
|
|
|
104
111
|
func TestService_Delete_AllowedWhenAnotherRoleStillManages(t *testing.T) {
|
|
105
|
-
repo := &fakeRepo{role: &
|
|
112
|
+
repo := &fakeRepo{role: &domain.Role{Code: "ops"}, countWithPermission: 1}
|
|
106
113
|
svc := NewService(repo)
|
|
107
114
|
if err := svc.Delete(context.Background(), "ops"); err != nil {
|
|
108
115
|
t.Fatalf("expected delete to succeed when another role still manages roles, got %v", err)
|
|
@@ -110,7 +117,7 @@ func TestService_Delete_AllowedWhenAnotherRoleStillManages(t *testing.T) {
|
|
|
110
117
|
}
|
|
111
118
|
|
|
112
119
|
func TestService_Delete_SystemRoleRejected(t *testing.T) {
|
|
113
|
-
repo := &fakeRepo{role: &
|
|
120
|
+
repo := &fakeRepo{role: &domain.Role{Code: "admin", IsSystem: true}}
|
|
114
121
|
svc := NewService(repo)
|
|
115
122
|
err := svc.Delete(context.Background(), "admin")
|
|
116
123
|
if got := status(t, err); got != http.StatusConflict {
|
|
@@ -3,33 +3,46 @@ package role
|
|
|
3
3
|
import (
|
|
4
4
|
"time"
|
|
5
5
|
|
|
6
|
+
httpadapter "{{goModule}}/internal/app/role/adapters/inbound/http"
|
|
7
|
+
"{{goModule}}/internal/app/role/adapters/outbound/postgres"
|
|
8
|
+
"{{goModule}}/internal/app/role/application"
|
|
9
|
+
"{{goModule}}/internal/app/role/domain"
|
|
10
|
+
"{{goModule}}/internal/app/role/ports"
|
|
6
11
|
"{{goModule}}/internal/shared/middleware"
|
|
7
12
|
|
|
8
13
|
"gorm.io/gorm"
|
|
9
14
|
)
|
|
10
15
|
|
|
11
|
-
// Composition
|
|
12
|
-
//
|
|
16
|
+
// Composition is the only public graph exposed to cmd/api. The feature root
|
|
17
|
+
// owns the role repository, application service, permission cache, and HTTP
|
|
18
|
+
// adapter; process wiring does not construct any of those internals.
|
|
13
19
|
type Composition struct {
|
|
14
|
-
Service *Service
|
|
20
|
+
Service *application.Service
|
|
15
21
|
Authz *middleware.Authz
|
|
16
|
-
Handler *Handler
|
|
22
|
+
Handler *httpadapter.Handler
|
|
17
23
|
}
|
|
18
24
|
|
|
19
|
-
|
|
25
|
+
type Service = application.Service
|
|
26
|
+
type ServicePort = application.ServicePort
|
|
27
|
+
type Repository = postgres.Repository
|
|
28
|
+
type CreateInput = application.CreateInput
|
|
29
|
+
type SetPermissionsInput = application.SetPermissionsInput
|
|
30
|
+
type Role = domain.Role
|
|
31
|
+
type Permission = domain.Permission
|
|
32
|
+
type AuthorizationPort = ports.AuthorizationPort
|
|
33
|
+
|
|
34
|
+
const PermRoleManage = application.PermRoleManage
|
|
35
|
+
|
|
20
36
|
func NewServiceFromDB(db *gorm.DB) *Service {
|
|
21
|
-
return NewService(NewRepository(db))
|
|
37
|
+
return application.NewService(postgres.NewRepository(db))
|
|
22
38
|
}
|
|
23
39
|
|
|
24
|
-
// NewCompositionFromDB builds role and its authorization middleware locally.
|
|
25
|
-
// The API binary supplies the shared DB/config values and only registers the
|
|
26
|
-
// returned handlers.
|
|
27
40
|
func NewCompositionFromDB(db *gorm.DB, jwtSecret string, cacheTTL time.Duration) *Composition {
|
|
28
41
|
service := NewServiceFromDB(db)
|
|
29
42
|
authz := middleware.NewAuthz(service.PermissionsOf, cacheTTL)
|
|
30
43
|
return &Composition{
|
|
31
44
|
Service: service,
|
|
32
45
|
Authz: authz,
|
|
33
|
-
Handler: NewHandler(service, jwtSecret, authz),
|
|
46
|
+
Handler: httpadapter.NewHandler(service, jwtSecret, authz),
|
|
34
47
|
}
|
|
35
48
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Package domain contains role and permission state without GORM or HTTP
|
|
2
|
+
// concerns.
|
|
3
|
+
package domain
|
|
4
|
+
|
|
5
|
+
import "time"
|
|
6
|
+
|
|
7
|
+
type Role struct {
|
|
8
|
+
Code string
|
|
9
|
+
Name string
|
|
10
|
+
IsSystem bool
|
|
11
|
+
CreatedAt time.Time
|
|
12
|
+
UpdatedAt time.Time
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type Permission struct {
|
|
16
|
+
Code string
|
|
17
|
+
Description string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type RolePermission struct {
|
|
21
|
+
RoleCode string
|
|
22
|
+
PermissionCode string
|
|
23
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
package domain
|
|
2
|
+
|
|
3
|
+
import "errors"
|
|
4
|
+
|
|
5
|
+
var (
|
|
6
|
+
ErrNotFound = errors.New("role not found")
|
|
7
|
+
ErrConflict = errors.New("role conflict")
|
|
8
|
+
ErrHasReferences = errors.New("role has references")
|
|
9
|
+
ErrInvalidCode = errors.New("role code is invalid")
|
|
10
|
+
ErrSystemRole = errors.New("system role cannot be changed")
|
|
11
|
+
ErrUnknownPermission = errors.New("permission does not exist")
|
|
12
|
+
ErrLastRoleManager = errors.New("last role manager cannot be removed")
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
type RuleError struct {
|
|
16
|
+
Code string
|
|
17
|
+
Message string
|
|
18
|
+
Cause error
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func (e *RuleError) Error() string { return e.Message }
|
|
22
|
+
func (e *RuleError) Unwrap() error { return e.Cause }
|
|
23
|
+
|
|
24
|
+
func Rule(code, message string, cause error) error {
|
|
25
|
+
return &RuleError{Code: code, Message: message, Cause: cause}
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Package ports contains role's application-owned repository contracts.
|
|
2
|
+
package ports
|
|
3
|
+
|
|
4
|
+
import (
|
|
5
|
+
"context"
|
|
6
|
+
|
|
7
|
+
"{{goModule}}/internal/app/role/domain"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
type Repository interface {
|
|
11
|
+
PermissionCodes(context.Context, string) ([]string, error)
|
|
12
|
+
FindAll(context.Context, int, int) ([]domain.Role, error)
|
|
13
|
+
FindByCode(context.Context, string) (*domain.Role, error)
|
|
14
|
+
Create(context.Context, *domain.Role) error
|
|
15
|
+
SetPermissions(context.Context, string, []string) error
|
|
16
|
+
Delete(context.Context, string) error
|
|
17
|
+
FindAllPermissions(context.Context, int, int) ([]domain.Permission, error)
|
|
18
|
+
AllPermissionCodes(context.Context) ([]string, error)
|
|
19
|
+
CountRolesWithPermission(context.Context, string, string) (int64, error)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type AuthorizationPort interface {
|
|
23
|
+
PermissionsOf(context.Context, string) (map[string]struct{}, error)
|
|
24
|
+
CodeExists(context.Context, string) (bool, error)
|
|
25
|
+
}
|