feat(auth): 登录注册 - 添加短信通知与组合验证支持

This commit is contained in:
ddCat committed 2026-09-22 23:46:15 +08:00
1 parent 6c013ebf7e
commit bc0d109428
55 files changed
+4013 -103

No files matched your search

+3 -1
View File
@@ -117,7 +117,9 @@ func (s *Service) authDomain() *auth.Service {
if s.auth != nil {
return s.auth
}
return auth.New(s.repo, authHost{svc: s}, nil)
domain := auth.New(s.repo, authHost{svc: s}, nil)
domain.SetSMSDelivery(s.smsDomain())
return domain
}
func (s *Service) PublicAuthSettings() (*PublicAuthSettings, error) {
@@ -0,0 +1,88 @@
package app
import (
"context"
"crypto/rand"
"fmt"
"infinite-canvas/backend/internal/auth"
"infinite-canvas/backend/internal/model"
"infinite-canvas/backend/internal/sms"
"math/big"
)
type SMSChannelRequest = sms.ChannelRequest
type VerificationPolicy = auth.VerificationPolicy
type VerificationRequest = auth.VerificationRequest
type VerificationConfirm = auth.VerificationConfirm
func (s *Service) newSMSDomain() *sms.Service {
return sms.New(s.repo, authHost{svc: s}, func(id string) (bool, error) {
items := s.Plugins()
runtime, exists := runtimePluginByID(items, id)
if !exists || runtime.Status != "enabled" {
return false, nil
}
state, err := s.pluginStateForUser(nil, id, items)
return state.EffectiveEnabled, err
})
}
func (s *Service) smsDomain() *sms.Service {
if s.sms != nil {
return s.sms
}
return s.newSMSDomain()
}
func (s *Service) AdminSMSChannels(actor *model.User) ([]sms.ChannelView, error) {
return s.smsDomain().Channels(actor)
}
func (s *Service) SaveSMSChannel(actor *model.User, id string, req SMSChannelRequest) (*sms.ChannelView, error) {
result, err := s.smsDomain().SaveChannel(actor, id, req)
if err != nil {
return nil, err
}
if err := s.appendAdminAudit(actor, "sms.channel.save", "sms_channel", result.ID, "保存短信渠道", map[string]any{"provider": result.Provider, "enabled": result.Enabled}); err != nil {
return nil, err
}
return result, nil
}
func (s *Service) DeleteSMSChannel(actor *model.User, id string) error {
if err := s.smsDomain().DeleteChannel(actor, id); err != nil {
return err
}
return s.appendAdminAudit(actor, "sms.channel.delete", "sms_channel", id, "删除短信渠道", nil)
}
func (s *Service) AdminSMSRecords(actor *model.User, channel, state string, page, size int) (*sms.RecordPage, error) {
return s.smsDomain().Records(actor, channel, state, page, size)
}
func (s *Service) TestSMSChannel(actor *model.User, ctx context.Context, id, purpose, phone string) (*model.SMSRecord, error) {
if err := s.RequireAdmin(actor); err != nil {
return nil, err
}
value, err := rand.Int(rand.Reader, big.NewInt(1000000))
if err != nil {
return nil, err
}
return s.smsDomain().Test(actor, ctx, id, purpose, phone, fmt.Sprintf("%06d", value.Int64()))
}
func (s *Service) AdminVerificationPolicy(actor *model.User) (*VerificationPolicy, error) {
return s.authDomain().AdminVerificationPolicy(actor)
}
func (s *Service) UpdateVerificationPolicy(actor *model.User, p VerificationPolicy) (*VerificationPolicy, error) {
result, err := s.authDomain().UpdateVerificationPolicy(actor, p)
if err != nil {
return nil, err
}
if err := s.appendAdminAudit(actor, "auth.policy.update", "setting", "auth_verification_policy", "更新验证码登录注册策略", nil); err != nil {
return nil, err
}
return result, nil
}
func (s *Service) StartVerification(ctx context.Context, actor *model.User, req VerificationRequest) (*auth.VerificationTicket, error) {
return s.authDomain().StartVerification(ctx, actor, req)
}
func (s *Service) LoginVerification(req VerificationConfirm) (*AuthSessionResult, error) {
return s.authDomain().LoginVerification(req)
}
func (s *Service) BindVerification(actor *model.User, req VerificationConfirm) (*AuthUser, error) {
return s.authDomain().BindVerification(actor, req)
}
+11
View File
@@ -23,6 +23,7 @@ const (
PluginKindProtocol = "protocol"
PluginKindApplication = "application"
PluginKindPayment = "payment"
PluginKindSMS = "sms"
PluginScopeSystem = "system"
PluginScopeUser = "user"
@@ -112,6 +113,9 @@ func pluginManagement(pluginID string, source string) PluginManagementView {
if policy, ok := systemPaymentPolicies[strings.TrimSpace(pluginID)]; ok {
return policy
}
if policy, ok := systemSMSPolicies[strings.TrimSpace(pluginID)]; ok {
return policy
}
return PluginManagementView{
Origin: PluginOriginOfficial, Kind: PluginKindProtocol,
ActivationScope: PluginScopeSystem, ConfigurationScope: PluginConfigurationSystem,
@@ -147,6 +151,10 @@ func knownPluginIDs(items []PluginView) []string {
seen[id] = struct{}{}
ids = append(ids, id)
}
for id := range systemSMSPolicies {
seen[id] = struct{}{}
ids = append(ids, id)
}
for _, item := range items {
if _, exists := seen[item.Manifest.ID]; exists {
continue
@@ -338,6 +346,9 @@ func (s *Service) SetPluginPlatformAvailability(actor *model.User, pluginID stri
}
func isKnownRuntimeOptionalPlugin(pluginID string) bool {
if isSystemSMSPluginID(pluginID) {
return true
}
if _, known := officialApplicationPolicies[pluginID]; known {
return true
}
+10 -1
View File
@@ -191,6 +191,7 @@ func (c *pluginRuntime) bootstrapBuiltInPlugins() error {
byID[id] = record
}
bundledManifests := append(bundledWorkflowPluginManifests(), bundledPaymentPluginManifests()...)
bundledManifests = append(bundledManifests, bundledSMSPluginManifests()...)
for _, bundled := range bundledManifests {
builtInIDs[bundled.Metadata.ID] = struct{}{}
if existing, exists := byID[bundled.Metadata.ID]; exists && existing.PackagePath != "" && isSystemPaymentPluginID(bundled.Metadata.ID) {
@@ -222,6 +223,9 @@ func (c *pluginRuntime) bootstrapBuiltInPlugins() error {
if _, systemPayment := systemPaymentPolicies[bundled.Metadata.ID]; systemPayment {
source = PluginOriginSystem
}
if isSystemSMSPluginID(bundled.Metadata.ID) {
source = PluginOriginSystem
}
record.UpdatedAt = now
record.ID, record.Raw, record.Source, record.PackagePath = bundled.Metadata.ID, data, source, ""
byID[bundled.Metadata.ID] = record
@@ -343,7 +347,12 @@ func (c *pluginRuntime) reload() error {
continue
}
}
if len(manifest.Contributes.PaymentProviders) > 0 && len(manifest.Contributes.Providers) == 0 {
if len(manifest.Contributes.SMSProviders) > 0 && (record.Source == PluginOriginUploaded || !isSystemSMSPluginID(id)) {
record.Metadata.Enabled, record.Status, record.Error = false, "invalid", "短信插件仅支持系统宿主适配器"
plugins[id] = record
continue
}
if (len(manifest.Contributes.PaymentProviders) > 0 || len(manifest.Contributes.SMSProviders) > 0) && len(manifest.Contributes.Providers) == 0 {
if record.Metadata.Enabled {
record.Status = "enabled"
} else {
+4
View File
@@ -19,6 +19,7 @@ import (
"infinite-canvas/backend/internal/prompts"
"infinite-canvas/backend/internal/repository"
"infinite-canvas/backend/internal/skills"
"infinite-canvas/backend/internal/sms"
"infinite-canvas/backend/internal/tools"
)
@@ -71,6 +72,7 @@ type Service struct {
tools *tools.Service
prompts *prompts.Service
auth *auth.Service
sms *sms.Service
canvas *canvas.Service
}
@@ -121,6 +123,8 @@ func newService(repo *repository.Repository, dataDir string) *Service {
service.tools = tools.New(service.repo)
service.prompts = prompts.New(service.repo, promptAdminGate{svc: service})
service.auth = auth.New(service.repo, authHost{svc: service}, nil)
service.sms = service.newSMSDomain()
service.auth.SetSMSDelivery(service.sms)
service.canvas = canvas.New(service.repo, canvasHost{svc: service})
service.platform = platform.New(service.repo, coordinator, platformHost{svc: service})
return service
+26
View File
@@ -0,0 +1,26 @@
package app
import (
"infinite-canvas/backend/internal/protocol"
"infinite-canvas/backend/internal/sms"
)
var systemSMSPolicies = map[string]PluginManagementView{
sms.PluginAliyun: {Origin: PluginOriginSystem, Kind: "sms", ActivationScope: PluginScopeSystem, ConfigurationScope: PluginConfigurationSystem},
sms.PluginTencent: {Origin: PluginOriginSystem, Kind: "sms", ActivationScope: PluginScopeSystem, ConfigurationScope: PluginConfigurationSystem},
}
func bundledSMSPluginManifests() []protocol.Manifest {
result := make([]protocol.Manifest, 0, 2)
for _, p := range []struct{ ID, Provider, Name string }{{sms.PluginAliyun, sms.Aliyun, "阿里云短信"}, {sms.PluginTencent, sms.Tencent, "腾讯云短信"}} {
result = append(result, protocol.Manifest{
APIVersion: "yingce.plugin/v1",
Metadata: protocol.Metadata{ID: p.ID, Version: "1.0.0", Name: p.Name, Vendor: p.Name, Enabled: false, Installable: true, Description: "系统短信插件;支持多渠道、场景模板和发送记录。凭据在短信渠道管理中配置。", Documentation: "# " + p.Name + "\n\n使用 go-sms-sender v0.25.0 聚合库及受控传输扩展。发送受理不等于送达,不自动重试。"},
Surfaces: []string{"settings"}, Runtime: protocol.ManifestRuntime{Backend: "host:sms-" + p.Provider, Web: "host"},
Permissions: []string{"sms.send"}, Contributes: protocol.ManifestContributions{SMSProviders: []protocol.ManifestSMSProvider{{ID: p.Provider, Label: p.Name}}},
})
}
return result
}
func isSystemSMSPluginID(id string) bool { _, ok := systemSMSPolicies[id]; return ok }