@robsun/create-keystone-app 0.2.13 → 0.4.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 +46 -43
- package/dist/create-keystone-app.js +347 -10
- package/dist/create-module.js +1219 -607
- package/package.json +22 -23
- package/template/.claude/skills/keystone-implement/SKILL.md +113 -0
- package/template/.claude/skills/keystone-implement/references/CHECKLIST.md +91 -0
- package/template/.claude/skills/keystone-implement/references/PATTERNS.md +1088 -0
- package/template/.claude/skills/keystone-implement/references/SCHEMA.md +135 -0
- package/template/.claude/skills/keystone-implement/references/TESTING.md +231 -0
- package/template/.claude/skills/keystone-requirements/SKILL.md +296 -0
- package/template/.claude/skills/keystone-requirements/references/CONFIRM_TEMPLATE.md +170 -0
- package/template/.claude/skills/keystone-requirements/references/SCHEMA.md +135 -0
- package/template/.eslintrc.js +3 -0
- package/template/.github/workflows/ci.yml +30 -0
- package/template/.github/workflows/release.yml +32 -0
- package/template/.golangci.yml +11 -0
- package/template/README.md +81 -73
- package/template/apps/server/README.md +8 -0
- package/template/apps/server/cmd/server/main.go +27 -185
- package/template/apps/server/config.example.yaml +31 -1
- package/template/apps/server/config.yaml +31 -1
- package/template/apps/server/go.mod +60 -18
- package/template/apps/server/go.sum +183 -31
- package/template/apps/server/internal/frontend/embed.go +3 -8
- package/template/apps/server/internal/modules/example/README.md +18 -0
- package/template/apps/server/internal/modules/example/api/handler/handler_test.go +9 -0
- package/template/apps/server/internal/modules/example/api/handler/item_handler.go +468 -165
- package/template/apps/server/internal/modules/example/bootstrap/seeds/item.go +217 -8
- package/template/apps/server/internal/modules/example/domain/models/item.go +40 -7
- package/template/apps/server/internal/modules/example/domain/service/approval_callback.go +68 -0
- package/template/apps/server/internal/modules/example/domain/service/approval_schema.go +41 -0
- package/template/apps/server/internal/modules/example/domain/service/errors.go +20 -22
- package/template/apps/server/internal/modules/example/domain/service/item_service.go +267 -7
- package/template/apps/server/internal/modules/example/domain/service/item_service_test.go +281 -0
- package/template/apps/server/internal/modules/example/i18n/keys.go +32 -20
- package/template/apps/server/internal/modules/example/i18n/locales/en-US.json +30 -18
- package/template/apps/server/internal/modules/example/i18n/locales/zh-CN.json +30 -18
- package/template/apps/server/internal/modules/example/infra/exporter/item_exporter.go +119 -0
- package/template/apps/server/internal/modules/example/infra/importer/item_importer.go +77 -0
- package/template/apps/server/internal/modules/example/infra/repository/item_repository.go +99 -49
- package/template/apps/server/internal/modules/example/module.go +171 -97
- package/template/apps/server/internal/modules/example/tests/integration_test.go +7 -0
- package/template/apps/server/internal/modules/manifest.go +7 -7
- package/template/apps/web/README.md +4 -2
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/src/app.config.ts +8 -6
- package/template/apps/web/src/index.css +7 -3
- package/template/apps/web/src/main.tsx +2 -5
- package/template/apps/web/src/modules/example/help/en-US/faq.md +27 -0
- package/template/apps/web/src/modules/example/help/en-US/items.md +30 -0
- package/template/apps/web/src/modules/example/help/en-US/overview.md +31 -0
- package/template/apps/web/src/modules/example/help/zh-CN/faq.md +27 -0
- package/template/apps/web/src/modules/example/help/zh-CN/items.md +31 -0
- package/template/apps/web/src/modules/example/help/zh-CN/overview.md +32 -0
- package/template/apps/web/src/modules/example/locales/en-US/example.json +99 -32
- package/template/apps/web/src/modules/example/locales/zh-CN/example.json +85 -18
- package/template/apps/web/src/modules/example/pages/ExampleItemsPage.tsx +840 -237
- package/template/apps/web/src/modules/example/services/exampleItems.ts +79 -8
- package/template/apps/web/src/modules/example/types.ts +14 -1
- package/template/apps/web/src/modules/index.ts +1 -0
- package/template/apps/web/vite.config.ts +9 -3
- package/template/docs/CONVENTIONS.md +10 -7
- package/template/package.json +4 -5
- package/template/pnpm-lock.yaml +76 -5
- package/template/scripts/build.bat +15 -3
- package/template/scripts/build.sh +9 -3
- package/template/scripts/check-help.js +249 -0
- package/template/scripts/compress-assets.js +89 -0
- package/template/scripts/test.bat +23 -0
- package/template/scripts/test.sh +16 -0
- package/template/.claude/skills/keystone-dev/SKILL.md +0 -103
- package/template/.claude/skills/keystone-dev/references/APPROVAL.md +0 -121
- package/template/.claude/skills/keystone-dev/references/CAPABILITIES.md +0 -261
- package/template/.claude/skills/keystone-dev/references/TEMPLATES.md +0 -532
- package/template/.claude/skills/keystone-dev/references/TESTING.md +0 -44
- package/template/.codex/skills/keystone-dev/SKILL.md +0 -103
- package/template/.codex/skills/keystone-dev/references/APPROVAL.md +0 -121
- package/template/.codex/skills/keystone-dev/references/CAPABILITIES.md +0 -261
- package/template/.codex/skills/keystone-dev/references/TEMPLATES.md +0 -532
- package/template/.codex/skills/keystone-dev/references/TESTING.md +0 -44
- package/template/apps/server/internal/app/routes/module_routes.go +0 -16
- package/template/apps/server/internal/app/routes/routes.go +0 -226
- package/template/apps/server/internal/app/startup/startup.go +0 -74
- package/template/apps/server/internal/frontend/handler.go +0 -122
- package/template/apps/server/internal/modules/registry.go +0 -145
- package/template/apps/web/src/modules/example/help/faq.md +0 -23
- package/template/apps/web/src/modules/example/help/items.md +0 -26
- package/template/apps/web/src/modules/example/help/overview.md +0 -25
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
package routes
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"log"
|
|
5
|
-
"time"
|
|
6
|
-
|
|
7
|
-
"github.com/gin-gonic/gin"
|
|
8
|
-
swaggerFiles "github.com/swaggo/files"
|
|
9
|
-
ginSwagger "github.com/swaggo/gin-swagger"
|
|
10
|
-
"gorm.io/gorm"
|
|
11
|
-
|
|
12
|
-
"github.com/robsuncn/keystone/api/handler/audit"
|
|
13
|
-
"github.com/robsuncn/keystone/api/handler/auth"
|
|
14
|
-
"github.com/robsuncn/keystone/api/handler/files"
|
|
15
|
-
"github.com/robsuncn/keystone/api/handler/health"
|
|
16
|
-
jobHandlers "github.com/robsuncn/keystone/api/handler/jobs"
|
|
17
|
-
"github.com/robsuncn/keystone/api/handler/notifications"
|
|
18
|
-
"github.com/robsuncn/keystone/api/handler/org"
|
|
19
|
-
"github.com/robsuncn/keystone/api/handler/rbac"
|
|
20
|
-
"github.com/robsuncn/keystone/api/handler/settings"
|
|
21
|
-
"github.com/robsuncn/keystone/api/handler/tenant"
|
|
22
|
-
"github.com/robsuncn/keystone/api/handler/user"
|
|
23
|
-
"github.com/robsuncn/keystone/api/middleware"
|
|
24
|
-
coreRoutes "github.com/robsuncn/keystone/api/routes"
|
|
25
|
-
"github.com/robsuncn/keystone/bootstrap/config"
|
|
26
|
-
approvalHandler "github.com/robsuncn/keystone/domain/approval/handler"
|
|
27
|
-
approvalRepo "github.com/robsuncn/keystone/domain/approval/repository"
|
|
28
|
-
approvalSvc "github.com/robsuncn/keystone/domain/approval/service"
|
|
29
|
-
"github.com/robsuncn/keystone/domain/permissions"
|
|
30
|
-
"github.com/robsuncn/keystone/domain/service"
|
|
31
|
-
jobInfra "github.com/robsuncn/keystone/infra/jobs"
|
|
32
|
-
"github.com/robsuncn/keystone/infra/queue"
|
|
33
|
-
"github.com/robsuncn/keystone/infra/repository"
|
|
34
|
-
"github.com/robsuncn/keystone/infra/storage"
|
|
35
|
-
|
|
36
|
-
"__APP_NAME__/apps/server/docs"
|
|
37
|
-
"__APP_NAME__/apps/server/internal/frontend"
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
// SetupRouter wires repositories, services, handlers and registers routes.
|
|
41
|
-
func SetupRouter(cfg *config.Config, db *gorm.DB, jobQueue queue.Queue) *gin.Engine {
|
|
42
|
-
r := gin.New()
|
|
43
|
-
r.Use(gin.Logger(), gin.Recovery())
|
|
44
|
-
|
|
45
|
-
if cfg != nil && cfg.Server.SwaggerEnabled {
|
|
46
|
-
docs.SwaggerInfo.BasePath = "/api/v1"
|
|
47
|
-
r.GET("/api/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
userRepo := repository.NewUserRepository(db)
|
|
51
|
-
userProfileRepo := repository.NewUserProfileRepository(db)
|
|
52
|
-
userInvitationRepo := repository.NewUserInvitationRepository(db)
|
|
53
|
-
platformUserRepo := repository.NewPlatformUserRepository(db)
|
|
54
|
-
roleRepo := repository.NewRoleRepository(db)
|
|
55
|
-
permRepo := repository.NewPermissionRepository(db)
|
|
56
|
-
dataScopeRepo := repository.NewDataScopePolicyRepository(db)
|
|
57
|
-
loginLogRepo := repository.NewLoginLogRepository(db)
|
|
58
|
-
blacklistRepo := repository.NewTokenBlacklistRepository(db)
|
|
59
|
-
auditLogRepo := repository.NewAuditLogRepository(db)
|
|
60
|
-
tenantRepo := repository.NewTenantRepository(db)
|
|
61
|
-
departmentRepo := repository.NewDepartmentRepository(db)
|
|
62
|
-
membershipRepo := repository.NewMembershipRepository(db)
|
|
63
|
-
settingRepo := repository.NewSettingRepository(db)
|
|
64
|
-
notificationRepo := repository.NewNotificationRepository(db)
|
|
65
|
-
jobRepo := repository.NewJobRepository(db)
|
|
66
|
-
|
|
67
|
-
approvalFlowRepo := approvalRepo.NewFlowRepository(db)
|
|
68
|
-
approvalNodeRepo := approvalRepo.NewNodeRepository(db)
|
|
69
|
-
approvalInstanceRepo := approvalRepo.NewInstanceRepository(db)
|
|
70
|
-
approvalRecordRepo := approvalRepo.NewRecordRepository(db)
|
|
71
|
-
|
|
72
|
-
permSvc := service.NewPermissionService(permRepo, userRepo)
|
|
73
|
-
platformPermSvc := service.NewPlatformPermissionService(platformUserRepo)
|
|
74
|
-
roleSvc := service.NewRoleService(roleRepo)
|
|
75
|
-
userSvc := service.NewUserService(userRepo, roleRepo, blacklistRepo)
|
|
76
|
-
tenantSvc := service.NewTenantService(db, tenantRepo, userRepo, roleRepo)
|
|
77
|
-
authSvc := service.NewAuthService(userRepo, roleRepo, blacklistRepo, loginLogRepo, cfg.JWT).
|
|
78
|
-
WithTenantRepository(tenantRepo)
|
|
79
|
-
platformAuthSvc := service.NewPlatformAuthService(platformUserRepo, blacklistRepo, loginLogRepo, cfg.JWT)
|
|
80
|
-
loginLogSvc := service.NewLoginLogService(loginLogRepo)
|
|
81
|
-
auditLogSvc := service.NewAuditLogService(db, auditLogRepo)
|
|
82
|
-
departmentSvc := service.NewDepartmentService(db, departmentRepo, membershipRepo)
|
|
83
|
-
membershipSvc := service.NewMembershipService(db, membershipRepo, departmentRepo)
|
|
84
|
-
profileSvc := service.NewUserProfileService(userRepo, userProfileRepo)
|
|
85
|
-
invitationSvc := service.NewUserInvitationService(userInvitationRepo)
|
|
86
|
-
settingSvc := service.NewSettingService(db, settingRepo)
|
|
87
|
-
prefixSvc := service.NewSettingPrefixService(settingSvc)
|
|
88
|
-
notificationSvc := service.NewNotificationService(db, notificationRepo)
|
|
89
|
-
jobSvc := service.NewJobService(db, jobRepo)
|
|
90
|
-
|
|
91
|
-
policySvc := permissions.NewPolicyService(dataScopeRepo)
|
|
92
|
-
evaluator := permissions.NewEvaluator(departmentSvc)
|
|
93
|
-
enforcer := permissions.NewEnforcer(policySvc, evaluator, userRepo)
|
|
94
|
-
|
|
95
|
-
approvalService := approvalSvc.NewService(
|
|
96
|
-
db,
|
|
97
|
-
approvalInstanceRepo,
|
|
98
|
-
approvalFlowRepo,
|
|
99
|
-
approvalNodeRepo,
|
|
100
|
-
approvalRecordRepo,
|
|
101
|
-
userRepo,
|
|
102
|
-
permSvc,
|
|
103
|
-
nil,
|
|
104
|
-
)
|
|
105
|
-
auditRecorder := service.NewAuditLogRecorder(auditLogSvc, userRepo)
|
|
106
|
-
approvalService.WithAuditRecorder(approvalSvc.NewAuditRecorderAdapter(auditRecorder))
|
|
107
|
-
|
|
108
|
-
healthHandler := health.NewHealthHandler(db, nil)
|
|
109
|
-
authHandler := auth.NewAuthHandler(authSvc)
|
|
110
|
-
platformAuthHandler := auth.NewPlatformAuthHandler(platformAuthSvc)
|
|
111
|
-
tenantHandler := tenant.NewTenantHandler(tenantSvc)
|
|
112
|
-
userHandler := user.NewUserHandler(userSvc, departmentSvc, invitationSvc, profileSvc)
|
|
113
|
-
roleHandler := rbac.NewRoleHandler(roleSvc, permSvc, policySvc)
|
|
114
|
-
permissionHandler := rbac.NewPermissionHandler(permSvc)
|
|
115
|
-
loginLogHandler := audit.NewLoginLogHandler(loginLogSvc)
|
|
116
|
-
auditLogHandler := audit.NewAuditLogHandler(auditLogSvc)
|
|
117
|
-
departmentHandler := org.NewDepartmentHandler(departmentSvc)
|
|
118
|
-
membershipHandler := org.NewMembershipHandler(membershipSvc)
|
|
119
|
-
systemSettingsHandler := settings.NewSystemSettingsHandler(settingSvc).WithPrefixSettings(prefixSvc)
|
|
120
|
-
notificationHandler := notifications.NewNotificationHandler(notificationSvc)
|
|
121
|
-
jobHandler := jobHandlers.NewJobHandler(jobSvc)
|
|
122
|
-
|
|
123
|
-
approvalSchemaHandler := approvalHandler.NewSchemaHandler()
|
|
124
|
-
approvalFlowHandler := approvalHandler.NewFlowHandler(db, approvalFlowRepo, approvalNodeRepo, approvalInstanceRepo)
|
|
125
|
-
approvalNodeHandler := approvalHandler.NewNodeHandler(approvalNodeRepo, approvalFlowRepo)
|
|
126
|
-
approvalInstanceHandler := approvalHandler.NewInstanceHandler(
|
|
127
|
-
approvalService,
|
|
128
|
-
approvalInstanceRepo,
|
|
129
|
-
approvalRecordRepo,
|
|
130
|
-
approvalFlowRepo,
|
|
131
|
-
approvalNodeRepo,
|
|
132
|
-
userRepo,
|
|
133
|
-
enforcer,
|
|
134
|
-
nil,
|
|
135
|
-
)
|
|
136
|
-
approvalWorkbenchHandler := approvalHandler.NewWorkbenchHandler(
|
|
137
|
-
approvalService,
|
|
138
|
-
approvalInstanceRepo,
|
|
139
|
-
approvalRecordRepo,
|
|
140
|
-
approvalFlowRepo,
|
|
141
|
-
userRepo,
|
|
142
|
-
enforcer,
|
|
143
|
-
nil,
|
|
144
|
-
)
|
|
145
|
-
approvalRecordHandler := approvalHandler.NewRecordHandler(
|
|
146
|
-
approvalRecordRepo,
|
|
147
|
-
approvalFlowRepo,
|
|
148
|
-
userRepo,
|
|
149
|
-
enforcer,
|
|
150
|
-
nil,
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
var fileHandler *files.FileHandler
|
|
154
|
-
storageSvc, err := storage.New(cfg.Storage)
|
|
155
|
-
if err != nil {
|
|
156
|
-
log.Printf("storage init failed: %v", err)
|
|
157
|
-
} else {
|
|
158
|
-
fileHandler = files.NewFileHandler(storageSvc)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
var dispatcher *jobInfra.Dispatcher
|
|
162
|
-
var importHandler *jobHandlers.ImportHandler
|
|
163
|
-
var exportHandler *jobHandlers.ExportHandler
|
|
164
|
-
if jobQueue != nil {
|
|
165
|
-
dispatcher = jobInfra.NewDispatcher(jobQueue, jobSvc)
|
|
166
|
-
}
|
|
167
|
-
if dispatcher != nil {
|
|
168
|
-
importHandler = jobHandlers.NewImportHandler(dispatcher)
|
|
169
|
-
exportHandler = jobHandlers.NewExportHandler(dispatcher)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
authMiddleware := middleware.NewAuthMiddleware(cfg.JWT, blacklistRepo, userRepo)
|
|
173
|
-
rbacMiddleware := middleware.NewRBACMiddleware(permSvc)
|
|
174
|
-
platformAuthMiddleware := middleware.NewPlatformAuthMiddleware(cfg.JWT, blacklistRepo, platformUserRepo)
|
|
175
|
-
platformRBACMiddleware := middleware.NewRBACMiddleware(platformPermSvc)
|
|
176
|
-
tenantGuards := []gin.HandlerFunc{
|
|
177
|
-
middleware.RequireTenant(),
|
|
178
|
-
middleware.NewTenantStatusMiddleware(tenantSvc),
|
|
179
|
-
middleware.NewTenantRLSMiddleware(db),
|
|
180
|
-
}
|
|
181
|
-
authRateLimiter := middleware.NewRateLimiter(20, time.Minute)
|
|
182
|
-
|
|
183
|
-
v1 := r.Group("/api/v1")
|
|
184
|
-
coreRoutes.RegisterCoreRoutes(v1, coreRoutes.CoreRouteDeps{
|
|
185
|
-
HealthHandler: healthHandler,
|
|
186
|
-
AuthMiddleware: authMiddleware,
|
|
187
|
-
PlatformAuth: platformAuthMiddleware,
|
|
188
|
-
PlatformRBAC: platformRBACMiddleware,
|
|
189
|
-
TenantGuards: tenantGuards,
|
|
190
|
-
RBAC: rbacMiddleware,
|
|
191
|
-
AuthHandler: authHandler,
|
|
192
|
-
PlatformAuthHandler: platformAuthHandler,
|
|
193
|
-
TenantHandler: tenantHandler,
|
|
194
|
-
AuthRateLimiter: authRateLimiter,
|
|
195
|
-
UserHandler: userHandler,
|
|
196
|
-
RoleHandler: roleHandler,
|
|
197
|
-
PermissionHandler: permissionHandler,
|
|
198
|
-
LoginLogHandler: loginLogHandler,
|
|
199
|
-
AuditLogHandler: auditLogHandler,
|
|
200
|
-
FileHandler: fileHandler,
|
|
201
|
-
DepartmentHandler: departmentHandler,
|
|
202
|
-
MembershipHandler: membershipHandler,
|
|
203
|
-
ApprovalSchema: approvalSchemaHandler,
|
|
204
|
-
ApprovalFlow: approvalFlowHandler,
|
|
205
|
-
ApprovalNode: approvalNodeHandler,
|
|
206
|
-
ApprovalInstance: approvalInstanceHandler,
|
|
207
|
-
ApprovalWorkbench: approvalWorkbenchHandler,
|
|
208
|
-
ApprovalRecord: approvalRecordHandler,
|
|
209
|
-
NotificationHandler: notificationHandler,
|
|
210
|
-
JobHandler: jobHandler,
|
|
211
|
-
ImportHandler: importHandler,
|
|
212
|
-
ExportHandler: exportHandler,
|
|
213
|
-
SystemSettingsHandler: systemSettingsHandler,
|
|
214
|
-
})
|
|
215
|
-
registerModuleRoutes(v1, cfg)
|
|
216
|
-
|
|
217
|
-
spaHandler, err := frontend.NewSPAHandler()
|
|
218
|
-
if err == nil {
|
|
219
|
-
r.NoRoute(spaHandler.GinHandler())
|
|
220
|
-
log.Println("Frontend: embedded SPA handler enabled")
|
|
221
|
-
} else if cfg != nil && cfg.Server.Mode == "release" {
|
|
222
|
-
log.Printf("Warning: frontend not available: %v", err)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return r
|
|
226
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
package startup
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"fmt"
|
|
5
|
-
"log"
|
|
6
|
-
"time"
|
|
7
|
-
|
|
8
|
-
"gorm.io/gorm"
|
|
9
|
-
|
|
10
|
-
"github.com/robsuncn/keystone/bootstrap/config"
|
|
11
|
-
"github.com/robsuncn/keystone/bootstrap/migrations"
|
|
12
|
-
"github.com/robsuncn/keystone/bootstrap/seeds"
|
|
13
|
-
"github.com/robsuncn/keystone/domain/permissions"
|
|
14
|
-
|
|
15
|
-
"__APP_NAME__/apps/server/internal/modules"
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
// InitializeDatabase runs base + module migrations and seeds.
|
|
19
|
-
func InitializeDatabase(cfg *config.Config) (*gorm.DB, error) {
|
|
20
|
-
if cfg == nil {
|
|
21
|
-
return nil, fmt.Errorf("config is nil")
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
dbCfg := &cfg.Database
|
|
25
|
-
start := time.Now()
|
|
26
|
-
log.Println("========================================")
|
|
27
|
-
log.Println("Keystone Database Initialization")
|
|
28
|
-
log.Println("========================================")
|
|
29
|
-
|
|
30
|
-
log.Printf("[startup] Connecting to database (driver: %s)...", dbCfg.Driver)
|
|
31
|
-
db, err := config.InitDB(dbCfg, cfg.Server.Mode)
|
|
32
|
-
if err != nil {
|
|
33
|
-
return nil, fmt.Errorf("database connection failed: %w", err)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
log.Println("[startup] Phase 1: Auth model migration...")
|
|
37
|
-
if err := config.MigrateDB(db, config.AuthModels()...); err != nil {
|
|
38
|
-
return nil, fmt.Errorf("auth model migration failed: %w", err)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
log.Println("[startup] Phase 2: Platform schema migrations...")
|
|
42
|
-
if err := migrations.RunAll(db); err != nil {
|
|
43
|
-
return nil, fmt.Errorf("platform migrations failed: %w", err)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
log.Println("[startup] Phase 3: Module schema migrations...")
|
|
47
|
-
if err := modules.MigrateEnabled(cfg.Modules.Enabled, db); err != nil {
|
|
48
|
-
return nil, fmt.Errorf("module migrations failed: %w", err)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
log.Println("[startup] Phase 4: Data seeding...")
|
|
52
|
-
if err := seeds.RunAll(db, seeds.Options{
|
|
53
|
-
RegisterPermissions: func(reg *permissions.Registry) error {
|
|
54
|
-
return modules.RegisterPermissions(cfg.Modules.Enabled, reg)
|
|
55
|
-
},
|
|
56
|
-
}); err != nil {
|
|
57
|
-
return nil, fmt.Errorf("data seeding failed: %w", err)
|
|
58
|
-
}
|
|
59
|
-
if err := modules.SeedEnabled(cfg.Modules.Enabled, db); err != nil {
|
|
60
|
-
return nil, fmt.Errorf("module seeding failed: %w", err)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
log.Println("[startup] Phase 5: Security policies...")
|
|
64
|
-
if err := migrations.MigrateTenantRLS(db); err != nil {
|
|
65
|
-
return nil, fmt.Errorf("RLS setup failed: %w", err)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
elapsed := time.Since(start).Round(time.Millisecond)
|
|
69
|
-
log.Println("========================================")
|
|
70
|
-
log.Printf("Database initialization complete (%v)", elapsed)
|
|
71
|
-
log.Println("========================================")
|
|
72
|
-
|
|
73
|
-
return db, nil
|
|
74
|
-
}
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
package frontend
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"io"
|
|
5
|
-
"net/http"
|
|
6
|
-
"path"
|
|
7
|
-
"strings"
|
|
8
|
-
|
|
9
|
-
"github.com/gin-gonic/gin"
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
// SPAHandler serves the embedded frontend files with SPA route fallback.
|
|
13
|
-
type SPAHandler struct {
|
|
14
|
-
fs http.FileSystem
|
|
15
|
-
fileServer http.Handler
|
|
16
|
-
indexHTML []byte
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// NewSPAHandler creates a new SPA handler with the embedded file system.
|
|
20
|
-
func NewSPAHandler() (*SPAHandler, error) {
|
|
21
|
-
fsys, err := GetFileSystem()
|
|
22
|
-
if err != nil {
|
|
23
|
-
return nil, err
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
handler := &SPAHandler{
|
|
27
|
-
fs: fsys,
|
|
28
|
-
fileServer: http.FileServer(fsys),
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if err := handler.loadIndex(); err != nil {
|
|
32
|
-
return nil, err
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return handler, nil
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
func (h *SPAHandler) loadIndex() error {
|
|
39
|
-
file, err := h.fs.Open("/index.html")
|
|
40
|
-
if err != nil {
|
|
41
|
-
return err
|
|
42
|
-
}
|
|
43
|
-
defer file.Close()
|
|
44
|
-
|
|
45
|
-
h.indexHTML, err = io.ReadAll(file)
|
|
46
|
-
if err != nil {
|
|
47
|
-
return err
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return nil
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ServeHTTP implements http.Handler for the SPA.
|
|
54
|
-
func (h *SPAHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
55
|
-
urlPath := path.Clean(r.URL.Path)
|
|
56
|
-
|
|
57
|
-
if urlPath == "/" || urlPath == "" {
|
|
58
|
-
h.serveIndex(w)
|
|
59
|
-
return
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
file, err := h.fs.Open(urlPath)
|
|
63
|
-
if err != nil {
|
|
64
|
-
h.serveIndex(w)
|
|
65
|
-
return
|
|
66
|
-
}
|
|
67
|
-
defer file.Close()
|
|
68
|
-
|
|
69
|
-
stat, err := file.Stat()
|
|
70
|
-
if err != nil || stat.IsDir() {
|
|
71
|
-
h.serveIndex(w)
|
|
72
|
-
return
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
h.setContentType(w, urlPath)
|
|
76
|
-
h.fileServer.ServeHTTP(w, r)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
func (h *SPAHandler) serveIndex(w http.ResponseWriter) {
|
|
80
|
-
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
81
|
-
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
82
|
-
w.WriteHeader(http.StatusOK)
|
|
83
|
-
w.Write(h.indexHTML)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
func (h *SPAHandler) setContentType(w http.ResponseWriter, filePath string) {
|
|
87
|
-
ext := strings.ToLower(path.Ext(filePath))
|
|
88
|
-
contentTypes := map[string]string{
|
|
89
|
-
".html": "text/html; charset=utf-8",
|
|
90
|
-
".js": "application/javascript; charset=utf-8",
|
|
91
|
-
".mjs": "application/javascript; charset=utf-8",
|
|
92
|
-
".css": "text/css; charset=utf-8",
|
|
93
|
-
".json": "application/json; charset=utf-8",
|
|
94
|
-
".svg": "image/svg+xml",
|
|
95
|
-
".png": "image/png",
|
|
96
|
-
".jpg": "image/jpeg",
|
|
97
|
-
".jpeg": "image/jpeg",
|
|
98
|
-
".gif": "image/gif",
|
|
99
|
-
".webp": "image/webp",
|
|
100
|
-
".ico": "image/x-icon",
|
|
101
|
-
".woff": "font/woff",
|
|
102
|
-
".woff2": "font/woff2",
|
|
103
|
-
".ttf": "font/ttf",
|
|
104
|
-
".eot": "application/vnd.ms-fontobject",
|
|
105
|
-
".map": "application/json",
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if ct, ok := contentTypes[ext]; ok {
|
|
109
|
-
w.Header().Set("Content-Type", ct)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if strings.Contains(filePath, "/assets/") {
|
|
113
|
-
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// GinHandler returns a gin.HandlerFunc for router.NoRoute().
|
|
118
|
-
func (h *SPAHandler) GinHandler() gin.HandlerFunc {
|
|
119
|
-
return func(c *gin.Context) {
|
|
120
|
-
h.ServeHTTP(c.Writer, c.Request)
|
|
121
|
-
}
|
|
122
|
-
}
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
package modules
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"sync"
|
|
5
|
-
|
|
6
|
-
"github.com/gin-gonic/gin"
|
|
7
|
-
"gorm.io/gorm"
|
|
8
|
-
|
|
9
|
-
"github.com/robsuncn/keystone/domain/permissions"
|
|
10
|
-
"github.com/robsuncn/keystone/infra/jobs"
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
// Module registers routes and lifecycle hooks for a business module.
|
|
14
|
-
type Module interface {
|
|
15
|
-
Name() string
|
|
16
|
-
RegisterRoutes(rg *gin.RouterGroup)
|
|
17
|
-
RegisterModels() []interface{}
|
|
18
|
-
RegisterPermissions(registry *permissions.Registry) error
|
|
19
|
-
RegisterJobs(registry *jobs.Registry) error
|
|
20
|
-
Migrate(db *gorm.DB) error
|
|
21
|
-
Seed(db *gorm.DB) error
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
var (
|
|
25
|
-
registryMu sync.RWMutex
|
|
26
|
-
registry = make(map[string]Module)
|
|
27
|
-
order []string
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
// Register adds or replaces a module by name.
|
|
31
|
-
func Register(module Module) {
|
|
32
|
-
if module == nil {
|
|
33
|
-
return
|
|
34
|
-
}
|
|
35
|
-
name := module.Name()
|
|
36
|
-
if name == "" {
|
|
37
|
-
return
|
|
38
|
-
}
|
|
39
|
-
registryMu.Lock()
|
|
40
|
-
defer registryMu.Unlock()
|
|
41
|
-
if _, ok := registry[name]; !ok {
|
|
42
|
-
order = append(order, name)
|
|
43
|
-
}
|
|
44
|
-
registry[name] = module
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Clear removes all registered modules.
|
|
48
|
-
func Clear() {
|
|
49
|
-
registryMu.Lock()
|
|
50
|
-
defer registryMu.Unlock()
|
|
51
|
-
registry = make(map[string]Module)
|
|
52
|
-
order = nil
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Registered returns a snapshot of the current modules.
|
|
56
|
-
func Registered() []Module {
|
|
57
|
-
registryMu.RLock()
|
|
58
|
-
defer registryMu.RUnlock()
|
|
59
|
-
out := make([]Module, 0, len(order))
|
|
60
|
-
for _, name := range order {
|
|
61
|
-
if module, ok := registry[name]; ok {
|
|
62
|
-
out = append(out, module)
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return out
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
func enabledModules(enabled []string) []Module {
|
|
69
|
-
if len(enabled) == 0 {
|
|
70
|
-
return nil
|
|
71
|
-
}
|
|
72
|
-
registryMu.RLock()
|
|
73
|
-
defer registryMu.RUnlock()
|
|
74
|
-
out := make([]Module, 0, len(enabled))
|
|
75
|
-
for _, name := range enabled {
|
|
76
|
-
module, ok := registry[name]
|
|
77
|
-
if !ok || module == nil {
|
|
78
|
-
continue
|
|
79
|
-
}
|
|
80
|
-
out = append(out, module)
|
|
81
|
-
}
|
|
82
|
-
return out
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// LoadEnabled registers enabled module routes.
|
|
86
|
-
func LoadEnabled(enabled []string, rg *gin.RouterGroup) {
|
|
87
|
-
if len(enabled) == 0 || rg == nil {
|
|
88
|
-
return
|
|
89
|
-
}
|
|
90
|
-
for _, module := range enabledModules(enabled) {
|
|
91
|
-
module.RegisterRoutes(rg)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// RegisterPermissions registers permission definitions for enabled modules.
|
|
96
|
-
func RegisterPermissions(enabled []string, reg *permissions.Registry) error {
|
|
97
|
-
if len(enabled) == 0 || reg == nil {
|
|
98
|
-
return nil
|
|
99
|
-
}
|
|
100
|
-
for _, module := range enabledModules(enabled) {
|
|
101
|
-
if err := module.RegisterPermissions(reg); err != nil {
|
|
102
|
-
return err
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return nil
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// RegisterJobs registers job handlers for enabled modules.
|
|
109
|
-
func RegisterJobs(enabled []string, reg *jobs.Registry) error {
|
|
110
|
-
if len(enabled) == 0 || reg == nil {
|
|
111
|
-
return nil
|
|
112
|
-
}
|
|
113
|
-
for _, module := range enabledModules(enabled) {
|
|
114
|
-
if err := module.RegisterJobs(reg); err != nil {
|
|
115
|
-
return err
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return nil
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// MigrateEnabled runs migrations for enabled modules.
|
|
122
|
-
func MigrateEnabled(enabled []string, db *gorm.DB) error {
|
|
123
|
-
if len(enabled) == 0 || db == nil {
|
|
124
|
-
return nil
|
|
125
|
-
}
|
|
126
|
-
for _, module := range enabledModules(enabled) {
|
|
127
|
-
if err := module.Migrate(db); err != nil {
|
|
128
|
-
return err
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return nil
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// SeedEnabled runs seed data setup for enabled modules.
|
|
135
|
-
func SeedEnabled(enabled []string, db *gorm.DB) error {
|
|
136
|
-
if len(enabled) == 0 || db == nil {
|
|
137
|
-
return nil
|
|
138
|
-
}
|
|
139
|
-
for _, module := range enabledModules(enabled) {
|
|
140
|
-
if err := module.Seed(db); err != nil {
|
|
141
|
-
return err
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return nil
|
|
145
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
helpKey: "example/faq"
|
|
3
|
-
title: "Example Module FAQ"
|
|
4
|
-
description: "Common questions for the Example module."
|
|
5
|
-
category: "example"
|
|
6
|
-
tags: ["example", "faq"]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# FAQ
|
|
10
|
-
|
|
11
|
-
## 为什么列表为空?
|
|
12
|
-
- 确认已运行迁移与种子(首次启动会自动执行)。
|
|
13
|
-
- 检查当前用户是否有 `example:item:view` 权限。
|
|
14
|
-
- 确认 `modules.enabled` 中已启用 `example`。
|
|
15
|
-
|
|
16
|
-
## 为什么保存失败?
|
|
17
|
-
- Title 必填,且不能为空字符串。
|
|
18
|
-
- 状态仅支持 `active` / `inactive`。
|
|
19
|
-
- 查看后端日志获取更详细错误信息。
|
|
20
|
-
|
|
21
|
-
## 为什么前后端权限不一致?
|
|
22
|
-
- 检查 `routes.tsx` 中的 `handle.permission`。
|
|
23
|
-
- 检查 `RegisterPermissions` 中的权限注册是否一致。
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
helpKey: "example/items"
|
|
3
|
-
title: "Example Items"
|
|
4
|
-
description: "Manage example items with full CRUD operations."
|
|
5
|
-
category: "example"
|
|
6
|
-
tags: ["example", "items", "crud"]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Example Items
|
|
10
|
-
|
|
11
|
-
该页面展示 Example 模块的完整 CRUD 流程,适合用来对照学习:
|
|
12
|
-
|
|
13
|
-
## 字段说明
|
|
14
|
-
- **Title**:必填,标题信息。
|
|
15
|
-
- **Description**:可选,简短描述。
|
|
16
|
-
- **Status**:`active` / `inactive`。
|
|
17
|
-
|
|
18
|
-
## 权限建议
|
|
19
|
-
- `example:item:view`:访问列表与详情。
|
|
20
|
-
- `example:item:manage`:创建、编辑、删除。
|
|
21
|
-
|
|
22
|
-
## API 端点
|
|
23
|
-
- `GET /api/v1/example/items`
|
|
24
|
-
- `POST /api/v1/example/items`
|
|
25
|
-
- `PATCH /api/v1/example/items/:id`
|
|
26
|
-
- `DELETE /api/v1/example/items/:id`
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
helpKey: "example/overview"
|
|
3
|
-
title: "Example Module Overview"
|
|
4
|
-
description: "Overview of the Example module and its full CRUD flow."
|
|
5
|
-
category: "example"
|
|
6
|
-
tags: ["example", "items", "crud"]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Example Module
|
|
10
|
-
|
|
11
|
-
Example 模块演示完整的 CRUD、权限命名与前后端分层结构,适合作为新模块开发参考。
|
|
12
|
-
|
|
13
|
-
## 你能看到什么
|
|
14
|
-
- 前端模块注册(`index.ts`)与路由配置(`routes.tsx`)
|
|
15
|
-
- CRUD 页面与 API 调用(`pages/` + `services/`)
|
|
16
|
-
- 后端 DDD 分层(`domain/` + `infra/` + `bootstrap/`)
|
|
17
|
-
- 权限注册与菜单同步(`RegisterPermissions`)
|
|
18
|
-
|
|
19
|
-
## 关键文件
|
|
20
|
-
- 前端:`apps/web/src/modules/example/`
|
|
21
|
-
- 后端:`apps/server/internal/modules/example/`
|
|
22
|
-
|
|
23
|
-
## API 与权限
|
|
24
|
-
- API:`/api/v1/example/items`
|
|
25
|
-
- 权限:`example:item:view`、`example:item:manage`
|