mirror of https://github.com/veypi/OneAuth.git
update perm
parent
52f2ae35ab
commit
ced7cc6a07
@ -0,0 +1,162 @@
|
||||
//
|
||||
// Copyright (C) 2024 veypi <i@veypi.com>
|
||||
// 2025-03-04 16:08:06
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/veypi/vbase/cfg"
|
||||
"github.com/veypi/vbase/models"
|
||||
)
|
||||
|
||||
// InitOrgPolicies 为组织初始化默认策略和角色
|
||||
func InitOrgPolicies(orgID string) error {
|
||||
// 创建默认策略
|
||||
policies := getDefaultPolicies()
|
||||
for _, policy := range policies {
|
||||
var count int64
|
||||
cfg.DB().Model(&models.Policy{}).Where("code = ?", policy.Code).Count(&count)
|
||||
if count == 0 {
|
||||
if err := cfg.DB().Create(&policy).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建默认角色
|
||||
roles := getDefaultRoles()
|
||||
for _, role := range roles {
|
||||
role.OrgID = orgID
|
||||
var count int64
|
||||
cfg.DB().Model(&models.Role{}).Where("code = ? AND org_id = ?", role.Code, orgID).Count(&count)
|
||||
if count == 0 {
|
||||
if err := cfg.DB().Create(&role).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDefaultPolicies 获取默认策略列表
|
||||
func getDefaultPolicies() []models.Policy {
|
||||
return []models.Policy{
|
||||
{
|
||||
Code: "user:read",
|
||||
Name: "读取用户信息",
|
||||
Resource: "user",
|
||||
Action: "read",
|
||||
Effect: models.PolicyEffectAllow,
|
||||
Scope: models.PolicyScopeOrg,
|
||||
},
|
||||
{
|
||||
Code: "user:update",
|
||||
Name: "更新用户信息",
|
||||
Resource: "user",
|
||||
Action: "update",
|
||||
Effect: models.PolicyEffectAllow,
|
||||
Condition: "owner",
|
||||
Scope: models.PolicyScopeOrg,
|
||||
},
|
||||
{
|
||||
Code: "role:manage",
|
||||
Name: "管理角色",
|
||||
Resource: "role",
|
||||
Action: "*",
|
||||
Effect: models.PolicyEffectAllow,
|
||||
Condition: "admin",
|
||||
Scope: models.PolicyScopeOrg,
|
||||
},
|
||||
{
|
||||
Code: "policy:manage",
|
||||
Name: "管理策略",
|
||||
Resource: "policy",
|
||||
Action: "*",
|
||||
Effect: models.PolicyEffectAllow,
|
||||
Condition: "admin",
|
||||
Scope: models.PolicyScopeOrg,
|
||||
},
|
||||
{
|
||||
Code: "org:read",
|
||||
Name: "读取组织信息",
|
||||
Resource: "org",
|
||||
Action: "read",
|
||||
Effect: models.PolicyEffectAllow,
|
||||
Scope: models.PolicyScopeOrg,
|
||||
},
|
||||
{
|
||||
Code: "org:update",
|
||||
Name: "更新组织信息",
|
||||
Resource: "org",
|
||||
Action: "update",
|
||||
Effect: models.PolicyEffectAllow,
|
||||
Condition: "admin",
|
||||
Scope: models.PolicyScopeOrg,
|
||||
},
|
||||
{
|
||||
Code: "org:delete",
|
||||
Name: "删除组织",
|
||||
Resource: "org",
|
||||
Action: "delete",
|
||||
Effect: models.PolicyEffectAllow,
|
||||
Condition: "owner",
|
||||
Scope: models.PolicyScopeOrg,
|
||||
},
|
||||
{
|
||||
Code: "member:manage",
|
||||
Name: "管理成员",
|
||||
Resource: "org_member",
|
||||
Action: "*",
|
||||
Effect: models.PolicyEffectAllow,
|
||||
Condition: "admin",
|
||||
Scope: models.PolicyScopeOrg,
|
||||
},
|
||||
{
|
||||
Code: "resource:read",
|
||||
Name: "读取资源",
|
||||
Resource: "resource",
|
||||
Action: "read",
|
||||
Effect: models.PolicyEffectAllow,
|
||||
Scope: models.PolicyScopeOrg,
|
||||
},
|
||||
{
|
||||
Code: "resource:write",
|
||||
Name: "写入资源",
|
||||
Resource: "resource",
|
||||
Action: "create,update,delete",
|
||||
Effect: models.PolicyEffectAllow,
|
||||
Condition: "owner",
|
||||
Scope: models.PolicyScopeOrg,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// getDefaultRoles 获取默认角色列表
|
||||
func getDefaultRoles() []models.Role {
|
||||
return []models.Role{
|
||||
{
|
||||
Name: "管理员",
|
||||
Code: models.RoleCodeAdmin,
|
||||
Description: "组织管理员,可以管理成员、角色和策略",
|
||||
Scope: models.PolicyScopeOrg,
|
||||
IsSystem: true,
|
||||
},
|
||||
{
|
||||
Name: "开发者",
|
||||
Code: models.RoleCodeDeveloper,
|
||||
Description: "开发者,可以创建和管理资源",
|
||||
Scope: models.PolicyScopeOrg,
|
||||
IsSystem: true,
|
||||
},
|
||||
{
|
||||
Name: "只读用户",
|
||||
Code: models.RoleCodeViewer,
|
||||
Description: "只读访问权限",
|
||||
Scope: models.PolicyScopeOrg,
|
||||
IsSystem: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,155 @@
|
||||
//
|
||||
// Copyright (C) 2024 veypi <i@veypi.com>
|
||||
// 2025-03-04 16:08:06
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/veypi/vbase/cfg"
|
||||
"github.com/veypi/vbase/models"
|
||||
"github.com/veypi/vigo"
|
||||
)
|
||||
|
||||
// Checker 权限检查器
|
||||
type Checker struct {
|
||||
userID string
|
||||
orgID string
|
||||
roles []string
|
||||
org *models.Org
|
||||
}
|
||||
|
||||
// NewChecker 创建权限检查器
|
||||
func NewChecker(x *vigo.X) *Checker {
|
||||
c := &Checker{}
|
||||
|
||||
if uid, ok := x.Get("user_id").(string); ok {
|
||||
c.userID = uid
|
||||
}
|
||||
if oid, ok := x.Get("org_id").(string); ok {
|
||||
c.orgID = oid
|
||||
}
|
||||
if roles, ok := x.Get("org_roles").(string); ok && roles != "" {
|
||||
c.roles = strings.Split(roles, ",")
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// IsOrgOwner 检查用户是否是组织所有者
|
||||
func (c *Checker) IsOrgOwner() bool {
|
||||
if c.orgID == "" || c.userID == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if c.org != nil {
|
||||
return c.org.OwnerID == c.userID
|
||||
}
|
||||
|
||||
var org models.Org
|
||||
if err := cfg.DB().First(&org, "id = ?", c.orgID).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
c.org = &org
|
||||
|
||||
return org.OwnerID == c.userID
|
||||
}
|
||||
|
||||
// IsOrgAdmin 检查用户是否是组织管理员
|
||||
func (c *Checker) IsOrgAdmin() bool {
|
||||
if c.IsOrgOwner() {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, roleID := range c.roles {
|
||||
var role models.Role
|
||||
if err := cfg.DB().First(&role, "id = ?", roleID).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
if role.Code == models.RoleCodeAdmin {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// HasRole 检查用户是否有指定角色
|
||||
func (c *Checker) HasRole(roleCode string) bool {
|
||||
for _, roleID := range c.roles {
|
||||
var role models.Role
|
||||
if err := cfg.DB().First(&role, "id = ?", roleID).Error; err != nil {
|
||||
continue
|
||||
}
|
||||
if role.Code == roleCode {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RequireAdmin 要求管理员权限
|
||||
func (c *Checker) RequireAdmin() error {
|
||||
if !c.IsOrgAdmin() {
|
||||
return vigo.ErrForbidden.WithString("admin permission required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequireOwner 要求所有者权限
|
||||
func (c *Checker) RequireOwner() error {
|
||||
if !c.IsOrgOwner() {
|
||||
return vigo.ErrForbidden.WithString("owner permission required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequireOrg 要求必须在组织上下文中
|
||||
func (c *Checker) RequireOrg() error {
|
||||
if c.orgID == "" {
|
||||
return vigo.ErrArgInvalid.WithString("organization context required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UserID 获取用户ID
|
||||
func (c *Checker) UserID() string {
|
||||
return c.userID
|
||||
}
|
||||
|
||||
// OrgID 获取组织ID
|
||||
func (c *Checker) OrgID() string {
|
||||
return c.orgID
|
||||
}
|
||||
|
||||
// GetUserRoles 获取用户角色列表
|
||||
func (c *Checker) GetUserRoles() []string {
|
||||
return c.roles
|
||||
}
|
||||
|
||||
// RequireAdmin 中间件:要求管理员权限
|
||||
func RequireAdmin() func(*vigo.X) error {
|
||||
return func(x *vigo.X) error {
|
||||
checker := NewChecker(x)
|
||||
return checker.RequireAdmin()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireOwner 中间件:要求组织所有者权限
|
||||
func RequireOwner() func(*vigo.X) error {
|
||||
return func(x *vigo.X) error {
|
||||
checker := NewChecker(x)
|
||||
return checker.RequireOwner()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireOrgContext 中间件:要求必须在组织上下文中
|
||||
func RequireOrgContext() func(*vigo.X) error {
|
||||
return func(x *vigo.X) error {
|
||||
checker := NewChecker(x)
|
||||
return checker.RequireOrg()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
//
|
||||
// Copyright (C) 2024 veypi <i@veypi.com>
|
||||
// 2025-03-04 16:08:06
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package policy
|
||||
|
||||
import (
|
||||
"github.com/veypi/vbase/cfg"
|
||||
"github.com/veypi/vbase/models"
|
||||
"github.com/veypi/vigo"
|
||||
)
|
||||
|
||||
type CreateRequest struct {
|
||||
Code string `json:"code" src:"json" desc:"策略代码"`
|
||||
Name string `json:"name" src:"json" desc:"策略名称"`
|
||||
Description string `json:"description,omitempty" src:"json" desc:"描述"`
|
||||
Resource string `json:"resource" src:"json" desc:"资源: user/org/resource/*"`
|
||||
Action string `json:"action" src:"json" desc:"操作: create/read/update/delete/*"`
|
||||
Effect string `json:"effect" src:"json" desc:"效果: allow/deny"`
|
||||
Condition string `json:"condition,omitempty" src:"json" desc:"条件: owner/org_member"`
|
||||
Scope string `json:"scope" src:"json" desc:"作用域: platform/org/resource"`
|
||||
}
|
||||
|
||||
func create(x *vigo.X, req *CreateRequest) (*models.Policy, error) {
|
||||
// 检查代码是否已存在
|
||||
var count int64
|
||||
cfg.DB().Model(&models.Policy{}).Where("code = ?", req.Code).Count(&count)
|
||||
if count > 0 {
|
||||
return nil, vigo.ErrArgInvalid.WithString("policy code already exists")
|
||||
}
|
||||
|
||||
policy := &models.Policy{
|
||||
Code: req.Code,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Resource: req.Resource,
|
||||
Action: req.Action,
|
||||
Effect: req.Effect,
|
||||
Condition: req.Condition,
|
||||
Scope: req.Scope,
|
||||
}
|
||||
|
||||
if policy.Effect == "" {
|
||||
policy.Effect = models.PolicyEffectAllow
|
||||
}
|
||||
if policy.Scope == "" {
|
||||
policy.Scope = models.PolicyScopeOrg
|
||||
}
|
||||
|
||||
if err := cfg.DB().Create(policy).Error; err != nil {
|
||||
return nil, vigo.ErrInternalServer.WithError(err)
|
||||
}
|
||||
|
||||
return policy, nil
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
//
|
||||
// Copyright (C) 2024 veypi <i@veypi.com>
|
||||
// 2025-03-04 16:08:06
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package policy
|
||||
|
||||
import (
|
||||
"github.com/veypi/vbase/cfg"
|
||||
"github.com/veypi/vbase/models"
|
||||
"github.com/veypi/vigo"
|
||||
)
|
||||
|
||||
type DeleteRequest struct {
|
||||
PolicyID string `src:"path@policy_id" desc:"策略ID"`
|
||||
}
|
||||
|
||||
func del(x *vigo.X, req *DeleteRequest) error {
|
||||
var policy models.Policy
|
||||
if err := cfg.DB().First(&policy, "id = ?", req.PolicyID).Error; err != nil {
|
||||
return vigo.ErrNotFound
|
||||
}
|
||||
|
||||
// 系统策略不允许删除
|
||||
if policy.Scope == models.PolicyScopePlatform {
|
||||
return vigo.ErrForbidden.WithString("system policies cannot be deleted")
|
||||
}
|
||||
|
||||
if err := cfg.DB().Delete(&policy).Error; err != nil {
|
||||
return vigo.ErrInternalServer.WithError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
//
|
||||
// Copyright (C) 2024 veypi <i@veypi.com>
|
||||
// 2025-03-04 16:08:06
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package policy
|
||||
|
||||
import (
|
||||
"github.com/veypi/vbase/cfg"
|
||||
"github.com/veypi/vbase/models"
|
||||
"github.com/veypi/vigo"
|
||||
)
|
||||
|
||||
type GetRequest struct {
|
||||
PolicyID string `src:"path@policy_id" desc:"策略ID"`
|
||||
}
|
||||
|
||||
func get(x *vigo.X, req *GetRequest) (*models.Policy, error) {
|
||||
var policy models.Policy
|
||||
if err := cfg.DB().First(&policy, "id = ?", req.PolicyID).Error; err != nil {
|
||||
return nil, vigo.ErrNotFound
|
||||
}
|
||||
return &policy, nil
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
//
|
||||
// Copyright (C) 2024 veypi <i@veypi.com>
|
||||
// 2025-03-04 16:08:06
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package policy
|
||||
|
||||
import (
|
||||
"github.com/veypi/vbase/api/middleware"
|
||||
"github.com/veypi/vigo"
|
||||
)
|
||||
|
||||
var Router = vigo.NewRouter()
|
||||
|
||||
func init() {
|
||||
// 列表和详情 - 需要读取权限
|
||||
Router.Get("/", middleware.Permission("policy", "list"), "策略列表", list)
|
||||
Router.Get("/{policy_id}", middleware.Permission("policy", "read"), "获取策略详情", get)
|
||||
|
||||
// 创建、更新、删除 - 需要管理权限
|
||||
Router.Post("/", middleware.Permission("policy", "create"), "创建策略", create)
|
||||
Router.Patch("/{policy_id}", middleware.Permission("policy", "update"), "更新策略", patch)
|
||||
Router.Delete("/{policy_id}", middleware.Permission("policy", "delete"), "删除策略", del)
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
//
|
||||
// Copyright (C) 2024 veypi <i@veypi.com>
|
||||
// 2025-03-04 16:08:06
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package policy
|
||||
|
||||
import (
|
||||
"github.com/veypi/vbase/cfg"
|
||||
"github.com/veypi/vbase/models"
|
||||
"github.com/veypi/vigo"
|
||||
)
|
||||
|
||||
type ListRequest struct {
|
||||
Page int `json:"page" src:"query" default:"1"`
|
||||
PageSize int `json:"page_size" src:"query" default:"20"`
|
||||
Resource string `json:"resource" src:"query" desc:"资源类型筛选"`
|
||||
Scope string `json:"scope" src:"query" desc:"作用域筛选"`
|
||||
}
|
||||
|
||||
type ListResponse struct {
|
||||
Items []models.Policy `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
func list(x *vigo.X, req *ListRequest) (*ListResponse, error) {
|
||||
db := cfg.DB().Model(&models.Policy{})
|
||||
|
||||
if req.Resource != "" {
|
||||
db = db.Where("resource = ?", req.Resource)
|
||||
}
|
||||
if req.Scope != "" {
|
||||
db = db.Where("scope = ?", req.Scope)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, vigo.ErrInternalServer.WithError(err)
|
||||
}
|
||||
|
||||
var policies []models.Policy
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
if err := db.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&policies).Error; err != nil {
|
||||
return nil, vigo.ErrInternalServer.WithError(err)
|
||||
}
|
||||
|
||||
totalPages := int(total) / req.PageSize
|
||||
if int(total)%req.PageSize > 0 {
|
||||
totalPages++
|
||||
}
|
||||
|
||||
return &ListResponse{
|
||||
Items: policies,
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
TotalPages: totalPages,
|
||||
}, nil
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
//
|
||||
// Copyright (C) 2024 veypi <i@veypi.com>
|
||||
// 2025-03-04 16:08:06
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package policy
|
||||
|
||||
import (
|
||||
"github.com/veypi/vbase/cfg"
|
||||
"github.com/veypi/vbase/models"
|
||||
"github.com/veypi/vigo"
|
||||
)
|
||||
|
||||
type PatchRequest struct {
|
||||
PolicyID string `src:"path@policy_id" desc:"策略ID"`
|
||||
Name *string `json:"name,omitempty" src:"json" desc:"策略名称"`
|
||||
Description *string `json:"description,omitempty" src:"json" desc:"描述"`
|
||||
Effect *string `json:"effect,omitempty" src:"json" desc:"效果: allow/deny"`
|
||||
Condition *string `json:"condition,omitempty" src:"json" desc:"条件"`
|
||||
}
|
||||
|
||||
func patch(x *vigo.X, req *PatchRequest) (*models.Policy, error) {
|
||||
var policy models.Policy
|
||||
if err := cfg.DB().First(&policy, "id = ?", req.PolicyID).Error; err != nil {
|
||||
return nil, vigo.ErrNotFound
|
||||
}
|
||||
|
||||
// 系统策略不允许修改
|
||||
if policy.Scope == models.PolicyScopePlatform {
|
||||
return nil, vigo.ErrForbidden.WithString("system policies cannot be modified")
|
||||
}
|
||||
|
||||
updates := make(map[string]any)
|
||||
if req.Name != nil {
|
||||
updates["name"] = *req.Name
|
||||
}
|
||||
if req.Description != nil {
|
||||
updates["description"] = *req.Description
|
||||
}
|
||||
if req.Effect != nil {
|
||||
updates["effect"] = *req.Effect
|
||||
}
|
||||
if req.Condition != nil {
|
||||
updates["condition"] = *req.Condition
|
||||
}
|
||||
|
||||
if err := cfg.DB().Model(&policy).Updates(updates).Error; err != nil {
|
||||
return nil, vigo.ErrInternalServer.WithError(err)
|
||||
}
|
||||
|
||||
return &policy, nil
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
// Copyright (C) 2024 veypi <i@veypi.com>
|
||||
// 2025-03-04 16:08:06
|
||||
// Distributed under terms of the MIT license.
|
||||
|
||||
package role
|
||||
|
||||
import (
|
||||
"github.com/veypi/vbase/cfg"
|
||||
"github.com/veypi/vbase/models"
|
||||
"github.com/veypi/vigo"
|
||||
)
|
||||
|
||||
type CreateRequest struct {
|
||||
OrgID string `json:"org_id" src:"json" desc:"组织ID"`
|
||||
Name string `json:"name" src:"json" desc:"角色名称"`
|
||||
Code string `json:"code" src:"json" desc:"角色代码"`
|
||||
Description string `json:"description,omitempty" src:"json" desc:"描述"`
|
||||
PolicyIDs []string `json:"policy_ids,omitempty" src:"json" desc:"策略ID列表"`
|
||||
}
|
||||
|
||||
func create(x *vigo.X, req *CreateRequest) (*models.Role, error) {
|
||||
// 检查同一组织内代码是否已存在
|
||||
var count int64
|
||||
cfg.DB().Model(&models.Role{}).Where("org_id = ? AND code = ?", req.OrgID, req.Code).Count(&count)
|
||||
if count > 0 {
|
||||
return nil, vigo.ErrArgInvalid.WithString("role code already exists in this organization")
|
||||
}
|
||||
|
||||
// 转换策略ID列表为字符串
|
||||
policyIDsStr := ""
|
||||
for i, id := range req.PolicyIDs {
|
||||
if i > 0 {
|
||||
policyIDsStr += ","
|
||||
}
|
||||
policyIDsStr += id
|
||||
}
|
||||
|
||||
role := &models.Role{
|
||||
OrgID: req.OrgID,
|
||||
Name: req.Name,
|
||||
Code: req.Code,
|
||||
Description: req.Description,
|
||||
PolicyIDs: policyIDsStr,
|
||||
Scope: models.PolicyScopeOrg,
|
||||
}
|
||||
|
||||
if err := cfg.DB().Create(role).Error; err != nil {
|
||||
return nil, vigo.ErrInternalServer.WithError(err)
|
||||
}
|
||||
|
||||
return role, nil
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
// Copyright (C) 2024 veypi <i@veypi.com>
|
||||
// 2025-03-04 16:08:06
|
||||
// Distributed under terms of the MIT license.
|
||||
|
||||
package role
|
||||
|
||||
import (
|
||||
"github.com/veypi/vbase/cfg"
|
||||
"github.com/veypi/vbase/models"
|
||||
"github.com/veypi/vigo"
|
||||
)
|
||||
|
||||
type DeleteRequest struct {
|
||||
RoleID string `src:"path@role_id" desc:"角色ID"`
|
||||
}
|
||||
|
||||
func del(x *vigo.X, req *DeleteRequest) error {
|
||||
var role models.Role
|
||||
if err := cfg.DB().First(&role, "id = ?", req.RoleID).Error; err != nil {
|
||||
return vigo.ErrNotFound
|
||||
}
|
||||
|
||||
// 系统角色不允许删除
|
||||
if role.IsSystem {
|
||||
return vigo.ErrForbidden.WithString("system roles cannot be deleted")
|
||||
}
|
||||
|
||||
if err := cfg.DB().Delete(&role).Error; err != nil {
|
||||
return vigo.ErrInternalServer.WithError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
// Copyright (C) 2024 veypi <i@veypi.com>
|
||||
// 2025-03-04 16:08:06
|
||||
// Distributed under terms of the MIT license.
|
||||
|
||||
package role
|
||||
|
||||
import (
|
||||
"github.com/veypi/vbase/cfg"
|
||||
"github.com/veypi/vbase/models"
|
||||
"github.com/veypi/vigo"
|
||||
)
|
||||
|
||||
type GetRequest struct {
|
||||
RoleID string `src:"path@role_id" desc:"角色ID"`
|
||||
}
|
||||
|
||||
type RoleDetail struct {
|
||||
models.Role
|
||||
Policies []models.Policy `json:"policies"`
|
||||
}
|
||||
|
||||
func get(x *vigo.X, req *GetRequest) (*RoleDetail, error) {
|
||||
var role models.Role
|
||||
if err := cfg.DB().First(&role, "id = ?", req.RoleID).Error; err != nil {
|
||||
return nil, vigo.ErrNotFound
|
||||
}
|
||||
|
||||
// 解析策略ID列表
|
||||
policies := []models.Policy{}
|
||||
// TODO: 根据 role.PolicyIDs 查询策略列表
|
||||
|
||||
return &RoleDetail{
|
||||
Role: role,
|
||||
Policies: policies,
|
||||
}, nil
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
// Copyright (C) 2024 veypi <i@veypi.com>
|
||||
// 2025-03-04 16:08:06
|
||||
// Distributed under terms of the MIT license.
|
||||
|
||||
package role
|
||||
|
||||
import (
|
||||
"github.com/veypi/vbase/api/middleware"
|
||||
"github.com/veypi/vigo"
|
||||
)
|
||||
|
||||
var Router = vigo.NewRouter()
|
||||
|
||||
func init() {
|
||||
// 列表和详情 - 需要读取权限
|
||||
Router.Get("/", middleware.Permission("role", "list"), "角色列表", list)
|
||||
Router.Get("/{role_id}", middleware.Permission("role", "read"), "获取角色详情", get)
|
||||
|
||||
// 创建、更新、删除 - 需要管理权限
|
||||
Router.Post("/", middleware.Permission("role", "create"), "创建角色", create)
|
||||
Router.Patch("/{role_id}", middleware.Permission("role", "update"), "更新角色", patch)
|
||||
Router.Delete("/{role_id}", middleware.Permission("role", "delete"), "删除角色", del)
|
||||
|
||||
// 角色策略管理 - 需要角色管理权限
|
||||
Router.Get("/{role_id}/policies", middleware.Permission("role", "read"), "获取角色策略", listPolicies)
|
||||
Router.Post("/{role_id}/policies", middleware.Permission("role", "update"), "为角色添加策略", addPolicy)
|
||||
Router.Delete("/{role_id}/policies/{policy_id}", middleware.Permission("role", "update"), "从角色移除策略", removePolicy)
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
// Copyright (C) 2024 veypi <i@veypi.com>
|
||||
// 2025-03-04 16:08:06
|
||||
// Distributed under terms of the MIT license.
|
||||
|
||||
package role
|
||||
|
||||
import (
|
||||
"github.com/veypi/vbase/cfg"
|
||||
"github.com/veypi/vbase/models"
|
||||
"github.com/veypi/vigo"
|
||||
)
|
||||
|
||||
type ListRequest struct {
|
||||
Page int `json:"page" src:"query" default:"1"`
|
||||
PageSize int `json:"page_size" src:"query" default:"20"`
|
||||
OrgID string `json:"org_id" src:"query" desc:"组织ID筛选"`
|
||||
}
|
||||
|
||||
type ListResponse struct {
|
||||
Items []models.Role `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
func list(x *vigo.X, req *ListRequest) (*ListResponse, error) {
|
||||
db := cfg.DB().Model(&models.Role{})
|
||||
|
||||
if req.OrgID != "" {
|
||||
db = db.Where("org_id = ?", req.OrgID)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, vigo.ErrInternalServer.WithError(err)
|
||||
}
|
||||
|
||||
var roles []models.Role
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
if err := db.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&roles).Error; err != nil {
|
||||
return nil, vigo.ErrInternalServer.WithError(err)
|
||||
}
|
||||
|
||||
totalPages := int(total) / req.PageSize
|
||||
if int(total)%req.PageSize > 0 {
|
||||
totalPages++
|
||||
}
|
||||
|
||||
return &ListResponse{
|
||||
Items: roles,
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
TotalPages: totalPages,
|
||||
}, nil
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
// Copyright (C) 2024 veypi <i@veypi.com>
|
||||
// 2025-03-04 16:08:06
|
||||
// Distributed under terms of the MIT license.
|
||||
|
||||
package role
|
||||
|
||||
import (
|
||||
"github.com/veypi/vbase/cfg"
|
||||
"github.com/veypi/vbase/models"
|
||||
"github.com/veypi/vigo"
|
||||
)
|
||||
|
||||
type PatchRequest struct {
|
||||
RoleID string `src:"path@role_id" desc:"角色ID"`
|
||||
Name *string `json:"name,omitempty" src:"json" desc:"角色名称"`
|
||||
Description *string `json:"description,omitempty" src:"json" desc:"描述"`
|
||||
PolicyIDs []string `json:"policy_ids,omitempty" src:"json" desc:"策略ID列表"`
|
||||
}
|
||||
|
||||
func patch(x *vigo.X, req *PatchRequest) (*models.Role, error) {
|
||||
var role models.Role
|
||||
if err := cfg.DB().First(&role, "id = ?", req.RoleID).Error; err != nil {
|
||||
return nil, vigo.ErrNotFound
|
||||
}
|
||||
|
||||
// 系统角色不允许修改
|
||||
if role.IsSystem {
|
||||
return nil, vigo.ErrForbidden.WithString("system roles cannot be modified")
|
||||
}
|
||||
|
||||
updates := make(map[string]any)
|
||||
if req.Name != nil {
|
||||
updates["name"] = *req.Name
|
||||
}
|
||||
if req.Description != nil {
|
||||
updates["description"] = *req.Description
|
||||
}
|
||||
if req.PolicyIDs != nil {
|
||||
policyIDsStr := ""
|
||||
for i, id := range req.PolicyIDs {
|
||||
if i > 0 {
|
||||
policyIDsStr += ","
|
||||
}
|
||||
policyIDsStr += id
|
||||
}
|
||||
updates["policy_ids"] = policyIDsStr
|
||||
}
|
||||
|
||||
if err := cfg.DB().Model(&role).Updates(updates).Error; err != nil {
|
||||
return nil, vigo.ErrInternalServer.WithError(err)
|
||||
}
|
||||
|
||||
return &role, nil
|
||||
}
|
||||
@ -0,0 +1,129 @@
|
||||
// Copyright (C) 2024 veypi <i@veypi.com>
|
||||
// 2025-03-04 16:08:06
|
||||
// Distributed under terms of the MIT license.
|
||||
|
||||
package role
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/veypi/vbase/cfg"
|
||||
"github.com/veypi/vbase/models"
|
||||
"github.com/veypi/vigo"
|
||||
)
|
||||
|
||||
// ListPoliciesRequest 获取角色策略请求
|
||||
type ListPoliciesRequest struct {
|
||||
RoleID string `src:"path@role_id" desc:"角色ID"`
|
||||
}
|
||||
|
||||
// listPolicies 获取角色关联的策略列表
|
||||
func listPolicies(x *vigo.X, req *ListPoliciesRequest) ([]models.Policy, error) {
|
||||
var role models.Role
|
||||
if err := cfg.DB().First(&role, "id = ?", req.RoleID).Error; err != nil {
|
||||
return nil, vigo.ErrNotFound
|
||||
}
|
||||
|
||||
if role.PolicyIDs == "" {
|
||||
return []models.Policy{}, nil
|
||||
}
|
||||
|
||||
// 解析策略ID列表
|
||||
policyIDs := strings.Split(role.PolicyIDs, ",")
|
||||
|
||||
var policies []models.Policy
|
||||
if err := cfg.DB().Where("id IN ?", policyIDs).Find(&policies).Error; err != nil {
|
||||
return nil, vigo.ErrInternalServer.WithError(err)
|
||||
}
|
||||
|
||||
return policies, nil
|
||||
}
|
||||
|
||||
// AddPolicyRequest 添加策略请求
|
||||
type AddPolicyRequest struct {
|
||||
RoleID string `src:"path@role_id" desc:"角色ID"`
|
||||
PolicyID string `json:"policy_id" src:"json" desc:"策略ID"`
|
||||
}
|
||||
|
||||
// addPolicy 为角色添加策略
|
||||
func addPolicy(x *vigo.X, req *AddPolicyRequest) (*models.Role, error) {
|
||||
var role models.Role
|
||||
if err := cfg.DB().First(&role, "id = ?", req.RoleID).Error; err != nil {
|
||||
return nil, vigo.ErrNotFound
|
||||
}
|
||||
|
||||
// 验证策略是否存在
|
||||
var policy models.Policy
|
||||
if err := cfg.DB().First(&policy, "id = ?", req.PolicyID).Error; err != nil {
|
||||
return nil, vigo.ErrArgInvalid.WithString("policy not found")
|
||||
}
|
||||
|
||||
// 解析现有策略ID
|
||||
existingIDs := map[string]bool{}
|
||||
if role.PolicyIDs != "" {
|
||||
for _, id := range strings.Split(role.PolicyIDs, ",") {
|
||||
existingIDs[id] = true
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
if existingIDs[req.PolicyID] {
|
||||
return nil, vigo.ErrArgInvalid.WithString("policy already added to this role")
|
||||
}
|
||||
|
||||
// 添加新策略ID
|
||||
if role.PolicyIDs != "" {
|
||||
role.PolicyIDs += ","
|
||||
}
|
||||
role.PolicyIDs += req.PolicyID
|
||||
|
||||
if err := cfg.DB().Model(&role).Update("policy_ids", role.PolicyIDs).Error; err != nil {
|
||||
return nil, vigo.ErrInternalServer.WithError(err)
|
||||
}
|
||||
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
// RemovePolicyRequest 移除策略请求
|
||||
type RemovePolicyRequest struct {
|
||||
RoleID string `src:"path@role_id" desc:"角色ID"`
|
||||
PolicyID string `src:"path@policy_id" desc:"策略ID"`
|
||||
}
|
||||
|
||||
// removePolicy 从角色移除策略
|
||||
func removePolicy(x *vigo.X, req *RemovePolicyRequest) (*models.Role, error) {
|
||||
var role models.Role
|
||||
if err := cfg.DB().First(&role, "id = ?", req.RoleID).Error; err != nil {
|
||||
return nil, vigo.ErrNotFound
|
||||
}
|
||||
|
||||
// 解析现有策略ID
|
||||
if role.PolicyIDs == "" {
|
||||
return nil, vigo.ErrArgInvalid.WithString("role has no policies")
|
||||
}
|
||||
|
||||
existingIDs := strings.Split(role.PolicyIDs, ",")
|
||||
newIDs := []string{}
|
||||
found := false
|
||||
|
||||
for _, id := range existingIDs {
|
||||
if id == req.PolicyID {
|
||||
found = true
|
||||
continue
|
||||
}
|
||||
newIDs = append(newIDs, id)
|
||||
}
|
||||
|
||||
if !found {
|
||||
return nil, vigo.ErrArgInvalid.WithString("policy not found in this role")
|
||||
}
|
||||
|
||||
// 更新策略ID列表
|
||||
role.PolicyIDs = strings.Join(newIDs, ",")
|
||||
|
||||
if err := cfg.DB().Model(&role).Update("policy_ids", role.PolicyIDs).Error; err != nil {
|
||||
return nil, vigo.ErrInternalServer.WithError(err)
|
||||
}
|
||||
|
||||
return &role, nil
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
//
|
||||
// init.go
|
||||
// Copyright (C) 2025 veypi <i@veypi.com>
|
||||
//
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package sms
|
||||
|
||||
import "github.com/veypi/vigo"
|
||||
|
||||
var Router = vigo.NewRouter()
|
||||
|
||||
|
||||
@ -0,0 +1,105 @@
|
||||
package sms
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/veypi/vbase/cfg"
|
||||
"github.com/veypi/vbase/libs/sms_providers"
|
||||
"github.com/veypi/vbase/libs/utils"
|
||||
"github.com/veypi/vbase/models"
|
||||
"github.com/veypi/vigo"
|
||||
"github.com/veypi/vigo/contrib/limiter"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SendCodeRequest 发送验证码请求
|
||||
type SendCodeRequest struct {
|
||||
Phone string `json:"phone" src:"json" desc:"手机号"`
|
||||
Region string `json:"region" src:"json" desc:"区域"`
|
||||
Purpose string `json:"purpose" src:"json" desc:"用途"`
|
||||
TemplateID string `json:"template_id" src:"json" desc:"模板ID"`
|
||||
}
|
||||
|
||||
// SendCodeResponse 发送验证码响应
|
||||
type SendCodeResponse struct {
|
||||
ID string `json:"id"`
|
||||
Phone string `json:"phone"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
Interval int `json:"interval"` // 下次可发送间隔(秒)
|
||||
}
|
||||
|
||||
var _ = Router.Post("/", "发送验证码", vigo.SkipBefore, limiter.NewAdvancedRequestLimiter(time.Minute*3, 5, time.Second*30).Limit, sendCode)
|
||||
|
||||
// SendCode 发送验证码
|
||||
func sendCode(x *vigo.X, req *SendCodeRequest) (*SendCodeResponse, error) {
|
||||
// 1. 验证手机号
|
||||
normalizedPhone := utils.NormalizePhoneNumber(req.Phone)
|
||||
if !utils.ValidatePhoneNumber(normalizedPhone) {
|
||||
return nil, fmt.Errorf("invalid phone number: %s", req.Phone)
|
||||
}
|
||||
|
||||
// 2. 检查区域配置
|
||||
provider, err := sms_providers.GetSMSProvider(req.Region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 检查发送频率限制
|
||||
if err := CheckSendInterval(normalizedPhone, req.Region, req.Purpose); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. 检查每日发送次数限制
|
||||
if err := CheckDailyLimit(normalizedPhone, req.Region); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 5. 生成验证码
|
||||
code, err := utils.GenerateCode(cfg.Config.SMS.Global.CodeLength)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate code: %w", err)
|
||||
}
|
||||
|
||||
// 6. 创建验证码记录
|
||||
smsCode := &models.SMSCode{
|
||||
Phone: normalizedPhone,
|
||||
Code: code,
|
||||
Region: req.Region,
|
||||
Purpose: req.Purpose,
|
||||
Status: models.CodeStatusPending,
|
||||
ExpiresAt: time.Now().Add(cfg.Config.SMS.Global.CodeExpiry),
|
||||
}
|
||||
|
||||
// 7. 发送短信
|
||||
sendReq := &sms_providers.SendSMSRequest{
|
||||
Phone: normalizedPhone,
|
||||
TemplateID: req.TemplateID,
|
||||
Region: req.Region,
|
||||
Purpose: req.Purpose,
|
||||
Params: map[string]string{
|
||||
"code": code,
|
||||
},
|
||||
}
|
||||
err = cfg.DB().Transaction(func(tx *gorm.DB) error {
|
||||
err := tx.Create(smsCode).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
smsID, err := provider.SendSMS(x.Context(), sendReq)
|
||||
LogSMS(normalizedPhone, req.Region, provider.GetName(), smsID, err)
|
||||
if err != nil || smsID == "" {
|
||||
return fmt.Errorf("failed to send sms: %v", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SendCodeResponse{
|
||||
ID: smsCode.ID,
|
||||
Phone: smsCode.Phone,
|
||||
ExpiresAt: smsCode.ExpiresAt,
|
||||
Interval: int(cfg.Config.SMS.Global.SendInterval.Seconds()),
|
||||
}, nil
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
//
|
||||
// utils.go
|
||||
// Copyright (C) 2025 veypi <i@veypi.com>
|
||||
//
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package sms
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/veypi/vbase/cfg"
|
||||
"github.com/veypi/vbase/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// checkDailyLimit 检查每日发送次数限制
|
||||
func CheckDailyLimit(phone, region string) error {
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
tomorrow := today.Add(24 * time.Hour)
|
||||
|
||||
var count int64
|
||||
err := cfg.DB().Model(&models.SMSCode{}).
|
||||
Where("phone = ? AND region = ? AND created_at >= ? AND created_at < ?",
|
||||
phone, region, today, tomorrow).
|
||||
Count(&count).Error
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check daily limit: %w", err)
|
||||
}
|
||||
|
||||
if count >= int64(cfg.Config.SMS.Global.MaxDailyCount) {
|
||||
return fmt.Errorf("daily sms limit exceeded")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkSendInterval 检查发送间隔
|
||||
func CheckSendInterval(phone, region, purpose string) error {
|
||||
var lastCode models.SMSCode
|
||||
err := cfg.DB().Where("phone = ? AND region = ? AND purpose = ?", phone, region, purpose).
|
||||
Order("created_at DESC").
|
||||
First(&lastCode).Error
|
||||
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return fmt.Errorf("failed to check send interval: %w", err)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
timeSinceLastSend := time.Since(lastCode.CreatedAt)
|
||||
if timeSinceLastSend < cfg.Config.SMS.Global.SendInterval {
|
||||
remaining := cfg.Config.SMS.Global.SendInterval - timeSinceLastSend
|
||||
return fmt.Errorf("please wait %d seconds before sending again", int(remaining.Seconds()))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// logSMS 记录短信发送日志
|
||||
func LogSMS(phone, region, provider, messageID string, err error) {
|
||||
log := &models.SMSLog{
|
||||
Phone: phone,
|
||||
Region: region,
|
||||
Provider: provider,
|
||||
MessageID: messageID,
|
||||
Status: "ok",
|
||||
}
|
||||
if err != nil {
|
||||
log.Status = "failed"
|
||||
log.Error = err.Error()
|
||||
}
|
||||
cfg.DB().Create(log)
|
||||
}
|
||||
|
||||
// CleanupExpiredCodes 清理过期的验证码
|
||||
func CleanupExpiredCodes() error {
|
||||
result := cfg.DB().Model(&models.SMSCode{}).
|
||||
Where("status = ? AND expires_at < ?", models.CodeStatusPending, time.Now()).
|
||||
Update("status", models.CodeStatusExpired)
|
||||
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("failed to cleanup expired codes: %w", result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
//
|
||||
// validate.go
|
||||
// Copyright (C) 2025 veypi <i@veypi.com>
|
||||
//
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
package sms
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/veypi/vbase/cfg"
|
||||
"github.com/veypi/vbase/libs/utils"
|
||||
"github.com/veypi/vbase/models"
|
||||
"github.com/veypi/vigo"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// VerifyCode 验证验证码
|
||||
func VerifyCode(Phone, Code, Region, Purpose string) error {
|
||||
normalizedPhone := utils.NormalizePhoneNumber(Phone)
|
||||
|
||||
// 1. 查找最新的待验证码
|
||||
var smsCode models.SMSCode
|
||||
err := cfg.DB().Where("phone = ? AND region = ? AND purpose = ? AND status = ?",
|
||||
normalizedPhone, Region, Purpose, models.CodeStatusPending).
|
||||
Order("created_at DESC").
|
||||
First(&smsCode).Error
|
||||
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return vigo.NewError("verification code not found or already used").WithCode(404)
|
||||
}
|
||||
return vigo.NewError("failed to query sms code").WithError(err)
|
||||
}
|
||||
|
||||
// 2. 检查验证码是否过期
|
||||
if smsCode.IsExpired() {
|
||||
cfg.DB().Model(&smsCode).Updates(map[string]any{
|
||||
"status": models.CodeStatusExpired,
|
||||
})
|
||||
return fmt.Errorf("verification code has expired")
|
||||
}
|
||||
|
||||
// 3. 检查是否已达到最大尝试次数
|
||||
if !smsCode.CanRetry(cfg.Config.SMS.Global.MaxAttempts) {
|
||||
cfg.DB().Model(&smsCode).Updates(map[string]any{
|
||||
"status": models.CodeStatusFailed,
|
||||
})
|
||||
return fmt.Errorf("verification failed too many times")
|
||||
}
|
||||
|
||||
// 4. 验证码不匹配
|
||||
if smsCode.Code != Code {
|
||||
cfg.DB().Model(&smsCode).Updates(map[string]any{
|
||||
"attempts": smsCode.Attempts + 1,
|
||||
})
|
||||
|
||||
remaining := cfg.Config.SMS.Global.MaxAttempts - smsCode.Attempts - 1
|
||||
return fmt.Errorf("verification code incorrect, %d attempts remaining", remaining)
|
||||
}
|
||||
|
||||
// 5. 验证成功
|
||||
now := time.Now()
|
||||
cfg.DB().Model(&smsCode).Updates(map[string]any{
|
||||
"status": models.CodeStatusUsed,
|
||||
"used_at": &now,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,157 @@
|
||||
# VBase 集成指南
|
||||
|
||||
## 1. 引入路由
|
||||
|
||||
```go
|
||||
import "github.com/veypi/vbase/api"
|
||||
|
||||
func main() {
|
||||
// 挂载 vbase 路由到 /api/v1/vb
|
||||
rootRouter.Extend("/api/v1/vb", api.Router)
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 集成配置
|
||||
|
||||
配置自动从 vigo 的 config.toml 读取:
|
||||
|
||||
```toml
|
||||
[vbase]
|
||||
jwt_secret = "your-secret-key"
|
||||
jwt_expire = 7200 # token 过期时间(秒)
|
||||
refresh_expire = 604800 # refresh token 过期时间(秒)
|
||||
bcrypt_cost = 10 # 密码加密强度
|
||||
|
||||
[vbase.redis]
|
||||
addr = "localhost:6379" # 留空或填 memory 使用内存缓存
|
||||
password = ""
|
||||
db = 0
|
||||
```
|
||||
|
||||
或在代码中自定义:
|
||||
|
||||
```go
|
||||
import "github.com/veypi/vbase/cfg"
|
||||
|
||||
cfg.Config.JWTSecret = "your-secret"
|
||||
cfg.Config.JWTExpire = 7200
|
||||
```
|
||||
|
||||
## 3. 配置策略
|
||||
|
||||
创建组织时自动初始化默认策略:
|
||||
|
||||
```go
|
||||
import "github.com/veypi/vbase/api/middleware"
|
||||
|
||||
// 创建组织后调用
|
||||
middleware.InitOrgPolicies(orgID)
|
||||
```
|
||||
|
||||
默认创建的策略:
|
||||
| 策略 | 资源 | 操作 | 条件 | 说明 |
|
||||
|------|------|------|------|------|
|
||||
| policy:manage | policy | * | admin | 管理策略 |
|
||||
| role:manage | role | * | admin | 管理角色 |
|
||||
| user:update | user | update | owner | 只能改自己 |
|
||||
|
||||
自定义策略:
|
||||
|
||||
```go
|
||||
import "github.com/veypi/vbase/models"
|
||||
|
||||
policy := &models.Policy{
|
||||
Code: "project:delete",
|
||||
Name: "删除项目",
|
||||
Resource: "project",
|
||||
Action: "delete",
|
||||
Effect: models.PolicyEffectAllow,
|
||||
Condition: "owner", // 只有所有者能删
|
||||
Scope: models.PolicyScopeOrg,
|
||||
}
|
||||
cfg.DB().Create(policy)
|
||||
```
|
||||
|
||||
## 4. 使用鉴权
|
||||
|
||||
### 4.1 全局中间件(已内置)
|
||||
|
||||
```go
|
||||
// api/init.go 已自动配置:
|
||||
Router.Use(middleware.AuthRequired()) // JWT 认证
|
||||
Router.Use(middleware.OrgContext()) // 组织上下文
|
||||
```
|
||||
|
||||
### 4.2 公开接口(跳过认证)
|
||||
|
||||
```go
|
||||
Router.Get("/public", vigo.SkipBefore, "公开接口", handler)
|
||||
```
|
||||
|
||||
### 4.3 接口级权限控制
|
||||
|
||||
```go
|
||||
import "github.com/veypi/vbase/api/middleware"
|
||||
|
||||
// 需要管理员权限
|
||||
Router.Post("/users", middleware.RequireAdmin(), "创建用户", createUser)
|
||||
|
||||
// 基于 Policy 的细粒度控制
|
||||
Router.Post("/projects", middleware.Permission("project", "create"), "创建项目", createProject)
|
||||
|
||||
// 带所有者检查(用户只能改自己的数据)
|
||||
Router.Patch("/users/{id}", middleware.PermissionWithOwner("user", "update", "owner_id"), "更新用户", updateUser)
|
||||
|
||||
// 管理员或所有者
|
||||
Router.Delete("/projects/{id}", middleware.AdminOrOwner("owner_id"), "删除项目", deleteProject)
|
||||
```
|
||||
|
||||
### 4.4 代码中手动检查
|
||||
|
||||
```go
|
||||
func myHandler(x *vigo.X, req *Req) error {
|
||||
checker := middleware.NewChecker(x)
|
||||
|
||||
// 检查是否为管理员
|
||||
if !checker.IsOrgAdmin() {
|
||||
return vigo.ErrForbidden
|
||||
}
|
||||
|
||||
// 检查具体权限
|
||||
if err := checker.RequirePermission("resource", "write"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 完整示例
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/veypi/vbase/api"
|
||||
"github.com/veypi/vbase/api/middleware"
|
||||
"github.com/veypi/vigo"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := vigo.NewRouter()
|
||||
|
||||
// 1. 挂载 vbase
|
||||
r.Extend("/api/vb", api.Router)
|
||||
|
||||
// 2. 业务路由加权限
|
||||
project := r.SubRouter("/projects")
|
||||
project.Use(middleware.AuthRequired())
|
||||
|
||||
project.Get("/", middleware.Permission("project", "list"), "项目列表", listProjects)
|
||||
project.Post("/", middleware.Permission("project", "create"), "创建项目", createProject)
|
||||
project.Patch("/{id}", middleware.PermissionWithOwner("project", "update", "owner_id"), "更新项目", updateProject)
|
||||
project.Delete("/{id}", middleware.AdminOrOwner("owner_id"), "删除项目", deleteProject)
|
||||
|
||||
vigo.Run(r)
|
||||
}
|
||||
```
|
||||
Loading…
Reference in New Issue