mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 06:16:37 +08:00
455e2f8be5
Public config now comes from admin as a flat visibility=1 map instead of a cross-plugin query that compared an integer column to "visible". Login and register resolve CaptchaService per request so CAP is not skipped when user applies before cap.
207 lines
6.0 KiB
Go
207 lines
6.0 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// Package user provides the user profile, credential management, role management, and access token domain plugin for Cordis.
|
|
package user
|
|
|
|
import (
|
|
"Wavelet/core"
|
|
"Wavelet/core/contracts"
|
|
"Wavelet/core/extpoints"
|
|
"Wavelet/pkg/ginutil"
|
|
"context"
|
|
"embed"
|
|
"reflect"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
//go:embed migrations/*/*.sql
|
|
var userMigrations embed.FS
|
|
|
|
// Option configures the user plugin.
|
|
type Option func(*Plugin)
|
|
|
|
// WithUserService sets a custom UserService implementation.
|
|
func WithUserService(svc contracts.UserService) Option {
|
|
return func(p *Plugin) {
|
|
p.userSvc = svc
|
|
}
|
|
}
|
|
|
|
// Plugin implements core.Plugin to provide user account and credential domain services.
|
|
type Plugin struct {
|
|
userSvc contracts.UserService
|
|
}
|
|
|
|
// New creates a new user domain plugin.
|
|
func New(opts ...Option) *Plugin {
|
|
p := &Plugin{}
|
|
for _, opt := range opts {
|
|
if opt != nil {
|
|
opt(p)
|
|
}
|
|
}
|
|
return p
|
|
}
|
|
|
|
// PluginName 用户插件唯一名称标识
|
|
const PluginName = "user"
|
|
|
|
// Name returns the unique identifier for the user domain plugin.
|
|
func (p *Plugin) Name() string {
|
|
return PluginName
|
|
}
|
|
|
|
// Inject declares required dependencies for the user domain plugin.
|
|
func (p *Plugin) Inject() []reflect.Type {
|
|
return []reflect.Type{
|
|
reflect.TypeFor[contracts.DBService](),
|
|
// Apply resolves AuthService to build the route auth middleware. The
|
|
// kernel only gates Apply on declared deps, so leaving this out lets
|
|
// user mount before auth and fall back to a pass-through middleware.
|
|
reflect.TypeFor[contracts.AuthService](),
|
|
}
|
|
}
|
|
|
|
// Manifest returns the plugin metadata.
|
|
func (p *Plugin) Manifest() core.Manifest {
|
|
return core.Manifest{
|
|
Name: PluginName,
|
|
Version: "1.0.0",
|
|
Description: "User profiles, credentials, role management, and access token domain plugin",
|
|
Author: "Wavelet Team",
|
|
}
|
|
}
|
|
|
|
// Apply registers user migrations, services, routes, tasks, schedules, and settings into the Context.
|
|
func (p *Plugin) Apply(ctx *core.Context) error {
|
|
core.Bind[contracts.DBService](ctx, SetDBService)
|
|
core.Bind[contracts.CacheService](ctx, SetCacheService)
|
|
core.Bind[contracts.TaskService](ctx, SetTaskService)
|
|
ctx.OnDispose(func() error {
|
|
SetDBService(nil)
|
|
SetCacheService(nil)
|
|
SetTaskService(nil)
|
|
return nil
|
|
})
|
|
|
|
// 0.1 Resolve auth service for middleware and cache invalidation (via IoC, not direct import)
|
|
denyAuth := ginutil.AuthUnavailable()
|
|
loginMW := denyAuth
|
|
noTokenMW := denyAuth
|
|
if authSvc, err := core.Inject[contracts.AuthService](ctx); err == nil && authSvc != nil {
|
|
SetAuthService(authSvc)
|
|
if mw, ok := authSvc.RequireAuthMiddleware().(gin.HandlerFunc); ok {
|
|
loginMW = mw
|
|
}
|
|
if mw, ok := authSvc.DisallowTokenAuthMiddleware().(gin.HandlerFunc); ok {
|
|
noTokenMW = mw
|
|
}
|
|
}
|
|
core.Bind[contracts.AuthService](ctx, SetAuthService)
|
|
ctx.OnDispose(func() error {
|
|
SetAuthService(nil)
|
|
return nil
|
|
})
|
|
|
|
// 1. Register migrations
|
|
ctx.Migrations().Register("user", userMigrations)
|
|
|
|
// 2. Initialize and provide UserService
|
|
if p.userSvc == nil {
|
|
p.userSvc = newUserService(ctx.Events())
|
|
}
|
|
core.Provide[contracts.UserService](ctx, p.userSvc)
|
|
|
|
// CAP middleware is resolved per request. user Apply runs before cap in
|
|
// the default plugin list; snapshotting CaptchaService here would leave
|
|
// login/register as a permanent pass-through.
|
|
loginCap := captchaGuard(ctx, "login")
|
|
registerCap := captchaGuard(ctx, "register")
|
|
emailCap := captchaGuard(ctx, "send_email_code")
|
|
|
|
// 3. Register HTTP Routes
|
|
userGroup := ctx.Router().Group("/api/v1/user")
|
|
{
|
|
userGroup.POST("/login", loginCap, Login)
|
|
userGroup.POST("/register", registerCap, Register)
|
|
userGroup.GET("/logout", Logout)
|
|
userGroup.POST("/send-email-code", emailCap, SendEmailCode)
|
|
userGroup.POST("/change-password", loginMW, ChangePassword)
|
|
userGroup.GET("/self", loginMW, Self)
|
|
userGroup.PUT("/profile", loginMW, UpdateProfile)
|
|
|
|
// Access Tokens
|
|
tokensGroup := userGroup.Group("/access-tokens", loginMW, noTokenMW)
|
|
{
|
|
tokensGroup.GET("", ListAccessTokens)
|
|
tokensGroup.POST("", CreateAccessToken)
|
|
tokensGroup.DELETE("/:id", DeleteAccessToken)
|
|
tokensGroup.POST("/:id/rotate", RotateAccessToken)
|
|
}
|
|
}
|
|
|
|
ctx.Task().Register(TaskSendEmailCode, &SendEmailCodeHandler{},
|
|
extpoints.WithTaskMeta(SendEmailCodeMeta), extpoints.WithTaskRetry(defaultUserTaskRetry))
|
|
ctx.Task().Register(TaskSendMail, &SendMailHandler{},
|
|
extpoints.WithTaskMeta(SendMailMeta), extpoints.WithTaskRetry(defaultUserTaskRetry))
|
|
ctx.Task().Register(TaskCleanupInactive, &CleanupInactiveHandler{},
|
|
extpoints.WithTaskMeta(CleanupInactiveMeta))
|
|
|
|
// 5. Register Settings Schemas
|
|
ctx.Settings().Register(extpoints.SettingSchema{
|
|
Key: "user.registration_enabled",
|
|
Default: true,
|
|
Description: "Whether new user registration is enabled",
|
|
Type: "boolean",
|
|
Category: "general",
|
|
Public: true,
|
|
})
|
|
ctx.Settings().Register(extpoints.SettingSchema{
|
|
Key: "user.password_login_enabled",
|
|
Default: true,
|
|
Description: "Whether password login is enabled",
|
|
Type: "boolean",
|
|
Category: "general",
|
|
Public: true,
|
|
})
|
|
ctx.Settings().Register(extpoints.SettingSchema{
|
|
Key: "user.min_password_length",
|
|
Default: 8,
|
|
Description: "Minimum password length required for user accounts",
|
|
Type: "integer",
|
|
Category: "security",
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func captchaGuard(appCtx *core.Context, scope string) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
svc := resolveCaptchaService(c.Request.Context(), appCtx)
|
|
if svc == nil {
|
|
c.Next()
|
|
return
|
|
}
|
|
mw, ok := svc.VerifyMiddleware(scope).(gin.HandlerFunc)
|
|
if !ok || mw == nil {
|
|
c.Next()
|
|
return
|
|
}
|
|
mw(c)
|
|
}
|
|
}
|
|
|
|
func resolveCaptchaService(reqCtx context.Context, appCtx *core.Context) contracts.CaptchaService {
|
|
if s, err := core.InjectFrom[contracts.CaptchaService](reqCtx); err == nil && s != nil {
|
|
return s
|
|
}
|
|
if appCtx != nil {
|
|
if s, err := core.Inject[contracts.CaptchaService](appCtx); err == nil && s != nil {
|
|
return s
|
|
}
|
|
}
|
|
return nil
|
|
}
|