Compare commits
No commits in common. "main" and "day02.08" have entirely different histories.
@ -6,20 +6,12 @@ import (
|
||||
"122.51.31.227/go-course/go18/book/v3/config"
|
||||
"122.51.31.227/go-course/go18/book/v3/exception"
|
||||
"122.51.31.227/go-course/go18/book/v3/models"
|
||||
"122.51.31.227/go-course/go18/skills/ioc"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func GetBookService() *BookController {
|
||||
return ioc.Controller.Get("book_controller").(*BookController)
|
||||
}
|
||||
|
||||
func init() {
|
||||
ioc.Controller.Registry("book_controller", &BookController{})
|
||||
}
|
||||
var Book = &BookController{}
|
||||
|
||||
type BookController struct {
|
||||
ioc.ObjectImpl
|
||||
}
|
||||
|
||||
func NewGetBookRequest(bookNumber int) *GetBookRequest {
|
||||
@ -75,14 +67,3 @@ func (c *BookController) CreateBook(ctx context.Context, in *models.BookSpec) (*
|
||||
|
||||
return bookInstance, nil
|
||||
}
|
||||
|
||||
func (c *BookController) UpdateBook() {
|
||||
// update(obj)
|
||||
// config.DB().Updates()
|
||||
}
|
||||
|
||||
func (c *BookController) update(ctx context.Context, obj models.Book) error {
|
||||
// obj.UpdateTime = now()
|
||||
// config.DB().Updates()
|
||||
return nil
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
func TestGetBook(t *testing.T) {
|
||||
book, err := controllers.GetBookService().GetBook(context.Background(), controllers.NewGetBookRequest(3))
|
||||
book, err := controllers.Book.GetBook(context.Background(), controllers.NewGetBookRequest(3))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -20,7 +20,7 @@ func TestGetBook(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateBook(t *testing.T) {
|
||||
book, err := controllers.GetBookService().CreateBook(context.Background(), &models.BookSpec{
|
||||
book, err := controllers.Book.CreateBook(context.Background(), &models.BookSpec{
|
||||
Title: "unit test for go controller obj",
|
||||
Author: "will",
|
||||
Price: 99.99,
|
||||
|
@ -19,7 +19,7 @@ type AddCommentRequest struct {
|
||||
func (c *CommentController) AddComment(ctx context.Context, in *AddCommentRequest) (*models.Comment, error) {
|
||||
// 业务处理的细节
|
||||
// 多个业务模块 进行交互
|
||||
book, err := GetBookService().GetBook(ctx, NewGetBookRequest(in.BookNumber))
|
||||
book, err := Book.GetBook(ctx, NewGetBookRequest(in.BookNumber))
|
||||
|
||||
// if exception.IsApiException(err, exception.CODE_NOT_FOUND) {
|
||||
|
||||
|
@ -114,7 +114,7 @@ func (h *BookApiHandler) createBook(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
book, err := controllers.GetBookService().CreateBook(ctx.Request.Context(), bookSpecInstance)
|
||||
book, err := controllers.Book.CreateBook(ctx.Request.Context(), bookSpecInstance)
|
||||
if err != nil {
|
||||
response.Failed(ctx, err)
|
||||
return
|
||||
@ -131,7 +131,7 @@ func (h *BookApiHandler) getBook(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
book, err := controllers.GetBookService().GetBook(ctx, controllers.NewGetBookRequest(int(bnInt)))
|
||||
book, err := controllers.Book.GetBook(ctx, controllers.NewGetBookRequest(int(bnInt)))
|
||||
if err != nil {
|
||||
response.Failed(ctx, err)
|
||||
return
|
||||
|
@ -9,16 +9,6 @@ type BookSet struct {
|
||||
Items []*Book `json:"items"`
|
||||
}
|
||||
|
||||
type BookSpec struct {
|
||||
// type 用于要使用gorm 来自动创建和更新表的时候 才需要定义
|
||||
Title string `json:"title" gorm:"column:title;type:varchar(200)" validate:"required"`
|
||||
Author string `json:"author" gorm:"column:author;type:varchar(200);index" validate:"required"`
|
||||
Price float64 `json:"price" gorm:"column:price" validate:"required"`
|
||||
// bool false
|
||||
// nil 是零值, false
|
||||
IsSale *bool `json:"is_sale" gorm:"column:is_sale"`
|
||||
}
|
||||
|
||||
type Book struct {
|
||||
// 对象Id
|
||||
Id uint `json:"id" gorm:"primaryKey;column:id"`
|
||||
@ -30,6 +20,16 @@ func (b *Book) String() string {
|
||||
return pretty.ToJSON(b)
|
||||
}
|
||||
|
||||
type BookSpec struct {
|
||||
// type 用于要使用gorm 来自动创建和更新表的时候 才需要定义
|
||||
Title string `json:"title" gorm:"column:title;type:varchar(200)" validate:"required"`
|
||||
Author string `json:"author" gorm:"column:author;type:varchar(200);index" validate:"required"`
|
||||
Price float64 `json:"price" gorm:"column:price" validate:"required"`
|
||||
// bool false
|
||||
// nil 是零值, false
|
||||
IsSale *bool `json:"is_sale" gorm:"column:is_sale"`
|
||||
}
|
||||
|
||||
// books
|
||||
func (b *Book) TableName() string {
|
||||
return "books"
|
||||
|
@ -10,7 +10,6 @@ import (
|
||||
// 2. 正常直接返回数据, Restful接口 怎么知道这些请求是成功还是失败喃? 通过HTTP判断 2xx
|
||||
// 如果后面 所有的返回数据 要进过特殊处理,都在这个函数内进行扩展,方便维护,比如 数据脱敏
|
||||
func OK(ctx *gin.Context, data any) {
|
||||
// v, ok := data.(Densener)
|
||||
ctx.JSON(200, data)
|
||||
ctx.Abort()
|
||||
}
|
||||
|
@ -1,28 +1 @@
|
||||
# 业务分区架构(基于mcube)
|
||||
|
||||
mcube 与 Ioc
|
||||
|
||||

|
||||
|
||||
更新部分:
|
||||
1. 单元测试 支持通过环境变量注入,优化单元测试配置,共用一套配置
|
||||
2. 新增Book Api项目, 从简单的脚本开发->配置分离->mvc模式->ioc业务分区 经历4个版本,讲解如何开发复杂项目。
|
||||
3. Vblog项目 新增部署,支持2中部署模式,1.前后端分离部署 与 前后端打包一体的部署。
|
||||
4. 优化其他几个项目,支持 可以通过 import的方式,快速使用。
|
||||
5. cmdb 云商凭证 支持加密存储
|
||||
|
||||
|
||||
## 业务分区的第一步 定义业务(RPC)
|
||||
|
||||
Book/Comment: 这个业务模块提供的功能
|
||||
|
||||
|
||||
## 具体实现
|
||||
|
||||
|
||||
|
||||
## 面向接口
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,23 +0,0 @@
|
||||
[app]
|
||||
name = "simple_api"
|
||||
description = "app desc"
|
||||
address = "localhost"
|
||||
encrypt_key = "defualt app encrypt key"
|
||||
|
||||
[datasource]
|
||||
provider = "mysql"
|
||||
host = "127.0.0.1"
|
||||
port = 3306
|
||||
database = "go18"
|
||||
username = "root"
|
||||
password = "123456"
|
||||
auto_migrate = false
|
||||
debug = false
|
||||
|
||||
[http]
|
||||
host = "127.0.0.1"
|
||||
port = 8010
|
||||
path_prefix = "api"
|
||||
|
||||
[comment]
|
||||
max_comment_per_book = 200
|
@ -1 +0,0 @@
|
||||
# 业务分区
|
@ -1,322 +0,0 @@
|
||||
# Book 业务分区
|
||||
|
||||
## 定义Book业务逻辑
|
||||
|
||||
业务功能: CRUD
|
||||
1. 创建书籍(录入)
|
||||
2. Book列表查询
|
||||
3. Book详情查询
|
||||
4. Book更新
|
||||
5. Book删除
|
||||
|
||||
通过Go语言的里面的接口 来定义描述业务功能
|
||||
|
||||
```go
|
||||
// book.Service, Book的业务定义
|
||||
type Service interface {
|
||||
// 1. 创建书籍(录入)
|
||||
CreateBook(context.Context, *CreateBookRequest) (*Book, error)
|
||||
// 2. Book列表查询
|
||||
QueryBook(context.Context, *QueryBookRequest) (*types.Set[*Book], error)
|
||||
// 3. Book详情查询
|
||||
DescribeBook(context.Context, *DescribeBookRequest) (*Book, error)
|
||||
// 4. Book更新
|
||||
UpdateBook(context.Context, *UpdateBookRequest) (*Book, error)
|
||||
// 5. Book删除
|
||||
DeleteBook(context.Context, *DeleteBookRequest) error
|
||||
}
|
||||
```
|
||||
|
||||
## 业务的具体实现(TDD: Test Drive Develop)
|
||||
|
||||
1. BookServiceImpl
|
||||
|
||||
```go
|
||||
// CreateBook implements book.Service.
|
||||
func (b *BookServiceImpl) CreateBook(context.Context, *book.CreateBookRequest) (*book.Book, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// DeleteBook implements book.Service.
|
||||
func (b *BookServiceImpl) DeleteBook(context.Context, *book.DeleteBookRequest) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// DescribeBook implements book.Service.
|
||||
func (b *BookServiceImpl) DescribeBook(context.Context, *book.DescribeBookRequest) (*book.Book, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// QueryBook implements book.Service.
|
||||
func (b *BookServiceImpl) QueryBook(context.Context, *book.QueryBookRequest) (*types.Set[*book.Book], error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// UpdateBook implements book.Service.
|
||||
func (b *BookServiceImpl) UpdateBook(context.Context, *book.UpdateBookRequest) (*book.Book, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
```
|
||||
|
||||
## 编写单元测试
|
||||
|
||||
```go
|
||||
func TestCreateBook(t *testing.T) {
|
||||
req := book.NewCreateBookRequest()
|
||||
req.SetIsSale(true)
|
||||
req.Title = "Go语言V4"
|
||||
req.Author = "will"
|
||||
req.Price = 10
|
||||
ins, err := svc.CreateBook(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(ins)
|
||||
}
|
||||
```
|
||||
|
||||
## 业务对象注册(ioc controller)
|
||||
|
||||
手动维护
|
||||
```sh
|
||||
pkg gloab
|
||||
|
||||
bookContrller = xxx
|
||||
commentContrller = xx
|
||||
...
|
||||
```
|
||||
|
||||
通过容器来维护对象
|
||||
|
||||
```go
|
||||
// Book业务的具体实现
|
||||
type BookServiceImpl struct {
|
||||
ioc.ObjectImpl
|
||||
}
|
||||
|
||||
// 返回对象的名称, 因此我需要 服务名称
|
||||
// 当前的MySQLBookServiceImpl 是 service book.APP_NAME 的 一个具体实现
|
||||
// 当前的MongoDBBookServiceImpl 是 service book.APP_NAME 的 一个具体实现
|
||||
func (s *BookServiceImpl) Name() string {
|
||||
return book.APP_NAME
|
||||
}
|
||||
|
||||
func init() {
|
||||
ioc.Controller().Registry(&BookServiceImpl{})
|
||||
}
|
||||
```
|
||||
|
||||
## 面向接口
|
||||
|
||||
对象取处理, 断言他满足业务接口,然后我们以接口的方式来使用
|
||||
|
||||
```go
|
||||
func GetService() Service {
|
||||
return ioc.Controller().Get(APP_NAME).(Service)
|
||||
}
|
||||
|
||||
const (
|
||||
APP_NAME = "book"
|
||||
)
|
||||
```
|
||||
|
||||
第三方模块,可以依赖 接口进行开发
|
||||
```go
|
||||
// AddComment implements comment.Service.
|
||||
func (c *CommentServiceImpl) AddComment(ctx context.Context, in *comment.AddCommentRequest) (*comment.Comment, error) {
|
||||
// 能不能 直接Book Service的具体实现
|
||||
// (&impl.BookServiceImpl{}).DescribeBook(ctx, nil)
|
||||
// 依赖接口,面向接口编程, 不依赖具体实现
|
||||
book.GetService().DescribeBook(ctx, nil)
|
||||
panic("unimplemented")
|
||||
}
|
||||
```
|
||||
|
||||
## 开发API
|
||||
|
||||
接口是需求, 对业务进行设计, 可以选择把这些能力 以那种接口的访问对外提供服务
|
||||
|
||||
1. 不对外提供接口,仅仅作为其他的业务的依赖
|
||||
2. (API)对外提供 HTTP接口, RESTful接口
|
||||
3. (API)对内提供 RPC接口(JSON RPC/GRPC/thrift)
|
||||
|
||||
1. 开发业务功能
|
||||
```go
|
||||
func (h *BookApiHandler) createBook(ctx *gin.Context) {
|
||||
req := book.NewCreateBookRequest()
|
||||
if err := ctx.BindJSON(req); err != nil {
|
||||
response.Failed(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
ins, err := h.svc.CreateBook(ctx.Request.Context(), req)
|
||||
if err != nil {
|
||||
response.Failed(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
response.Success(ctx, ins)
|
||||
}
|
||||
```
|
||||
|
||||
2. 注册路由
|
||||
```go
|
||||
type BookApiHandler struct {
|
||||
ioc.ObjectImpl
|
||||
|
||||
// 业务依赖
|
||||
svc book.Service
|
||||
}
|
||||
|
||||
func (h *BookApiHandler) Name() string {
|
||||
return "books"
|
||||
}
|
||||
|
||||
// 对象的初始化, 初始化对象的一些熟悉 &BookApiHandler{}
|
||||
// 构造函数
|
||||
// 当你这个对象初始化的时候,直接把的处理函数(ApiHandler注册给Server)
|
||||
func (h *BookApiHandler) Init() error {
|
||||
h.svc = book.GetService()
|
||||
r := ioc_gin.ObjectRouter(h)
|
||||
|
||||
r.GET("", h.queryBook)
|
||||
r.POST("", h.createBook)
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
ioc.Api().Registry(&BookApiHandler{})
|
||||
}
|
||||
```
|
||||
|
||||
## 业务注册
|
||||
|
||||
每写完一个业务,就需要在 注册到ioc(注册表)
|
||||
```go
|
||||
// 业务加载区, 选择性的价值的业务处理对象
|
||||
|
||||
import (
|
||||
// Api Impl
|
||||
_ "122.51.31.227/go-course/go18/book/v4/apps/book/api"
|
||||
|
||||
// Service Impl
|
||||
_ "122.51.31.227/go-course/go18/book/v4/apps/book/impl"
|
||||
_ "122.51.31.227/go-course/go18/book/v4/apps/comment/impl"
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
## 启动服务
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/infraboard/mcube/v2/ioc/server/cmd"
|
||||
|
||||
// 业务对象
|
||||
_ "122.51.31.227/go-course/go18/book/v4/apps"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// ioc框架 加载对象, 注入对象, 配置对象
|
||||
// server.Gin.Run()
|
||||
// application.Get().AppName
|
||||
// http.Get().Host
|
||||
// server.DefaultConfig.ConfigFile.Enabled = true
|
||||
// server.DefaultConfig.ConfigFile.Path = "application.toml"
|
||||
// server.Run(context.Background())
|
||||
// 不能指定配置文件逻辑
|
||||
// 使用者来说,体验不佳
|
||||
|
||||
// ioc 直接提供server, 直接run就行了,
|
||||
// mcube 包含 一个 gin Engine
|
||||
// CLI, start 指令 -f 指定配置文件
|
||||
cmd.Start()
|
||||
}
|
||||
```
|
||||
|
||||
```toml
|
||||
[app]
|
||||
name = "simple_api"
|
||||
description = "app desc"
|
||||
address = "localhost"
|
||||
encrypt_key = "defualt app encrypt key"
|
||||
|
||||
[datasource]
|
||||
provider = "mysql"
|
||||
host = "127.0.0.1"
|
||||
port = 3306
|
||||
database = "go18"
|
||||
username = "root"
|
||||
password = "123456"
|
||||
auto_migrate = false
|
||||
debug = false
|
||||
|
||||
[http]
|
||||
host = "127.0.0.1"
|
||||
port = 8010
|
||||
path_prefix = "api"
|
||||
|
||||
[comment]
|
||||
max_comment_per_book = 200
|
||||
```
|
||||
|
||||
```sh
|
||||
➜ v4 git:(main) ✗ go run main.go -f application.toml start
|
||||
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
|
||||
|
||||
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
|
||||
- using env: export GIN_MODE=release
|
||||
- using code: gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
2025-05-25T15:59:33+08:00 INFO config/gin/framework.go:41 > enable gin recovery component:GIN_WEBFRAMEWORK
|
||||
200
|
||||
[GIN-debug] GET /api/simple_api/v1/books --> 122.51.31.227/go-course/go18/book/v4/apps/book/api.(*BookApiHandler).queryBook-fm (4 handlers)
|
||||
[GIN-debug] POST /api/simple_api/v1/books --> 122.51.31.227/go-course/go18/book/v4/apps/book/api.(*BookApiHandler).createBook-fm (4 handlers)
|
||||
2025-05-25T15:59:33+08:00 INFO ioc/server/server.go:74 > loaded configs: [app.v1 trace.v1 log.v1 validator.v1 gin_webframework.v1 datasource.v1 grpc.v1 http.v1] component:SERVER
|
||||
2025-05-25T15:59:33+08:00 INFO ioc/server/server.go:75 > loaded controllers: [comment.v1 book.v1] component:SERVER
|
||||
2025-05-25T15:59:33+08:00 INFO ioc/server/server.go:76 > loaded apis: [books.v1] component:SERVER
|
||||
2025-05-25T15:59:33+08:00 INFO ioc/server/server.go:77 > loaded defaults: [] component:SERVER
|
||||
2025-05-25T15:59:33+08:00 INFO config/http/http.go:144 > HTTP服务启动成功, 监听地址: 127.0.0.1:8010 component:HTTP
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
业务分区框架, 我们专注于业务对象的开发, mcube相对于一个工具箱,承接其他非业务的公共功能
|
||||
|
||||
|
||||
## 其他非功能需求
|
||||
|
||||
工具箱 提供很多工具,开箱即用, 比如health check, 比如metrics
|
||||
|
||||
```go
|
||||
// 健康检查
|
||||
_ "github.com/infraboard/mcube/v2/ioc/apps/health/gin"
|
||||
// metrics
|
||||
_ "github.com/infraboard/mcube/v2/ioc/apps/metric/gin"
|
||||
```
|
||||
|
||||
|
||||
[metric.v1 books.v1 health.v1] metric, health 使用注入的对象
|
||||
```sh
|
||||
➜ v4 git:(main) ✗ go run main.go -f application.toml start
|
||||
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
|
||||
|
||||
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
|
||||
- using env: export GIN_MODE=release
|
||||
- using code: gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
2025-05-25T16:06:42+08:00 INFO config/gin/framework.go:41 > enable gin recovery component:GIN_WEBFRAMEWORK
|
||||
200
|
||||
[GIN-debug] GET /metrics/ --> github.com/infraboard/mcube/v2/ioc/apps/metric/gin.(*ginHandler).Registry.func1 (5 handlers)
|
||||
2025-05-25T16:06:42+08:00 INFO metric/gin/metric.go:89 > Get the Metric using http://127.0.0.1:8010/metrics component:METRIC
|
||||
[GIN-debug] GET /api/simple_api/v1/books --> 122.51.31.227/go-course/go18/book/v4/apps/book/api.(*BookApiHandler).queryBook-fm (5 handlers)
|
||||
[GIN-debug] POST /api/simple_api/v1/books --> 122.51.31.227/go-course/go18/book/v4/apps/book/api.(*BookApiHandler).createBook-fm (5 handlers)
|
||||
[GIN-debug] GET /healthz/ --> github.com/infraboard/mcube/v2/ioc/apps/health/gin.(*HealthChecker).HealthHandleFunc-fm (5 handlers)
|
||||
2025-05-25T16:06:42+08:00 INFO health/gin/check.go:55 > Get the Health using http://127.0.0.1:8010/healthz component:HEALTH_CHECK
|
||||
2025-05-25T16:06:42+08:00 INFO ioc/server/server.go:74 > loaded configs: [app.v1 trace.v1 log.v1 validator.v1 gin_webframework.v1 datasource.v1 grpc.v1 http.v1] component:SERVER
|
||||
2025-05-25T16:06:42+08:00 INFO ioc/server/server.go:75 > loaded controllers: [comment.v1 book.v1] component:SERVER
|
||||
2025-05-25T16:06:42+08:00 INFO ioc/server/server.go:76 > loaded apis: [metric.v1 books.v1 health.v1] component:SERVER
|
||||
2025-05-25T16:06:42+08:00 INFO ioc/server/server.go:77 > loaded defaults: [] component:SERVER
|
||||
2025-05-25T16:06:42+08:00 INFO config/http/http.go:144 > HTTP服务启动成功, 监听地址: 127.0.0.1:8010 component:HTTP
|
||||
```
|
@ -1,55 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"122.51.31.227/go-course/go18/book/v4/apps/book"
|
||||
"github.com/infraboard/mcube/v2/ioc"
|
||||
|
||||
// 引入Gin Root Router: *gin.Engine
|
||||
ioc_gin "github.com/infraboard/mcube/v2/ioc/config/gin"
|
||||
// 引入Gin Root Router: *gin.Engine
|
||||
)
|
||||
|
||||
type BookApiHandler struct {
|
||||
ioc.ObjectImpl
|
||||
|
||||
// 业务依赖
|
||||
svc book.Service
|
||||
}
|
||||
|
||||
// 这个就是 API 的资源名称
|
||||
// /api/book/v1/books
|
||||
func (h *BookApiHandler) Name() string {
|
||||
return "books"
|
||||
}
|
||||
|
||||
// 对象的初始化, 初始化对象的一些熟悉 &BookApiHandler{}
|
||||
// 构造函数
|
||||
// 当你这个对象初始化的时候,直接把的处理函数(ApiHandler注册给Server)
|
||||
func (h *BookApiHandler) Init() error {
|
||||
h.svc = book.GetService()
|
||||
|
||||
// 本地依赖
|
||||
// r := server.Gin
|
||||
// 框架托管, 通过容器获取 Server对象
|
||||
// 获取的 Gin Engine对象
|
||||
// ioc_gin.RootRouter()
|
||||
// URL 容器冲突, book/comment
|
||||
// 怎么避免 2个业务API 不不冲突,加上业务板块的前缀,或者 对的名称
|
||||
// /<prefix>/<service_name>/<object_version>/<object_name>
|
||||
// http 接口前缀
|
||||
r := ioc_gin.ObjectRouter(h)
|
||||
|
||||
// Book Restful API
|
||||
// List of books
|
||||
// /api/simple_api/v1/books
|
||||
r.GET("", h.queryBook)
|
||||
// Create new book
|
||||
// Body: HTTP Entity
|
||||
// /api/simple_api/v1/books
|
||||
r.POST("", h.createBook)
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
ioc.Api().Registry(&BookApiHandler{})
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"122.51.31.227/go-course/go18/book/v4/apps/book"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/infraboard/mcube/v2/http/gin/response"
|
||||
)
|
||||
|
||||
func (h *BookApiHandler) queryBook(ctx *gin.Context) {
|
||||
// 给默认值
|
||||
req := book.NewQueryBookRequest()
|
||||
req.Keywords = ctx.Query("keywords")
|
||||
// /api/books?page_number=1&page_size=20
|
||||
pageNumber := ctx.Query("page_number")
|
||||
if pageNumber != "" {
|
||||
pnInt, err := strconv.ParseInt(pageNumber, 10, 64)
|
||||
if err != nil {
|
||||
response.Failed(ctx, err)
|
||||
return
|
||||
}
|
||||
req.PageNumber = uint64(pnInt)
|
||||
}
|
||||
|
||||
pageSize := ctx.Query("page_size")
|
||||
if pageSize != "" {
|
||||
psInt, err := strconv.ParseInt(pageSize, 10, 64)
|
||||
if err != nil {
|
||||
response.Failed(ctx, err)
|
||||
return
|
||||
}
|
||||
req.PageSize = uint64(psInt)
|
||||
}
|
||||
|
||||
set, err := h.svc.QueryBook(ctx.Request.Context(), req)
|
||||
if err != nil {
|
||||
// 针对Response的统一封装, 已经落到 mcube
|
||||
response.Failed(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(ctx, set)
|
||||
}
|
||||
|
||||
func (h *BookApiHandler) createBook(ctx *gin.Context) {
|
||||
req := book.NewCreateBookRequest()
|
||||
if err := ctx.BindJSON(req); err != nil {
|
||||
response.Failed(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
ins, err := h.svc.CreateBook(ctx.Request.Context(), req)
|
||||
if err != nil {
|
||||
response.Failed(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
response.Success(ctx, ins)
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram id="j5aaZ-Vtlo4HorS1MuuZ" name="第 1 页">
|
||||
<mxGraphModel dx="1657" dy="1468" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="3" value="BookServiceImpl" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="70" y="210" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="4" value="CommentServiceImpl" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="250" y="210" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="读取配置<div>调用Init</div>" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="6" target="12">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="6" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="70" y="-90" width="460" height="240" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="7" value="Controllers" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="150" y="65" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="8" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="3" target="7">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="9" value="import" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="8">
|
||||
<mxGeometry x="-0.3065" y="1" relative="1" as="geometry">
|
||||
<mxPoint y="1" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="10" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="4" target="7">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="11" value="import" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="10">
|
||||
<mxGeometry x="0.1423" y="-4" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="12" value="application.toml" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="610" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="13" value="Config" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="384" y="65" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="14" value="面向接口" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="140" y="300" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="15" value="面向具体对象" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="430" y="300" width="80" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="16" value="Api" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="240" y="-60" width="160" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="18" style="edgeStyle=none;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="17" target="16">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="19" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="17" target="7">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="21" value="依赖Book Service" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="19">
|
||||
<mxGeometry x="-0.0899" y="2" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="17" value="BookApiHandler" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="-90" y="-50" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
@ -1,3 +0,0 @@
|
||||
# 业务实现包
|
||||
|
||||
ServiceImpl(book.Service)
|
@ -1,68 +0,0 @@
|
||||
package impl
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"122.51.31.227/go-course/go18/book/v4/apps/book"
|
||||
"github.com/infraboard/mcube/v2/exception"
|
||||
"github.com/infraboard/mcube/v2/types"
|
||||
|
||||
// 自动解析配置文件里面, 相应的部分
|
||||
"github.com/infraboard/mcube/v2/ioc/config/datasource"
|
||||
)
|
||||
|
||||
// CreateBook implements book.Service.
|
||||
func (b *BookServiceImpl) CreateBook(ctx context.Context, in *book.CreateBookRequest) (*book.Book, error) {
|
||||
// 自定义异常改造, 放到mcube
|
||||
// 自定义异常, exception 包, 统一放到一个公共库里面, mcube
|
||||
if err := in.Validate(); err != nil {
|
||||
return nil, exception.NewBadRequest("校验Book创建失败, %s", err)
|
||||
}
|
||||
|
||||
bookInstance := &book.Book{CreateBookRequest: *in}
|
||||
|
||||
// config对象改造
|
||||
// 数据入库(Grom), 补充自增Id的值
|
||||
if err := datasource.DBFromCtx(ctx).Save(bookInstance).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bookInstance, nil
|
||||
}
|
||||
|
||||
// DeleteBook implements book.Service.
|
||||
func (b *BookServiceImpl) DeleteBook(context.Context, *book.DeleteBookRequest) error {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// DescribeBook implements book.Service.
|
||||
func (b *BookServiceImpl) DescribeBook(context.Context, *book.DescribeBookRequest) (*book.Book, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// QueryBook implements book.Service.
|
||||
func (b *BookServiceImpl) QueryBook(ctx context.Context, in *book.QueryBookRequest) (*types.Set[*book.Book], error) {
|
||||
set := types.New[*book.Book]()
|
||||
|
||||
query := datasource.DBFromCtx(ctx).Model(&book.Book{})
|
||||
// 关键字过滤
|
||||
if in.Keywords != "" {
|
||||
query = query.Where("title LIKE ?", "%"+in.Keywords+"%")
|
||||
}
|
||||
|
||||
if err := query.
|
||||
Count(&set.Total).
|
||||
Offset(int(in.ComputeOffset())).
|
||||
Limit(int(in.PageSize)).
|
||||
Find(&set.Items).
|
||||
Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return set, nil
|
||||
}
|
||||
|
||||
// UpdateBook implements book.Service.
|
||||
func (b *BookServiceImpl) UpdateBook(context.Context, *book.UpdateBookRequest) (*book.Book, error) {
|
||||
panic("unimplemented")
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
package impl_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"122.51.31.227/go-course/go18/book/v4/apps/book"
|
||||
)
|
||||
|
||||
func TestCreateBook(t *testing.T) {
|
||||
req := book.NewCreateBookRequest()
|
||||
req.SetIsSale(true)
|
||||
req.Title = "Go语言V4"
|
||||
req.Author = "will"
|
||||
req.Price = 10
|
||||
ins, err := svc.CreateBook(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(ins)
|
||||
}
|
||||
|
||||
func TestQueryBook(t *testing.T) {
|
||||
req := book.NewQueryBookRequest()
|
||||
ins, err := svc.QueryBook(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(ins)
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
package impl
|
||||
|
||||
import (
|
||||
"122.51.31.227/go-course/go18/book/v4/apps/book"
|
||||
"github.com/infraboard/mcube/v2/ioc"
|
||||
)
|
||||
|
||||
// 写好一个业务对象(业务实现),就把这个对象,注册到一个公共空间(ioc Controller Namespace)
|
||||
// mcube 提供这个空间 ioc.Controller().Registry 把对象注册过去
|
||||
// 提供对象的名称, 对象的初始化方法
|
||||
|
||||
// 怎么知道他有没有实现该业务, 可以通过类型约束
|
||||
// var _ book.Service = &BookServiceImpl{}
|
||||
|
||||
// &BookServiceImpl 的 nil对象
|
||||
//
|
||||
// int64(1) int64 1
|
||||
// *BookServiceImpl(nil)
|
||||
var _ book.Service = (*BookServiceImpl)(nil)
|
||||
|
||||
// Book业务的具体实现
|
||||
type BookServiceImpl struct {
|
||||
ioc.ObjectImpl
|
||||
}
|
||||
|
||||
// 返回对象的名称, 因此我需要 服务名称
|
||||
// 当前的MySQLBookServiceImpl 是 service book.APP_NAME 的 一个具体实现
|
||||
// 当前的MongoDBBookServiceImpl 是 service book.APP_NAME 的 一个具体实现
|
||||
func (s *BookServiceImpl) Name() string {
|
||||
return book.APP_NAME
|
||||
}
|
||||
|
||||
func init() {
|
||||
ioc.Controller().Registry(&BookServiceImpl{})
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package impl_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"122.51.31.227/go-course/go18/book/v4/apps/book"
|
||||
"122.51.31.227/go-course/go18/book/v4/test"
|
||||
)
|
||||
|
||||
var ctx = context.Background()
|
||||
var svc book.Service
|
||||
|
||||
func init() {
|
||||
test.DevelopmentSet()
|
||||
|
||||
svc = book.GetService()
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
package book
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/infraboard/mcube/v2/http/request"
|
||||
"github.com/infraboard/mcube/v2/ioc"
|
||||
"github.com/infraboard/mcube/v2/ioc/config/validator"
|
||||
"github.com/infraboard/mcube/v2/types"
|
||||
)
|
||||
|
||||
func GetService() Service {
|
||||
return ioc.Controller().Get(APP_NAME).(Service)
|
||||
}
|
||||
|
||||
const (
|
||||
APP_NAME = "book"
|
||||
)
|
||||
|
||||
// book.Service, Book的业务定义
|
||||
type Service interface {
|
||||
// 1. 创建书籍(录入)
|
||||
CreateBook(context.Context, *CreateBookRequest) (*Book, error)
|
||||
// 2. Book列表查询
|
||||
QueryBook(context.Context, *QueryBookRequest) (*types.Set[*Book], error)
|
||||
// 3. Book详情查询
|
||||
DescribeBook(context.Context, *DescribeBookRequest) (*Book, error)
|
||||
// 4. Book更新
|
||||
UpdateBook(context.Context, *UpdateBookRequest) (*Book, error)
|
||||
// 5. Book删除
|
||||
DeleteBook(context.Context, *DeleteBookRequest) error
|
||||
}
|
||||
|
||||
type DeleteBookRequest struct {
|
||||
DescribeBookRequest
|
||||
}
|
||||
|
||||
type UpdateBookRequest struct {
|
||||
DescribeBookRequest
|
||||
CreateBookRequest
|
||||
}
|
||||
|
||||
type DescribeBookRequest struct {
|
||||
Id uint
|
||||
}
|
||||
|
||||
type BookSet struct {
|
||||
// 总共多少个
|
||||
Total int64 `json:"total"`
|
||||
// book清单
|
||||
Items []*Book `json:"items"`
|
||||
}
|
||||
|
||||
func (b *BookSet) Add(item *Book) {
|
||||
b.Items = append(b.Items, item)
|
||||
}
|
||||
|
||||
// type CommentSet struct {
|
||||
// // 总共多少个
|
||||
// Total int64 `json:"total"`
|
||||
// // book清单
|
||||
// Items []*Comment `json:"items"`
|
||||
// }
|
||||
|
||||
// func (b *CommentSet) Add(item *Comment) {
|
||||
// b.Items = append(b.Items, item)
|
||||
// }
|
||||
|
||||
func NewCreateBookRequest() *CreateBookRequest {
|
||||
return (&CreateBookRequest{}).SetIsSale(false)
|
||||
}
|
||||
|
||||
type CreateBookRequest struct {
|
||||
// type 用于要使用gorm 来自动创建和更新表的时候 才需要定义
|
||||
Title string `json:"title" gorm:"column:title;type:varchar(200)" validate:"required"`
|
||||
Author string `json:"author" gorm:"column:author;type:varchar(200);index" validate:"required"`
|
||||
Price float64 `json:"price" gorm:"column:price" validate:"required"`
|
||||
// bool false
|
||||
// nil 是零值, false
|
||||
IsSale *bool `json:"is_sale" gorm:"column:is_sale"`
|
||||
}
|
||||
|
||||
// 这个请求对象的教育
|
||||
func (r *CreateBookRequest) Validate() error {
|
||||
// validate := validator.New()
|
||||
// validate.Struct(r)
|
||||
return validator.Validate(r)
|
||||
}
|
||||
|
||||
func (r *CreateBookRequest) SetIsSale(v bool) *CreateBookRequest {
|
||||
r.IsSale = &v
|
||||
return r
|
||||
}
|
||||
|
||||
func NewQueryBookRequest() *QueryBookRequest {
|
||||
return &QueryBookRequest{
|
||||
// PageRequest{PageSize:20, PageNumber: 1}
|
||||
PageRequest: *request.NewDefaultPageRequest(),
|
||||
}
|
||||
}
|
||||
|
||||
type QueryBookRequest struct {
|
||||
// PageSize uint
|
||||
// PageNumber uint
|
||||
request.PageRequest
|
||||
// 关键字参数
|
||||
Keywords string `json:"keywords"`
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package book
|
||||
|
||||
import "github.com/infraboard/mcube/v2/tools/pretty"
|
||||
|
||||
type Book struct {
|
||||
// 对象Id
|
||||
Id uint `json:"id" gorm:"primaryKey;column:id"`
|
||||
|
||||
CreateBookRequest
|
||||
}
|
||||
|
||||
func (b *Book) String() string {
|
||||
return pretty.ToJSON(b)
|
||||
}
|
||||
|
||||
// books
|
||||
func (b *Book) TableName() string {
|
||||
return "books"
|
||||
}
|
@ -1 +0,0 @@
|
||||
# 评论模块
|
@ -1,17 +0,0 @@
|
||||
package impl
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"122.51.31.227/go-course/go18/book/v4/apps/book"
|
||||
"122.51.31.227/go-course/go18/book/v4/apps/comment"
|
||||
)
|
||||
|
||||
// AddComment implements comment.Service.
|
||||
func (c *CommentServiceImpl) AddComment(ctx context.Context, in *comment.AddCommentRequest) (*comment.Comment, error) {
|
||||
// 能不能 直接Book Service的具体实现
|
||||
// (&impl.BookServiceImpl{}).DescribeBook(ctx, nil)
|
||||
// 依赖接口,面向接口编程, 不依赖具体实现
|
||||
book.GetService().DescribeBook(ctx, nil)
|
||||
panic("unimplemented")
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package impl_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"122.51.31.227/go-course/go18/book/v4/apps/comment"
|
||||
)
|
||||
|
||||
func TestAddComment(t *testing.T) {
|
||||
ins, err := svc.AddComment(ctx, &comment.AddCommentRequest{
|
||||
BookId: 10,
|
||||
Comment: "评论测试",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(ins)
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
package impl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"122.51.31.227/go-course/go18/book/v4/apps/comment"
|
||||
"github.com/infraboard/mcube/v2/ioc"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ioc.Controller().Registry(&CommentServiceImpl{
|
||||
MaxCommentPerBook: 100,
|
||||
})
|
||||
}
|
||||
|
||||
// 怎么知道他有没有实现该业务, 可以通过类型约束
|
||||
// var _ book.Service = &BookServiceImpl{}
|
||||
|
||||
// &BookServiceImpl 的 nil对象
|
||||
//
|
||||
// int64(1) int64 1
|
||||
// *BookServiceImpl(nil)
|
||||
var _ comment.Service = (*CommentServiceImpl)(nil)
|
||||
|
||||
// Book业务的具体实现
|
||||
type CommentServiceImpl struct {
|
||||
ioc.ObjectImpl
|
||||
|
||||
// Comment最大限制
|
||||
MaxCommentPerBook int `toml:"max_comment_per_book"`
|
||||
}
|
||||
|
||||
func (s *CommentServiceImpl) Init() error {
|
||||
// 当前对象,已经读取了配置文件
|
||||
fmt.Println(s.MaxCommentPerBook)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CommentServiceImpl) Name() string {
|
||||
return comment.APP_NAME
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package impl_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"122.51.31.227/go-course/go18/book/v4/apps/comment/impl"
|
||||
"github.com/infraboard/mcube/v2/ioc"
|
||||
)
|
||||
|
||||
var ctx = context.Background()
|
||||
var svc = impl.CommentServiceImpl{}
|
||||
|
||||
func init() {
|
||||
// import 后自动执行的逻辑
|
||||
// 工具对象的初始化, 需要的是绝对路径
|
||||
ioc.DevelopmentSetupWithPath(os.Getenv("workspaceFolder") + "/book/v4/application.toml")
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package comment
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const (
|
||||
APP_NAME = "comment"
|
||||
)
|
||||
|
||||
// comment.Service, Comment的业务定义
|
||||
type Service interface {
|
||||
// 为书籍添加评论
|
||||
AddComment(context.Context, *AddCommentRequest) (*Comment, error)
|
||||
}
|
||||
|
||||
type AddCommentRequest struct {
|
||||
BookId uint
|
||||
Comment string
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
package comment
|
||||
|
||||
type Comment struct {
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package apps
|
||||
|
||||
// 业务加载区, 选择性的价值的业务处理对象
|
||||
|
||||
import (
|
||||
// Api Impl
|
||||
_ "122.51.31.227/go-course/go18/book/v4/apps/book/api"
|
||||
|
||||
// Service Impl
|
||||
_ "122.51.31.227/go-course/go18/book/v4/apps/book/impl"
|
||||
_ "122.51.31.227/go-course/go18/book/v4/apps/comment/impl"
|
||||
)
|
@ -1,16 +0,0 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram id="Ak93bPvnUy-TIIMCp3wx" name="第 1 页">
|
||||
<mxGraphModel dx="777" dy="534" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="2" value="Book" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="90" y="260" width="170" height="90" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="3" value="Comment" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="380" y="260" width="170" height="90" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
Binary file not shown.
Before Width: | Height: | Size: 61 KiB |
@ -1,30 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/infraboard/mcube/v2/ioc/server/cmd"
|
||||
|
||||
// 业务对象
|
||||
_ "122.51.31.227/go-course/go18/book/v4/apps"
|
||||
|
||||
// 健康检查
|
||||
_ "github.com/infraboard/mcube/v2/ioc/apps/health/gin"
|
||||
// metrics
|
||||
_ "github.com/infraboard/mcube/v2/ioc/apps/metric/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// ioc框架 加载对象, 注入对象, 配置对象
|
||||
// server.Gin.Run()
|
||||
// application.Get().AppName
|
||||
// http.Get().Host
|
||||
// server.DefaultConfig.ConfigFile.Enabled = true
|
||||
// server.DefaultConfig.ConfigFile.Path = "application.toml"
|
||||
// server.Run(context.Background())
|
||||
// 不能指定配置文件逻辑
|
||||
// 使用者来说,体验不佳
|
||||
|
||||
// ioc 直接提供server, 直接run就行了,
|
||||
// mcube 包含 一个 gin Engine
|
||||
// CLI, start 指令 -f 指定配置文件
|
||||
cmd.Start()
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package server
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
var Gin = gin.Default()
|
||||
|
||||
// ObjectRouter
|
@ -1,53 +0,0 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram id="6JtAa5eU4ERA-y8NrUjI" name="第 1 页">
|
||||
<mxGraphModel dx="777" dy="471" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="2" value="BookServiceImpl" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="210" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="3" value="CommentServiceImpl" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="220" y="210" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="10" value="init" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="4" target="9">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="4" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="40" width="460" height="110" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="Controllers" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="120" y="65" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="6" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="2" target="5">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="8" value="import" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="6">
|
||||
<mxGeometry x="-0.3065" y="1" relative="1" as="geometry">
|
||||
<mxPoint y="1" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="7" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="3" target="5">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="13" value="import" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="7">
|
||||
<mxGeometry x="0.1423" y="-4" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="9" value="application.toml" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="580" y="65" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="11" value="Config" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="354" y="65" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="15" value="面向接口" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="110" y="300" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="16" value="面向具体对象" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="400" y="300" width="80" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
@ -1,17 +0,0 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/infraboard/mcube/v2/ioc"
|
||||
// 要注册哪些对象, Book, Comment
|
||||
|
||||
_ "122.51.31.227/go-course/go18/book/v4/apps/book/impl"
|
||||
_ "122.51.31.227/go-course/go18/book/v4/apps/comment/impl"
|
||||
)
|
||||
|
||||
func DevelopmentSet() {
|
||||
// import 后自动执行的逻辑
|
||||
// 工具对象的初始化, 需要的是绝对路径
|
||||
ioc.DevelopmentSetupWithPath(os.Getenv("workspaceFolder") + "/book/v4/application.toml")
|
||||
}
|
17
devcloud/.vscode/launch.json
vendored
17
devcloud/.vscode/launch.json
vendored
@ -1,17 +0,0 @@
|
||||
{
|
||||
// 使用 IntelliSense 了解相关属性。
|
||||
// 悬停以查看现有属性的描述。
|
||||
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "mcenter api server",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/main.go",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"args": ["start","-f","${workspaceFolder}/etc/application.toml"]
|
||||
}
|
||||
]
|
||||
}
|
7
devcloud/.vscode/settings.json
vendored
7
devcloud/.vscode/settings.json
vendored
@ -1,7 +0,0 @@
|
||||
{
|
||||
"go.testEnvVars": {
|
||||
"workspaceFolder": "${workspaceFolder}",
|
||||
"CONFIG_PATH": "${workspaceFolder}/etc/application.toml"
|
||||
},
|
||||
"go.testEnvFile": "${workspaceFolder}/etc/unit_test.env"
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
PKG := "122.51.31.227/go-course/go18/devcloud"
|
||||
MOD_DIR := $(shell go env GOPATH)/pkg/mod
|
||||
PKG_LIST := $(shell go list ${PKG}/... | grep -v /vendor/ | grep -v redis)
|
||||
GO_FILES := $(shell find . -name '*.go' | grep -v /vendor/ | grep -v _test.go)
|
||||
|
||||
GIT_TAG := $(shell git describe --tags --exact-match 2>/dev/null || echo "UnTag")
|
||||
BUILD_BRANCH := $(shell git rev-parse --abbrev-ref HEAD)
|
||||
BUILD_COMMIT := ${shell git rev-parse HEAD}
|
||||
BUILD_TIME := ${shell date '+%Y-%m-%d %H:%M:%S'}
|
||||
BUILD_GO_VERSION := $(shell go version | grep -o 'go[0-9].[0-9].*')
|
||||
VERSION_PATH := "github.com/infraboard/mcube/v2/ioc/config/application"
|
||||
OUTPUT_NAME := "devcloud-api"
|
||||
|
||||
.PHONY: all dep lint vet test test-coverage build clean
|
||||
|
||||
all: build
|
||||
|
||||
dep: ## Get the dependencies
|
||||
@go mod tidy
|
||||
|
||||
lint: ## Lint Golang files
|
||||
@golint -set_exit_status ${PKG_LIST}
|
||||
|
||||
vet: ## Run go vet
|
||||
@go vet ${PKG_LIST}
|
||||
|
||||
run: ## Run Devcloud
|
||||
@go run main.go start
|
||||
|
||||
build: dep ## Build the binary file
|
||||
@go build -a -o dist/${OUTPUT_NAME} -ldflags "-s -w" -ldflags "-X '${VERSION_PATH}.GIT_BRANCH=${BUILD_BRANCH}' -X '${VERSION_PATH}.GIT_COMMIT=${BUILD_COMMIT}' -X '${VERSION_PATH}.BUILD_TIME=${BUILD_TIME}' -X '${VERSION_PATH}.GO_VERSION=${BUILD_GO_VERSION}' -X '${VERSION_PATH}.GIT_TAG=${GIT_TAG}'" ${MAIN_FILE}
|
||||
|
||||
linux: dep ## Build the linux binary file
|
||||
@GOOS=linux GOARCH=amd64 go build -a -o dist/${OUTPUT_NAME} -ldflags "-s -w" -ldflags "-X '${VERSION_PATH}.GIT_BRANCH=${BUILD_BRANCH}' -X '${VERSION_PATH}.GIT_COMMIT=${BUILD_COMMIT}' -X '${VERSION_PATH}.BUILD_TIME=${BUILD_TIME}' -X '${VERSION_PATH}.GO_VERSION=${BUILD_GO_VERSION}' -X '${VERSION_PATH}.GIT_TAG=${GIT_TAG}'" ${MAIN_FILE}
|
||||
|
||||
image: dep ## Build the docker image
|
||||
docker build -t ${IMAGE_VERSION} -f Dockerfile .
|
||||
|
||||
test: ## Run unittests
|
||||
@go test -short ${PKG_LIST}
|
||||
|
||||
test-coverage: ## Run tests with coverage
|
||||
@go test -short -coverprofile cover.out -covermode=atomic ${PKG_LIST}
|
||||
@cat cover.out >> coverage.txt
|
||||
|
||||
help: ## Display this help screen
|
||||
@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
@ -1,13 +1,3 @@
|
||||
# 研发云
|
||||
|
||||
devcloud: 研发云, 给产研团队(技术团队), 产品经理, 项目经理, 研发人员/测试人员, 运维(上线,维护) 使用的: DevOps
|
||||
+ 审计中心: 平台的所有用户操作,记录下来, 变更审计
|
||||
+ 用户中心: 管理用户认证和鉴权
|
||||
+ 需求管理: Jira, 禅道, ...(x)
|
||||
+ 应用管理: 立项后的 SCM 源代码管理, 应用的元数据, 服务树(服务分组)
|
||||
+ 资源管理: CMDB
|
||||
+ 应用构建: CI, 流水线发布, 应用的持续构建, Jenkins, 新一代的流程, 基于K8s Job自己设计
|
||||
+ 发布中心(Dev/Test/Pre/Pro)CD: 发布, 应用维护, 部署集群的维护
|
||||
# 项目代码
|
||||
|
||||
多业务模块组成, 渐进式微服务开发方式
|
||||
|
||||
|
@ -1,20 +0,0 @@
|
||||
[app]
|
||||
name = "devcloud"
|
||||
description = "app desc"
|
||||
address = "http://127.0.0.1:8080"
|
||||
encrypt_key = "defualt app encrypt key"
|
||||
|
||||
[datasource]
|
||||
provider = "mysql"
|
||||
host = "127.0.0.1"
|
||||
port = 3306
|
||||
database = "devcloud_go18"
|
||||
username = "root"
|
||||
password = "123456"
|
||||
auto_migrate = true
|
||||
debug = true
|
||||
|
||||
[http]
|
||||
host = "127.0.0.1"
|
||||
port = 8080
|
||||
path_prefix = "api"
|
@ -1,18 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/infraboard/mcube/v2/ioc/server/cmd"
|
||||
|
||||
// mcenter 业务对象
|
||||
_ "122.51.31.227/go-course/go18/devcloud/mcenter/apps"
|
||||
|
||||
// 非功能性模块
|
||||
_ "github.com/infraboard/mcube/v2/ioc/apps/apidoc/restful"
|
||||
_ "github.com/infraboard/mcube/v2/ioc/apps/health/restful"
|
||||
_ "github.com/infraboard/mcube/v2/ioc/apps/metric/restful"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 启动
|
||||
cmd.Start()
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
# 用户中心
|
||||
|
||||
管理用户认证和鉴权
|
||||
|
||||
## 需求
|
||||
|
||||
认证: 你是谁
|
||||
+ Basic Auth: 通过用户名密码来认证
|
||||
+ 访问令牌: 最灵活的 框架
|
||||
|
||||
鉴权: 你能干什么(范围)
|
||||
|
||||
|
||||
## 概要设计
|
||||
|
||||
针对问题(需求),给出一种解决方案(解题)
|
||||
|
||||

|
||||
|
||||
## 详细设计
|
||||
|
||||
定义业务
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,17 +0,0 @@
|
||||
# 接口管理
|
||||
|
||||
如何提取 当前这个服务的路由条目, GoRestful框架的Container这一层 获取
|
||||
|
||||
```go
|
||||
func NewEntryFromRestfulContainer(c *restful.Container) (entries []*RouteEntry) {
|
||||
wss := c.RegisteredWebServices()
|
||||
for i := range wss {
|
||||
for _, route := range wss[i].Routes() {
|
||||
es := NewEntryFromRestRoute(route)
|
||||
entries = append(entries, es)
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}
|
||||
```
|
||||
|
@ -1,21 +0,0 @@
|
||||
package endpoint
|
||||
|
||||
type ACCESS_MODE uint8
|
||||
|
||||
const (
|
||||
// 只读模式
|
||||
ACCESS_MODE_READ = iota
|
||||
// 读写模式
|
||||
ACCESS_MODE_READ_WRITE
|
||||
)
|
||||
|
||||
const (
|
||||
META_REQUIRED_AUTH_KEY = "required_auth"
|
||||
META_REQUIRED_CODE_KEY = "required_code"
|
||||
META_REQUIRED_PERM_KEY = "required_perm"
|
||||
META_REQUIRED_ROLE_KEY = "required_role"
|
||||
META_REQUIRED_AUDIT_KEY = "required_audit"
|
||||
META_REQUIRED_NAMESPACE_KEY = "required_namespace"
|
||||
META_RESOURCE_KEY = "resource"
|
||||
META_ACTION_KEY = "action"
|
||||
)
|
@ -1,92 +0,0 @@
|
||||
package impl
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/endpoint"
|
||||
"github.com/infraboard/mcube/v2/exception"
|
||||
"github.com/infraboard/mcube/v2/ioc/config/datasource"
|
||||
"github.com/infraboard/mcube/v2/types"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 注册API接口
|
||||
// 这是一个批量接口, 一次添加多条记录
|
||||
// 需要保证事务: 同时成功,或者同时失败, MySQL事务
|
||||
func (i *EndpointServiceImpl) RegistryEndpoint(ctx context.Context, in *endpoint.RegistryEndpointRequest) (*types.Set[*endpoint.Endpoint], error) {
|
||||
if err := in.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
set := types.New[*endpoint.Endpoint]()
|
||||
err := datasource.DBFromCtx(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
for i := range in.Items {
|
||||
item := in.Items[i].BuildUUID()
|
||||
ins := endpoint.NewEndpoint().SetRouteEntry(*item)
|
||||
|
||||
oldEnpoint := endpoint.NewEndpoint()
|
||||
if err := tx.Where("uuid = ?", item.UUID).Take(oldEnpoint).Error; err != nil {
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
// 需要创建
|
||||
if err := tx.Save(ins).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// 需要更新
|
||||
ins.Id = oldEnpoint.Id
|
||||
if err := tx.Where("uuid = ?", item.UUID).Updates(ins).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
set.Add(ins)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return set, nil
|
||||
}
|
||||
|
||||
// 查询API接口列表
|
||||
func (i *EndpointServiceImpl) QueryEndpoint(ctx context.Context, in *endpoint.QueryEndpointRequest) (*types.Set[*endpoint.Endpoint], error) {
|
||||
set := types.New[*endpoint.Endpoint]()
|
||||
|
||||
query := datasource.DBFromCtx(ctx).Model(&endpoint.Endpoint{})
|
||||
if len(in.Services) > 0 && !in.IsMatchAllService() {
|
||||
query = query.Where("service IN ?", in.Services)
|
||||
}
|
||||
|
||||
err := query.Count(&set.Total).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = query.
|
||||
Order("created_at desc").
|
||||
Find(&set.Items).
|
||||
Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return set, nil
|
||||
}
|
||||
|
||||
// 查询API接口详情
|
||||
func (i *EndpointServiceImpl) DescribeEndpoint(ctx context.Context, in *endpoint.DescribeEndpointRequest) (*endpoint.Endpoint, error) {
|
||||
query := datasource.DBFromCtx(ctx)
|
||||
|
||||
ins := &endpoint.Endpoint{}
|
||||
if err := query.First(ins).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, exception.NewNotFound("endpoint %d not found", in.Id)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ins, nil
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package impl
|
||||
|
||||
import (
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/endpoint"
|
||||
"github.com/infraboard/mcube/v2/ioc"
|
||||
"github.com/infraboard/mcube/v2/ioc/config/datasource"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ioc.Controller().Registry(&EndpointServiceImpl{})
|
||||
}
|
||||
|
||||
var _ endpoint.Service = (*EndpointServiceImpl)(nil)
|
||||
|
||||
// 他是user service 服务的控制器
|
||||
type EndpointServiceImpl struct {
|
||||
ioc.ObjectImpl
|
||||
}
|
||||
|
||||
func (i *EndpointServiceImpl) Init() error {
|
||||
// 自动创建表
|
||||
if datasource.Get().AutoMigrate {
|
||||
err := datasource.DB().AutoMigrate(&endpoint.Endpoint{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 定义托管到Ioc里面的名称
|
||||
func (i *EndpointServiceImpl) Name() string {
|
||||
return endpoint.APP_NAME
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"slices"
|
||||
|
||||
"github.com/infraboard/mcube/v2/ioc"
|
||||
"github.com/infraboard/mcube/v2/ioc/config/validator"
|
||||
"github.com/infraboard/mcube/v2/types"
|
||||
"github.com/infraboard/modules/iam/apps"
|
||||
)
|
||||
|
||||
const (
|
||||
APP_NAME = "endpoint"
|
||||
)
|
||||
|
||||
func GetService() Service {
|
||||
return ioc.Controller().Get(APP_NAME).(Service)
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
// 注册API接口
|
||||
RegistryEndpoint(context.Context, *RegistryEndpointRequest) (*types.Set[*Endpoint], error)
|
||||
// 查询API接口列表
|
||||
QueryEndpoint(context.Context, *QueryEndpointRequest) (*types.Set[*Endpoint], error)
|
||||
// 查询API接口详情
|
||||
DescribeEndpoint(context.Context, *DescribeEndpointRequest) (*Endpoint, error)
|
||||
}
|
||||
|
||||
func NewQueryEndpointRequest() *QueryEndpointRequest {
|
||||
return &QueryEndpointRequest{}
|
||||
}
|
||||
|
||||
type QueryEndpointRequest struct {
|
||||
Services []string `form:"services" json:"serivces"`
|
||||
}
|
||||
|
||||
func (r *QueryEndpointRequest) WithService(services ...string) *QueryEndpointRequest {
|
||||
for _, service := range services {
|
||||
if !slices.Contains(r.Services, service) {
|
||||
r.Services = append(r.Services, services...)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *QueryEndpointRequest) IsMatchAllService() bool {
|
||||
return slices.Contains(r.Services, "*")
|
||||
}
|
||||
|
||||
func NewDescribeEndpointRequest() *DescribeEndpointRequest {
|
||||
return &DescribeEndpointRequest{}
|
||||
}
|
||||
|
||||
type DescribeEndpointRequest struct {
|
||||
apps.GetRequest
|
||||
}
|
||||
|
||||
func NewRegistryEndpointRequest() *RegistryEndpointRequest {
|
||||
return &RegistryEndpointRequest{
|
||||
Items: []*RouteEntry{},
|
||||
}
|
||||
}
|
||||
|
||||
type RegistryEndpointRequest struct {
|
||||
Items []*RouteEntry `json:"items"`
|
||||
}
|
||||
|
||||
func (r *RegistryEndpointRequest) AddItem(items ...*RouteEntry) *RegistryEndpointRequest {
|
||||
r.Items = append(r.Items, items...)
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *RegistryEndpointRequest) Validate() error {
|
||||
return validator.Validate(r)
|
||||
}
|
@ -1,251 +0,0 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/emicklei/go-restful/v3"
|
||||
"github.com/google/uuid"
|
||||
"github.com/infraboard/mcube/v2/ioc/config/application"
|
||||
"github.com/infraboard/mcube/v2/tools/pretty"
|
||||
"github.com/infraboard/mcube/v2/types"
|
||||
"github.com/infraboard/modules/iam/apps"
|
||||
)
|
||||
|
||||
func NewEndpoint() *Endpoint {
|
||||
return &Endpoint{
|
||||
ResourceMeta: *apps.NewResourceMeta(),
|
||||
}
|
||||
}
|
||||
|
||||
func IsEndpointExist(set *types.Set[*Endpoint], target *Endpoint) bool {
|
||||
for _, item := range set.Items {
|
||||
if item.Id == target.Id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Endpoint Service's features
|
||||
type Endpoint struct {
|
||||
// 基础数据
|
||||
apps.ResourceMeta
|
||||
// 路由条目信息
|
||||
RouteEntry `bson:",inline" validate:"required"`
|
||||
}
|
||||
|
||||
func (e *Endpoint) TableName() string {
|
||||
return "endpoints"
|
||||
}
|
||||
|
||||
func (e *Endpoint) String() string {
|
||||
return pretty.ToJSON(e)
|
||||
}
|
||||
|
||||
func (e *Endpoint) IsMatched(service, method, path string) bool {
|
||||
if e.Service != service {
|
||||
return false
|
||||
}
|
||||
if e.Method != method {
|
||||
return false
|
||||
}
|
||||
if e.Path != path {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (u *Endpoint) SetRouteEntry(v RouteEntry) *Endpoint {
|
||||
u.RouteEntry = v
|
||||
return u
|
||||
}
|
||||
|
||||
func NewRouteEntry() *RouteEntry {
|
||||
return &RouteEntry{
|
||||
RequiredRole: []string{},
|
||||
Extras: map[string]string{},
|
||||
}
|
||||
}
|
||||
|
||||
// Entry 路由条目, service-method-path
|
||||
type RouteEntry struct {
|
||||
// 该功能属于那个服务
|
||||
UUID string `json:"uuid" bson:"uuid" gorm:"column:uuid;type:varchar(100);uniqueIndex" optional:"true" description:"路由UUID"`
|
||||
// 该功能属于那个服务
|
||||
Service string `json:"service" bson:"service" validate:"required,lte=64" gorm:"column:service;type:varchar(100);index" description:"服务名称"`
|
||||
// 服务那个版本的功能
|
||||
Version string `json:"version" bson:"version" validate:"required,lte=64" gorm:"column:version;type:varchar(100)" optional:"true" description:"版本版本"`
|
||||
// 资源名称
|
||||
Resource string `json:"resource" bson:"resource" gorm:"column:resource;type:varchar(100);index" description:"资源名称"`
|
||||
// 资源操作
|
||||
Action string `json:"action" bson:"action" gorm:"column:action;type:varchar(100);index" description:"资源操作"`
|
||||
// 读或者写
|
||||
AccessMode ACCESS_MODE `json:"access_mode" bson:"access_mode" gorm:"column:access_mode;type:tinyint(1);index" optional:"true" description:"读写权限"`
|
||||
// 操作标签
|
||||
ActionLabel string `json:"action_label" gorm:"column:action_label;type:varchar(200);index" optional:"true" description:"资源标签"`
|
||||
// 函数名称
|
||||
FunctionName string `json:"function_name" bson:"function_name" gorm:"column:function_name;type:varchar(100)" optional:"true" description:"函数名称"`
|
||||
// HTTP path 用于自动生成http api
|
||||
Path string `json:"path" bson:"path" gorm:"column:path;type:varchar(200);index" description:"接口的路径"`
|
||||
// HTTP method 用于自动生成http api
|
||||
Method string `json:"method" bson:"method" gorm:"column:method;type:varchar(100);index" description:"接口的方法"`
|
||||
// 接口说明
|
||||
Description string `json:"description" bson:"description" gorm:"column:description;type:text" optional:"true" description:"接口说明"`
|
||||
// 是否校验用户身份 (acccess_token 校验)
|
||||
RequiredAuth bool `json:"required_auth" bson:"required_auth" gorm:"column:required_auth;type:tinyint(1)" optional:"true" description:"是否校验用户身份 (acccess_token 校验)"`
|
||||
// 验证码校验(开启双因子认证需要) (code 校验)
|
||||
RequiredCode bool `json:"required_code" bson:"required_code" gorm:"column:required_code;type:tinyint(1)" optional:"true" description:"验证码校验(开启双因子认证需要) (code 校验)"`
|
||||
// 开启鉴权
|
||||
RequiredPerm bool `json:"required_perm" bson:"required_perm" gorm:"column:required_perm;type:tinyint(1)" optional:"true" description:"开启鉴权"`
|
||||
// ACL模式下, 允许的通过的身份标识符, 比如角色, 用户类型之类
|
||||
RequiredRole []string `json:"required_role" bson:"required_role" gorm:"column:required_role;serializer:json;type:json" optional:"true" description:"ACL模式下, 允许的通过的身份标识符, 比如角色, 用户类型之类"`
|
||||
// 是否开启操作审计, 开启后这次操作将被记录
|
||||
RequiredAudit bool `json:"required_audit" bson:"required_audit" gorm:"column:required_audit;type:tinyint(1)" optional:"true" description:"是否开启操作审计, 开启后这次操作将被记录"`
|
||||
// 名称空间不能为空
|
||||
RequiredNamespace bool `json:"required_namespace" bson:"required_namespace" gorm:"column:required_namespace;type:tinyint(1)" optional:"true" description:"名称空间不能为空"`
|
||||
// 扩展信息
|
||||
Extras map[string]string `json:"extras" bson:"extras" gorm:"column:extras;serializer:json;type:json" optional:"true" description:"扩展信息"`
|
||||
}
|
||||
|
||||
// service-method-path
|
||||
func (e *RouteEntry) BuildUUID() *RouteEntry {
|
||||
e.UUID = uuid.NewSHA1(uuid.Nil, fmt.Appendf(nil, "%s-%s-%s", e.Service, e.Method, e.Path)).String()
|
||||
return e
|
||||
}
|
||||
|
||||
func GetRouteMeta[T any](m map[string]any, key string) T {
|
||||
if v, ok := m[key]; ok {
|
||||
return v.(T)
|
||||
}
|
||||
|
||||
var t T
|
||||
return t
|
||||
}
|
||||
|
||||
// func GetRouteMetaString(m map[string]any, key string) string {
|
||||
// if v, ok := m[key]; ok {
|
||||
// return v.(string)
|
||||
// }
|
||||
|
||||
// var t string
|
||||
// return t
|
||||
// }
|
||||
|
||||
func (e *RouteEntry) LoadMeta(meta map[string]any) {
|
||||
e.Service = application.Get().AppName
|
||||
e.Resource = GetRouteMeta[string](meta, META_RESOURCE_KEY)
|
||||
e.Action = GetRouteMeta[string](meta, META_ACTION_KEY)
|
||||
e.RequiredAuth = GetRouteMeta[bool](meta, META_REQUIRED_AUTH_KEY)
|
||||
e.RequiredCode = GetRouteMeta[bool](meta, META_REQUIRED_CODE_KEY)
|
||||
e.RequiredPerm = GetRouteMeta[bool](meta, META_REQUIRED_PERM_KEY)
|
||||
e.RequiredRole = GetRouteMeta[[]string](meta, META_REQUIRED_ROLE_KEY)
|
||||
e.RequiredAudit = GetRouteMeta[bool](meta, META_REQUIRED_AUDIT_KEY)
|
||||
e.RequiredNamespace = GetRouteMeta[bool](meta, META_REQUIRED_NAMESPACE_KEY)
|
||||
}
|
||||
|
||||
// UniquePath todo
|
||||
func (e *RouteEntry) HasRequiredRole() bool {
|
||||
return len(e.RequiredRole) > 0
|
||||
}
|
||||
|
||||
// UniquePath todo
|
||||
func (e *RouteEntry) UniquePath() string {
|
||||
return fmt.Sprintf("%s.%s", e.Method, e.Path)
|
||||
}
|
||||
|
||||
func (e *RouteEntry) IsRequireRole(target string) bool {
|
||||
for i := range e.RequiredRole {
|
||||
if e.RequiredRole[i] == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
if e.RequiredRole[i] == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (e *RouteEntry) SetRequiredAuth(v bool) *RouteEntry {
|
||||
e.RequiredAuth = v
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *RouteEntry) AddRequiredRole(roles ...string) *RouteEntry {
|
||||
e.RequiredRole = append(e.RequiredRole, roles...)
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *RouteEntry) SetRequiredPerm(v bool) *RouteEntry {
|
||||
e.RequiredPerm = v
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *RouteEntry) SetLabel(value string) *RouteEntry {
|
||||
e.ActionLabel = value
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *RouteEntry) SetExtensionFromMap(m map[string]string) *RouteEntry {
|
||||
if e.Extras == nil {
|
||||
e.Extras = map[string]string{}
|
||||
}
|
||||
|
||||
for k, v := range m {
|
||||
e.Extras[k] = v
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *RouteEntry) SetRequiredCode(v bool) *RouteEntry {
|
||||
e.RequiredCode = v
|
||||
return e
|
||||
}
|
||||
|
||||
func NewEntryFromRestRequest(req *restful.Request) *RouteEntry {
|
||||
entry := NewRouteEntry()
|
||||
|
||||
// 请求拦截
|
||||
route := req.SelectedRoute()
|
||||
if route == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
entry.FunctionName = route.Operation()
|
||||
entry.Method = route.Method()
|
||||
entry.LoadMeta(route.Metadata())
|
||||
entry.Path = route.Path()
|
||||
return entry
|
||||
}
|
||||
|
||||
func NewEntryFromRestRouteReader(route restful.RouteReader) *RouteEntry {
|
||||
entry := NewRouteEntry()
|
||||
entry.FunctionName = route.Operation()
|
||||
entry.Method = route.Method()
|
||||
entry.LoadMeta(route.Metadata())
|
||||
entry.Path = route.Path()
|
||||
return entry
|
||||
}
|
||||
|
||||
func NewEntryFromRestRoute(route restful.Route) *RouteEntry {
|
||||
entry := NewRouteEntry()
|
||||
entry.FunctionName = route.Operation
|
||||
entry.Method = route.Method
|
||||
entry.LoadMeta(route.Metadata)
|
||||
entry.Path = route.Path
|
||||
return entry
|
||||
}
|
||||
|
||||
func NewEntryFromRestfulContainer(c *restful.Container) (entries []*RouteEntry) {
|
||||
// 获取当前Container里面所有的 WebService
|
||||
wss := c.RegisteredWebServices()
|
||||
for i := range wss {
|
||||
// 获取WebService下的路由条目
|
||||
for _, route := range wss[i].Routes() {
|
||||
es := NewEntryFromRestRoute(route)
|
||||
entries = append(entries, es)
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}
|
@ -1 +0,0 @@
|
||||
# 空间管理
|
@ -1,5 +0,0 @@
|
||||
package namespace
|
||||
|
||||
const (
|
||||
DEFAULT_NS_NAME = "default"
|
||||
)
|
@ -1,31 +0,0 @@
|
||||
package impl
|
||||
|
||||
import (
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/namespace"
|
||||
"github.com/infraboard/mcube/v2/ioc"
|
||||
"github.com/infraboard/mcube/v2/ioc/config/datasource"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ioc.Controller().Registry(&NameSpaceServiceImpl{})
|
||||
}
|
||||
|
||||
var _ namespace.Service = (*NameSpaceServiceImpl)(nil)
|
||||
|
||||
type NameSpaceServiceImpl struct {
|
||||
ioc.ObjectImpl
|
||||
}
|
||||
|
||||
func (i *NameSpaceServiceImpl) Init() error {
|
||||
if datasource.Get().AutoMigrate {
|
||||
err := datasource.DB().AutoMigrate(&namespace.Namespace{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *NameSpaceServiceImpl) Name() string {
|
||||
return namespace.AppName
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package impl_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/namespace"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/test"
|
||||
)
|
||||
|
||||
var (
|
||||
impl namespace.Service
|
||||
ctx = context.Background()
|
||||
)
|
||||
|
||||
func init() {
|
||||
test.DevelopmentSetUp()
|
||||
impl = namespace.GetService()
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
package impl
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/namespace"
|
||||
"github.com/infraboard/mcube/v2/exception"
|
||||
"github.com/infraboard/mcube/v2/ioc/config/datasource"
|
||||
"github.com/infraboard/mcube/v2/types"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 创建空间
|
||||
func (i *NameSpaceServiceImpl) CreateNamespace(ctx context.Context, in *namespace.CreateNamespaceRequest) (*namespace.Namespace, error) {
|
||||
if err := in.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ins := namespace.NewNamespace()
|
||||
ins.CreateNamespaceRequest = *in
|
||||
|
||||
if err := datasource.DBFromCtx(ctx).
|
||||
Create(ins).
|
||||
Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ins, nil
|
||||
}
|
||||
|
||||
// 查询空间
|
||||
func (i *NameSpaceServiceImpl) QueryNamespace(ctx context.Context, in *namespace.QueryNamespaceRequest) (*types.Set[*namespace.Namespace], error) {
|
||||
set := types.New[*namespace.Namespace]()
|
||||
|
||||
query := datasource.DBFromCtx(ctx).Model(&namespace.Namespace{})
|
||||
err := query.Count(&set.Total).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = query.
|
||||
Order("created_at desc").
|
||||
Offset(int(in.ComputeOffset())).
|
||||
Limit(int(in.PageSize)).
|
||||
Find(&set.Items).
|
||||
Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return set, nil
|
||||
}
|
||||
|
||||
// 查询空间详情
|
||||
func (i *NameSpaceServiceImpl) DescribeNamespace(ctx context.Context, in *namespace.DescribeNamespaceRequest) (*namespace.Namespace, error) {
|
||||
query := datasource.DBFromCtx(ctx)
|
||||
|
||||
ins := &namespace.Namespace{}
|
||||
if err := query.Where("id = ?", in.Id).First(ins).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, exception.NewNotFound("namespace %d not found", in.Id)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ins, nil
|
||||
}
|
||||
|
||||
// 更新空间
|
||||
func (i *NameSpaceServiceImpl) UpdateNamespace(ctx context.Context, in *namespace.UpdateNamespaceRequest) (*namespace.Namespace, error) {
|
||||
descReq := namespace.NewDescribeNamespaceRequest()
|
||||
descReq.SetId(in.Id)
|
||||
ins, err := i.DescribeNamespace(ctx, descReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ins.CreateNamespaceRequest = in.CreateNamespaceRequest
|
||||
return ins, datasource.DBFromCtx(ctx).Where("id = ?", in.Id).Updates(ins).Error
|
||||
}
|
||||
|
||||
// 删除空间
|
||||
func (i *NameSpaceServiceImpl) DeleteNamespace(ctx context.Context, in *namespace.DeleteNamespaceRequest) (*namespace.Namespace, error) {
|
||||
descReq := namespace.NewDescribeNamespaceRequest()
|
||||
descReq.SetId(in.Id)
|
||||
ins, err := i.DescribeNamespace(ctx, descReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ins, datasource.DBFromCtx(ctx).
|
||||
Where("id = ?", in.Id).
|
||||
Delete(&namespace.Namespace{}).
|
||||
Error
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package impl_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/namespace"
|
||||
)
|
||||
|
||||
func TestQueryNamespace(t *testing.T) {
|
||||
req := namespace.NewQueryNamespaceRequest()
|
||||
set, err := impl.QueryNamespace(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(set)
|
||||
}
|
||||
|
||||
func TestCreateNamespace(t *testing.T) {
|
||||
req := namespace.NewCreateNamespaceRequest()
|
||||
req.Name = namespace.DEFAULT_NS_NAME
|
||||
req.Description = "默认空间"
|
||||
req.OwnerUserId = 1
|
||||
set, err := impl.CreateNamespace(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(set)
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
package namespace
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/infraboard/mcube/v2/http/request"
|
||||
"github.com/infraboard/mcube/v2/ioc"
|
||||
"github.com/infraboard/mcube/v2/types"
|
||||
"github.com/infraboard/modules/iam/apps"
|
||||
)
|
||||
|
||||
const (
|
||||
AppName = "namespace"
|
||||
)
|
||||
|
||||
func GetService() Service {
|
||||
return ioc.Controller().Get(AppName).(Service)
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
// 创建空间
|
||||
CreateNamespace(context.Context, *CreateNamespaceRequest) (*Namespace, error)
|
||||
// 查询空间
|
||||
QueryNamespace(context.Context, *QueryNamespaceRequest) (*types.Set[*Namespace], error)
|
||||
// 查询空间详情
|
||||
DescribeNamespace(context.Context, *DescribeNamespaceRequest) (*Namespace, error)
|
||||
// 更新空间
|
||||
UpdateNamespace(context.Context, *UpdateNamespaceRequest) (*Namespace, error)
|
||||
// 删除空间
|
||||
DeleteNamespace(context.Context, *DeleteNamespaceRequest) (*Namespace, error)
|
||||
}
|
||||
|
||||
func NewQueryNamespaceRequest() *QueryNamespaceRequest {
|
||||
return &QueryNamespaceRequest{
|
||||
PageRequest: *request.NewDefaultPageRequest(),
|
||||
NamespaceIds: []uint64{},
|
||||
}
|
||||
}
|
||||
|
||||
type QueryNamespaceRequest struct {
|
||||
request.PageRequest
|
||||
NamespaceIds []uint64 `json:"namespace_ids"`
|
||||
}
|
||||
|
||||
func (r *QueryNamespaceRequest) AddNamespaceIds(ids ...uint64) {
|
||||
for _, id := range ids {
|
||||
if !r.HasNamespaceIds(id) {
|
||||
r.NamespaceIds = append(r.NamespaceIds, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *QueryNamespaceRequest) HasNamespaceIds(namespaceId uint64) bool {
|
||||
for i := range r.NamespaceIds {
|
||||
if r.NamespaceIds[i] == namespaceId {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func NewDescribeNamespaceRequest() *DescribeNamespaceRequest {
|
||||
return &DescribeNamespaceRequest{}
|
||||
}
|
||||
|
||||
type DescribeNamespaceRequest struct {
|
||||
apps.GetRequest
|
||||
}
|
||||
|
||||
func (r *DescribeNamespaceRequest) SetNamespaceId(id uint64) *DescribeNamespaceRequest {
|
||||
r.Id = id
|
||||
return r
|
||||
}
|
||||
|
||||
func NewUpdateNamespaceRequest() *UpdateNamespaceRequest {
|
||||
return &UpdateNamespaceRequest{}
|
||||
}
|
||||
|
||||
type UpdateNamespaceRequest struct {
|
||||
apps.GetRequest
|
||||
CreateNamespaceRequest
|
||||
}
|
||||
|
||||
func NewDeleteNamespaceRequest() *DeleteNamespaceRequest {
|
||||
return &DeleteNamespaceRequest{}
|
||||
}
|
||||
|
||||
type DeleteNamespaceRequest struct {
|
||||
apps.GetRequest
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
package namespace
|
||||
|
||||
import (
|
||||
"github.com/infraboard/mcube/v2/ioc/config/validator"
|
||||
"github.com/infraboard/mcube/v2/tools/pretty"
|
||||
"github.com/infraboard/modules/iam/apps"
|
||||
)
|
||||
|
||||
func NewNamespace() *Namespace {
|
||||
return &Namespace{
|
||||
ResourceMeta: *apps.NewResourceMeta(),
|
||||
}
|
||||
}
|
||||
|
||||
type Namespace struct {
|
||||
// 基础数据
|
||||
apps.ResourceMeta
|
||||
// 空间属性
|
||||
CreateNamespaceRequest
|
||||
}
|
||||
|
||||
func (n *Namespace) IsOwner(ownerUserId uint64) bool {
|
||||
return n.OwnerUserId == ownerUserId
|
||||
}
|
||||
|
||||
func (n *Namespace) TableName() string {
|
||||
return "namespaces"
|
||||
}
|
||||
|
||||
func (n *Namespace) String() string {
|
||||
return pretty.ToJSON(n)
|
||||
}
|
||||
|
||||
func NewCreateNamespaceRequest() *CreateNamespaceRequest {
|
||||
return &CreateNamespaceRequest{
|
||||
Extras: map[string]string{},
|
||||
Enabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
type CreateNamespaceRequest struct {
|
||||
// 父Namespace Id
|
||||
ParentId uint64 `json:"parent_id" bson:"parent_id" gorm:"column:parent_id;type:uint;index" description:"父Namespace Id"`
|
||||
// 全局唯一
|
||||
Name string `json:"name" bson:"name" validate:"required" gorm:"column:name;type:varchar(200);not null;uniqueIndex" description:"空间名称" unique:"true"`
|
||||
// 空间负责人
|
||||
OwnerUserId uint64 `json:"owner_user_id" bson:"owner_user_id" gorm:"column:owner_user_id;type:uint;index;not null" description:" 空间负责人Id"`
|
||||
// 禁用项目, 该项目所有人暂时都无法访问
|
||||
Enabled bool `json:"enabled" bson:"enabled" gorm:"column:enabled;type:tinyint(1)" description:"是否启用"`
|
||||
// 空间描述图片
|
||||
Icon string `json:"icon" bson:"icon" gorm:"column:icon;type:varchar(200)" description:"空间图标"`
|
||||
// 空间描述
|
||||
Description string `json:"description" bson:"description" gorm:"column:description;type:text" description:"空间描述"`
|
||||
// 标签
|
||||
Label string `json:"label" gorm:"column:label;type:varchar(200);index" description:"标签"`
|
||||
// 扩展信息
|
||||
Extras map[string]string `json:"extras" bson:"extras" gorm:"column:extras;serializer:json;type:json" description:"扩展信息"`
|
||||
}
|
||||
|
||||
func (r *CreateNamespaceRequest) Validate() error {
|
||||
return validator.Validate(r)
|
||||
}
|
@ -1 +0,0 @@
|
||||
# 授权策略
|
@ -1,38 +0,0 @@
|
||||
package impl
|
||||
|
||||
import (
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/namespace"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/policy"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/role"
|
||||
"github.com/infraboard/mcube/v2/ioc"
|
||||
"github.com/infraboard/mcube/v2/ioc/config/datasource"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ioc.Controller().Registry(&PolicyServiceImpl{})
|
||||
}
|
||||
|
||||
var _ policy.Service = (*PolicyServiceImpl)(nil)
|
||||
|
||||
type PolicyServiceImpl struct {
|
||||
ioc.ObjectImpl
|
||||
|
||||
namespace namespace.Service
|
||||
role role.Service
|
||||
}
|
||||
|
||||
func (i *PolicyServiceImpl) Init() error {
|
||||
if datasource.Get().AutoMigrate {
|
||||
err := datasource.DB().AutoMigrate(&policy.Policy{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
i.namespace = namespace.GetService()
|
||||
i.role = role.GetService()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *PolicyServiceImpl) Name() string {
|
||||
return policy.AppName
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package impl_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/policy"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/test"
|
||||
)
|
||||
|
||||
var (
|
||||
impl policy.Service
|
||||
ctx = context.Background()
|
||||
)
|
||||
|
||||
func init() {
|
||||
test.DevelopmentSetUp()
|
||||
impl = policy.GetService()
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
package impl
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/endpoint"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/namespace"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/policy"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/role"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/view"
|
||||
"github.com/infraboard/mcube/v2/types"
|
||||
)
|
||||
|
||||
// 查询用户可以访问的空间
|
||||
func (i *PolicyServiceImpl) QueryNamespace(ctx context.Context, in *policy.QueryNamespaceRequest) (*types.Set[*namespace.Namespace], error) {
|
||||
nsReq := namespace.NewQueryNamespaceRequest()
|
||||
|
||||
policies, err := i.QueryPolicy(ctx,
|
||||
policy.NewQueryPolicyRequest().
|
||||
SetSkipPage(true).
|
||||
SetUserId(in.UserId).
|
||||
SetExpired(false).
|
||||
SetEnabled(true))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
policies.ForEach(func(t *policy.Policy) {
|
||||
if t.NamespaceId != nil {
|
||||
nsReq.AddNamespaceIds(*t.NamespaceId)
|
||||
}
|
||||
})
|
||||
|
||||
return i.namespace.QueryNamespace(ctx, nsReq)
|
||||
}
|
||||
|
||||
// 查询用户可以访问的Api接口
|
||||
// 找到用户可以访问的角色列表,然后在找出角色对应的Api访问权限
|
||||
func (i *PolicyServiceImpl) QueryEndpoint(ctx context.Context, in *policy.QueryEndpointRequest) (*types.Set[*endpoint.Endpoint], error) {
|
||||
set := types.New[*endpoint.Endpoint]()
|
||||
policies, err := i.QueryPolicy(ctx,
|
||||
policy.NewQueryPolicyRequest().
|
||||
SetSkipPage(true).
|
||||
SetNamespaceId(in.NamespaceId).
|
||||
SetUserId(in.UserId).
|
||||
SetExpired(false).
|
||||
SetEnabled(true))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roleReq := role.NewQueryMatchedEndpointRequest()
|
||||
policies.ForEach(func(t *policy.Policy) {
|
||||
roleReq.Add(t.RoleId)
|
||||
})
|
||||
|
||||
if policies.Len() > 0 {
|
||||
set, err = role.GetService().QueryMatchedEndpoint(ctx, roleReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return set, nil
|
||||
}
|
||||
|
||||
// 校验Api接口权限
|
||||
func (i *PolicyServiceImpl) ValidateEndpointPermission(ctx context.Context, in *policy.ValidateEndpointPermissionRequest) (*policy.ValidateEndpointPermissionResponse, error) {
|
||||
resp := policy.NewValidateEndpointPermissionResponse(*in)
|
||||
|
||||
// 空间Owner有所有权限
|
||||
ns, err := namespace.GetService().DescribeNamespace(ctx, namespace.NewDescribeNamespaceRequest().SetNamespaceId(in.NamespaceId))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ns.IsOwner(in.UserId) {
|
||||
resp.HasPermission = true
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// 非空间管理员需要独立鉴权, 查询用户可以访问的API列表
|
||||
endpointReq := policy.NewQueryEndpointRequest()
|
||||
endpointReq.UserId = in.UserId
|
||||
endpointReq.NamespaceId = in.NamespaceId
|
||||
endpointSet, err := i.QueryEndpoint(ctx, endpointReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range endpointSet.Items {
|
||||
if item.IsMatched(in.Service, in.Method, in.Path) {
|
||||
resp.HasPermission = true
|
||||
resp.Endpoint = item
|
||||
break
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// 查询用户可以访问的菜单
|
||||
func (i *PolicyServiceImpl) QueryMenu(ctx context.Context, in *policy.QueryMenuRequest) (*types.Set[*view.Menu], error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 校验Menu视图权限
|
||||
func (i *PolicyServiceImpl) ValidatePagePermission(ctx context.Context, in *policy.ValidatePagePermissionRequest) (*policy.ValidatePagePermissionResponse, error) {
|
||||
return nil, nil
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
package impl_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/policy"
|
||||
)
|
||||
|
||||
func TestQueryNamespace(t *testing.T) {
|
||||
req := policy.NewQueryNamespaceRequest()
|
||||
req.UserId = 1
|
||||
set, err := impl.QueryNamespace(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(set)
|
||||
}
|
||||
|
||||
func TestQueryEndpoint(t *testing.T) {
|
||||
req := policy.NewQueryEndpointRequest()
|
||||
req.UserId = 2
|
||||
req.NamespaceId = 1
|
||||
set, err := impl.QueryEndpoint(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(set)
|
||||
}
|
||||
|
||||
func TestValidateEndpointPermission(t *testing.T) {
|
||||
req := policy.NewValidateEndpointPermissionRequest()
|
||||
req.UserId = 1
|
||||
req.NamespaceId = 1
|
||||
req.Service = "devcloud"
|
||||
req.Method = "GET"
|
||||
req.Path = "/api/devcloud/v1/users/"
|
||||
set, err := impl.ValidateEndpointPermission(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(set)
|
||||
}
|
@ -1,158 +0,0 @@
|
||||
package impl
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/namespace"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/policy"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/role"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/user"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/view"
|
||||
"github.com/infraboard/mcube/v2/exception"
|
||||
"github.com/infraboard/mcube/v2/ioc/config/datasource"
|
||||
"github.com/infraboard/mcube/v2/types"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 创建策略
|
||||
func (i *PolicyServiceImpl) CreatePolicy(ctx context.Context, in *policy.CreatePolicyRequest) (*policy.Policy, error) {
|
||||
if err := in.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ins := policy.NewPolicy()
|
||||
ins.CreatePolicyRequest = *in
|
||||
|
||||
if err := datasource.DBFromCtx(ctx).
|
||||
Create(ins).
|
||||
Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ins, nil
|
||||
}
|
||||
|
||||
// 查询策略列表
|
||||
func (i *PolicyServiceImpl) QueryPolicy(ctx context.Context, in *policy.QueryPolicyRequest) (*types.Set[*policy.Policy], error) {
|
||||
set := types.New[*policy.Policy]()
|
||||
|
||||
query := datasource.DBFromCtx(ctx).Model(&policy.Policy{}).Order("created_at desc")
|
||||
if in.UserId != nil {
|
||||
query = query.Where("user_id = ?", in.UserId)
|
||||
}
|
||||
if in.NamespaceId != nil {
|
||||
query = query.Where("namespace_id = ?", in.NamespaceId)
|
||||
}
|
||||
if in.Enabled != nil {
|
||||
query = query.Where("enabled = ?", in.Enabled)
|
||||
}
|
||||
|
||||
err := query.Count(&set.Total).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !in.SkipPage {
|
||||
query = query.
|
||||
Offset(int(in.ComputeOffset())).
|
||||
Limit(int(in.PageSize))
|
||||
}
|
||||
|
||||
if err = query.Find(&set.Items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if in.WithUser {
|
||||
userReq := user.NewQueryUserRequest()
|
||||
set.ForEach(func(t *policy.Policy) {
|
||||
userReq.AddUser(t.UserId)
|
||||
})
|
||||
userSet, err := user.GetService().QueryUser(ctx, userReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
set.ForEach(func(p *policy.Policy) {
|
||||
p.User = userSet.Filter(func(t *user.User) bool {
|
||||
return p.UserId == t.Id
|
||||
}).First()
|
||||
})
|
||||
}
|
||||
if in.WithRole {
|
||||
roleReq := role.NewQueryRoleRequest()
|
||||
set.ForEach(func(t *policy.Policy) {
|
||||
roleReq.AddRoleId(t.RoleId)
|
||||
})
|
||||
roleSet, err := role.GetService().QueryRole(ctx, roleReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
set.ForEach(func(p *policy.Policy) {
|
||||
p.Role = roleSet.Filter(func(t *role.Role) bool {
|
||||
return p.RoleId == t.Id
|
||||
}).First()
|
||||
})
|
||||
}
|
||||
if in.WithNamespace {
|
||||
nsReq := namespace.NewQueryNamespaceRequest()
|
||||
set.ForEach(func(t *policy.Policy) {
|
||||
if t.NamespaceId != nil {
|
||||
nsReq.AddNamespaceIds(*t.NamespaceId)
|
||||
}
|
||||
})
|
||||
nsSet, err := namespace.GetService().QueryNamespace(ctx, nsReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
set.ForEach(func(p *policy.Policy) {
|
||||
if p.NamespaceId != nil {
|
||||
p.Namespace = nsSet.Filter(func(t *namespace.Namespace) bool {
|
||||
return *p.NamespaceId == t.Id
|
||||
}).First()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return set, nil
|
||||
}
|
||||
|
||||
// 查询详情
|
||||
func (i *PolicyServiceImpl) DescribePolicy(ctx context.Context, in *policy.DescribePolicyRequest) (*policy.Policy, error) {
|
||||
query := datasource.DBFromCtx(ctx)
|
||||
|
||||
ins := &policy.Policy{}
|
||||
if err := query.Where("id =?", in.Id).First(ins).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, exception.NewNotFound("policy %d not found", in.Id)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ins, nil
|
||||
}
|
||||
|
||||
// 更新策略
|
||||
func (i *PolicyServiceImpl) UpdatePolicy(ctx context.Context, in *policy.UpdatePolicyRequest) (*policy.Policy, error) {
|
||||
descReq := policy.NewDescribePolicyRequest()
|
||||
descReq.SetId(in.Id)
|
||||
ins, err := i.DescribePolicy(ctx, descReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ins.CreatePolicyRequest = in.CreatePolicyRequest
|
||||
return ins, datasource.DBFromCtx(ctx).Where("id = ?", in.Id).Updates(ins).Error
|
||||
}
|
||||
|
||||
// 删除策略
|
||||
func (i *PolicyServiceImpl) DeletePolicy(ctx context.Context, in *policy.DeletePolicyRequest) (*policy.Policy, error) {
|
||||
descReq := policy.NewDescribePolicyRequest()
|
||||
descReq.SetId(in.Id)
|
||||
ins, err := i.DescribePolicy(ctx, descReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ins, datasource.DBFromCtx(ctx).
|
||||
Where("id = ?", in.Id).
|
||||
Delete(&view.Menu{}).
|
||||
Error
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package impl_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/policy"
|
||||
)
|
||||
|
||||
func TestQueryPolicy(t *testing.T) {
|
||||
req := policy.NewQueryPolicyRequest()
|
||||
req.WithUser = true
|
||||
req.WithRole = true
|
||||
req.WithNamespace = true
|
||||
set, err := impl.QueryPolicy(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(set)
|
||||
}
|
||||
|
||||
func TestCreatePolicy(t *testing.T) {
|
||||
req := policy.NewCreatePolicyRequest()
|
||||
req.SetNamespaceId(1)
|
||||
req.UserId = 2
|
||||
req.RoleId = 1
|
||||
set, err := impl.CreatePolicy(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(set)
|
||||
}
|
@ -1,233 +0,0 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/endpoint"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/namespace"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/view"
|
||||
"github.com/infraboard/mcube/v2/http/request"
|
||||
"github.com/infraboard/mcube/v2/ioc"
|
||||
"github.com/infraboard/mcube/v2/tools/pretty"
|
||||
"github.com/infraboard/mcube/v2/types"
|
||||
"github.com/infraboard/modules/iam/apps"
|
||||
)
|
||||
|
||||
const (
|
||||
AppName = "policy"
|
||||
)
|
||||
|
||||
func GetService() Service {
|
||||
return ioc.Controller().Get(AppName).(Service)
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
// 策略管理
|
||||
PolicyService
|
||||
// 权限查询, 整合用户多个角色的权限合集
|
||||
PermissionService
|
||||
}
|
||||
|
||||
type PolicyService interface {
|
||||
// 创建策略
|
||||
CreatePolicy(context.Context, *CreatePolicyRequest) (*Policy, error)
|
||||
// 查询策略列表
|
||||
QueryPolicy(context.Context, *QueryPolicyRequest) (*types.Set[*Policy], error)
|
||||
// 查询详情
|
||||
DescribePolicy(context.Context, *DescribePolicyRequest) (*Policy, error)
|
||||
// 更新策略
|
||||
UpdatePolicy(context.Context, *UpdatePolicyRequest) (*Policy, error)
|
||||
// 删除策略
|
||||
DeletePolicy(context.Context, *DeletePolicyRequest) (*Policy, error)
|
||||
}
|
||||
|
||||
func NewQueryPolicyRequest() *QueryPolicyRequest {
|
||||
return &QueryPolicyRequest{
|
||||
PageRequest: request.NewDefaultPageRequest(),
|
||||
}
|
||||
}
|
||||
|
||||
type QueryPolicyRequest struct {
|
||||
*request.PageRequest
|
||||
// 忽略分页
|
||||
SkipPage bool `json:"skip_page"`
|
||||
// 关联用户Id
|
||||
UserId *uint64 `json:"user_id"`
|
||||
// 关联空间
|
||||
NamespaceId *uint64 `json:"namespace_id"`
|
||||
// 没有过期
|
||||
Expired *bool `json:"expired"`
|
||||
// 有没有启动
|
||||
Enabled *bool `json:"active"`
|
||||
// 关联查询出空间对象
|
||||
WithNamespace bool `json:"with_namespace"`
|
||||
// 关联查询出用户对象
|
||||
WithUser bool `json:"with_user"`
|
||||
// 关联查询角色对象
|
||||
WithRole bool `json:"with_role"`
|
||||
}
|
||||
|
||||
func (r *QueryPolicyRequest) SetNamespaceId(nsId uint64) *QueryPolicyRequest {
|
||||
r.NamespaceId = &nsId
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *QueryPolicyRequest) SetUserId(uid uint64) *QueryPolicyRequest {
|
||||
r.UserId = &uid
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *QueryPolicyRequest) SetExpired(v bool) *QueryPolicyRequest {
|
||||
r.Expired = &v
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *QueryPolicyRequest) SetEnabled(v bool) *QueryPolicyRequest {
|
||||
r.Enabled = &v
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *QueryPolicyRequest) SetSkipPage(v bool) *QueryPolicyRequest {
|
||||
r.SkipPage = v
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *QueryPolicyRequest) SetWithRole(v bool) *QueryPolicyRequest {
|
||||
r.WithRole = v
|
||||
return r
|
||||
}
|
||||
func (r *QueryPolicyRequest) SetWithUsers(v bool) *QueryPolicyRequest {
|
||||
r.WithUser = v
|
||||
return r
|
||||
}
|
||||
func (r *QueryPolicyRequest) SetWithUser(v bool) *QueryPolicyRequest {
|
||||
r.WithNamespace = v
|
||||
return r
|
||||
}
|
||||
|
||||
func NewDescribePolicyRequest() *DescribePolicyRequest {
|
||||
return &DescribePolicyRequest{}
|
||||
}
|
||||
|
||||
type DescribePolicyRequest struct {
|
||||
apps.GetRequest
|
||||
}
|
||||
|
||||
type UpdatePolicyRequest struct {
|
||||
apps.GetRequest
|
||||
CreatePolicyRequest
|
||||
}
|
||||
|
||||
func NewDeletePolicyRequest() *DeletePolicyRequest {
|
||||
return &DeletePolicyRequest{}
|
||||
}
|
||||
|
||||
type DeletePolicyRequest struct {
|
||||
apps.GetRequest
|
||||
}
|
||||
|
||||
type PermissionService interface {
|
||||
// 查询用户可以访问的空间
|
||||
QueryNamespace(context.Context, *QueryNamespaceRequest) (*types.Set[*namespace.Namespace], error)
|
||||
// 查询用户可以访问的菜单
|
||||
QueryMenu(context.Context, *QueryMenuRequest) (*types.Set[*view.Menu], error)
|
||||
// 查询用户可以访问的Api接口
|
||||
QueryEndpoint(context.Context, *QueryEndpointRequest) (*types.Set[*endpoint.Endpoint], error)
|
||||
// 校验页面权限
|
||||
ValidatePagePermission(context.Context, *ValidatePagePermissionRequest) (*ValidatePagePermissionResponse, error)
|
||||
// 校验接口权限
|
||||
ValidateEndpointPermission(context.Context, *ValidateEndpointPermissionRequest) (*ValidateEndpointPermissionResponse, error)
|
||||
}
|
||||
|
||||
type ValidatePagePermissionRequest struct {
|
||||
UserId uint64 `json:"user_id" form:"user_id"`
|
||||
NamespaceId uint64 `json:"namespace_id" form:"namespace_id"`
|
||||
Path string `json:"path" form:"path"`
|
||||
}
|
||||
|
||||
func NewValidatePagePermissionResponse(req ValidatePagePermissionRequest) *ValidatePagePermissionResponse {
|
||||
return &ValidatePagePermissionResponse{
|
||||
ValidatePagePermissionRequest: req,
|
||||
}
|
||||
}
|
||||
|
||||
type ValidatePagePermissionResponse struct {
|
||||
ValidatePagePermissionRequest
|
||||
HasPermission bool `json:"has_permission"`
|
||||
Page *view.Page `json:"page"`
|
||||
}
|
||||
|
||||
func NewValidateEndpointPermissionRequest() *ValidateEndpointPermissionRequest {
|
||||
return &ValidateEndpointPermissionRequest{}
|
||||
}
|
||||
|
||||
type ValidateEndpointPermissionRequest struct {
|
||||
UserId uint64 `json:"user_id" form:"user_id"`
|
||||
NamespaceId uint64 `json:"namespace_id" form:"namespace_id"`
|
||||
Service string `json:"service" form:"service"`
|
||||
Path string `json:"path" form:"path"`
|
||||
Method string `json:"method" form:"method"`
|
||||
}
|
||||
|
||||
func NewValidateEndpointPermissionResponse(req ValidateEndpointPermissionRequest) *ValidateEndpointPermissionResponse {
|
||||
return &ValidateEndpointPermissionResponse{
|
||||
ValidateEndpointPermissionRequest: req,
|
||||
}
|
||||
}
|
||||
|
||||
type ValidateEndpointPermissionResponse struct {
|
||||
ValidateEndpointPermissionRequest
|
||||
HasPermission bool `json:"has_permission"`
|
||||
Endpoint *endpoint.Endpoint `json:"endpoint"`
|
||||
}
|
||||
|
||||
func (r *ValidateEndpointPermissionResponse) String() string {
|
||||
return pretty.ToJSON(r)
|
||||
}
|
||||
|
||||
func NewQueryNamespaceRequest() *QueryNamespaceRequest {
|
||||
return &QueryNamespaceRequest{}
|
||||
}
|
||||
|
||||
type QueryNamespaceRequest struct {
|
||||
UserId uint64 `json:"user_id"`
|
||||
NamespaceId uint64 `json:"namespace_id"`
|
||||
}
|
||||
|
||||
func (r *QueryNamespaceRequest) SetUserId(v uint64) *QueryNamespaceRequest {
|
||||
r.UserId = v
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *QueryNamespaceRequest) SetNamespaceId(v uint64) *QueryNamespaceRequest {
|
||||
r.NamespaceId = v
|
||||
return r
|
||||
}
|
||||
|
||||
func NewQueryMenuRequest() *QueryMenuRequest {
|
||||
return &QueryMenuRequest{}
|
||||
}
|
||||
|
||||
type QueryMenuRequest struct {
|
||||
UserId uint64 `json:"user_id"`
|
||||
NamespaceId uint64 `json:"namespace_id"`
|
||||
}
|
||||
|
||||
func NewQueryEndpointRequest() *QueryEndpointRequest {
|
||||
return &QueryEndpointRequest{}
|
||||
}
|
||||
|
||||
type QueryEndpointRequest struct {
|
||||
UserId uint64 `json:"user_id"`
|
||||
NamespaceId uint64 `json:"namespace_id"`
|
||||
}
|
||||
|
||||
func (r *QueryEndpointRequest) SetUserId(v uint64) *QueryEndpointRequest {
|
||||
r.UserId = v
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *QueryEndpointRequest) SetNamespaceId(v uint64) *QueryEndpointRequest {
|
||||
r.NamespaceId = v
|
||||
return r
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/namespace"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/role"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/user"
|
||||
"github.com/infraboard/mcube/v2/ioc/config/validator"
|
||||
"github.com/infraboard/mcube/v2/tools/pretty"
|
||||
"github.com/infraboard/modules/iam/apps"
|
||||
)
|
||||
|
||||
func NewPolicy() *Policy {
|
||||
return &Policy{
|
||||
ResourceMeta: *apps.NewResourceMeta(),
|
||||
}
|
||||
}
|
||||
|
||||
type Policy struct {
|
||||
// 基础数据
|
||||
apps.ResourceMeta
|
||||
// 策略定义
|
||||
CreatePolicyRequest
|
||||
// 关联空间
|
||||
Namespace *namespace.Namespace `json:"namespace,omitempty" gorm:"-"`
|
||||
// 关联用户
|
||||
User *user.User `json:"user,omitempty" gorm:"-"`
|
||||
// 关联角色
|
||||
Role *role.Role `json:"role,omitempty" gorm:"-"`
|
||||
}
|
||||
|
||||
func (p *Policy) TableName() string {
|
||||
return "policy"
|
||||
}
|
||||
|
||||
func (p *Policy) String() string {
|
||||
return pretty.ToJSON(p)
|
||||
}
|
||||
|
||||
func NewCreatePolicyRequest() *CreatePolicyRequest {
|
||||
return &CreatePolicyRequest{
|
||||
Extras: map[string]string{},
|
||||
Scope: map[string]string{},
|
||||
Enabled: true,
|
||||
ReadOnly: false,
|
||||
}
|
||||
}
|
||||
|
||||
type CreatePolicyRequest struct {
|
||||
// 创建者
|
||||
CreateBy uint64 `json:"create_by" bson:"create_by" gorm:"column:create_by;type:uint" description:"创建者" optional:"true"`
|
||||
// 空间
|
||||
NamespaceId *uint64 `json:"namespace_id" bson:"namespace_id" gorm:"column:namespace_id;type:varchar(200);index" description:"策略生效的空间Id" optional:"true"`
|
||||
// 用户Id
|
||||
UserId uint64 `json:"user_id" bson:"user_id" gorm:"column:user_id;type:uint;not null;index" validate:"required" description:"被授权的用户"`
|
||||
// 角色Id
|
||||
RoleId uint64 `json:"role_id" bson:"role_id" gorm:"column:role_id;type:uint;not null;index" validate:"required" description:"被关联的角色"`
|
||||
// 访问范围, 需要提前定义scope, 比如环境
|
||||
Scope map[string]string `json:"scope" bson:"scope" gorm:"column:scope;serializer:json;type:json" description:"数据访问的范围" optional:"true"`
|
||||
// 策略过期时间
|
||||
ExpiredTime *time.Time `json:"expired_time" bson:"expired_time" gorm:"column:expired_time;type:timestamp;index" description:"策略过期时间" optional:"true"`
|
||||
// 只读策略, 不允许用户修改, 一般用于系统管理
|
||||
ReadOnly bool `json:"read_only" bson:"read_only" gorm:"column:read_only;type:tinyint(1)" description:"只读策略, 不允许用户修改, 一般用于系统管理" optional:"true"`
|
||||
// 该策略是否启用
|
||||
Enabled bool `json:"enabled" bson:"enabled" gorm:"column:enabled;type:tinyint(1)" description:"该策略是否启用" optional:"true"`
|
||||
// 策略标签
|
||||
Label string `json:"label" gorm:"column:label;type:varchar(200);index" description:"策略标签" optional:"true"`
|
||||
// 扩展信息
|
||||
Extras map[string]string `json:"extras" bson:"extras" gorm:"column:extras;serializer:json;type:json" description:"扩展信息" optional:"true"`
|
||||
}
|
||||
|
||||
func (r *CreatePolicyRequest) Validate() error {
|
||||
return validator.Validate(r)
|
||||
}
|
||||
|
||||
func (r *CreatePolicyRequest) SetNamespaceId(namespaceId uint64) *CreatePolicyRequest {
|
||||
r.NamespaceId = &namespaceId
|
||||
return r
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
_ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/user/api"
|
||||
_ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/user/impl"
|
||||
|
||||
_ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token/api"
|
||||
_ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token/impl"
|
||||
|
||||
// 鉴权
|
||||
_ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/endpoint/impl"
|
||||
_ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/namespace/impl"
|
||||
_ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/policy/impl"
|
||||
_ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/role/impl"
|
||||
|
||||
// 颁发器
|
||||
_ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token/issuers"
|
||||
// 鉴权中间件
|
||||
_ "122.51.31.227/go-course/go18/devcloud/mcenter/permission"
|
||||
)
|
@ -1,2 +0,0 @@
|
||||
# 角色管理
|
||||
|
@ -1,102 +0,0 @@
|
||||
package role
|
||||
|
||||
import (
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/endpoint"
|
||||
"github.com/infraboard/mcube/v2/tools/pretty"
|
||||
"github.com/infraboard/modules/iam/apps"
|
||||
)
|
||||
|
||||
func NewApiPermission(roleId uint64, spec *ApiPermissionSpec) *ApiPermission {
|
||||
return &ApiPermission{
|
||||
ResourceMeta: *apps.NewResourceMeta(),
|
||||
RoleId: roleId,
|
||||
ApiPermissionSpec: *spec,
|
||||
}
|
||||
}
|
||||
|
||||
type ApiPermission struct {
|
||||
// 基础数据
|
||||
apps.ResourceMeta
|
||||
// 角色Id
|
||||
RoleId uint64 `json:"role_id" gorm:"column:role_id;index" description:"角色Id"`
|
||||
// Api权限定义
|
||||
ApiPermissionSpec
|
||||
}
|
||||
|
||||
func (r *ApiPermission) TableName() string {
|
||||
return "api_permissions"
|
||||
}
|
||||
|
||||
func (r *ApiPermission) String() string {
|
||||
return pretty.ToJSON(r)
|
||||
}
|
||||
|
||||
func NewResourceActionApiPermissionSpec(service, resource, action string) *ApiPermissionSpec {
|
||||
return &ApiPermissionSpec{
|
||||
Extras: map[string]string{},
|
||||
MatchBy: MATCH_BY_RESOURCE_ACTION,
|
||||
Service: service,
|
||||
Resource: resource,
|
||||
Action: action,
|
||||
}
|
||||
}
|
||||
|
||||
type ApiPermissionSpec struct {
|
||||
// 创建者ID
|
||||
CreateBy uint64 `json:"create_by" gorm:"column:create_by" description:"创建者ID" optional:"true"`
|
||||
// 角色描述
|
||||
Description string `json:"description" gorm:"column:description;type:text" bson:"description" description:"角色描述"`
|
||||
// 权限匹配方式
|
||||
MatchBy MATCH_BY `json:"match_by" gorm:"column:match_by;type:tinyint(1);index" bson:"match_by" description:"权限匹配方式"`
|
||||
// MATCH_BY_ID 时指定的 Endpoint Id
|
||||
EndpointId *uint64 `json:"endpoint_id" gorm:"column:endpoint_id;type:uint;index"`
|
||||
// 操作标签
|
||||
Label string `json:"label" gorm:"column:label;type:varchar(200);index"`
|
||||
// 服务
|
||||
Service string `json:"service" gorm:"column:service;type:varchar(100);index" bson:"service" description:"服务名称"`
|
||||
// 资源列表
|
||||
Resource string `json:"resource" gorm:"column:resource;type:varchar(100);index" bson:"resource" description:"资源名称"`
|
||||
// 资源操作
|
||||
Action string `json:"action" bson:"action" gorm:"column:action;type:varchar(100);index"`
|
||||
// 读或者读写
|
||||
AccessMode endpoint.ACCESS_MODE `json:"access_mode" bson:"access_mode" gorm:"column:access_mode;type:tinyint(1);index"`
|
||||
|
||||
// 其他扩展信息
|
||||
Extras map[string]string `json:"extras" gorm:"column:extras;serializer:json;type:json" description:"其他扩展信息" optional:"true"`
|
||||
}
|
||||
|
||||
func (a *ApiPermissionSpec) GetEndpointId() uint64 {
|
||||
if a.EndpointId == nil {
|
||||
return 0
|
||||
}
|
||||
return *a.EndpointId
|
||||
}
|
||||
|
||||
// 判断是否有当前API的访问权限
|
||||
func (a *ApiPermissionSpec) IsMatch(target *endpoint.Endpoint) bool {
|
||||
switch a.MatchBy {
|
||||
case MATCH_BY_ID:
|
||||
if a.EndpointId == nil {
|
||||
return false
|
||||
}
|
||||
if *a.EndpointId == target.Id {
|
||||
return true
|
||||
}
|
||||
case MATCH_BY_RESOURCE_ACCESS_MODE:
|
||||
if a.AccessMode == target.AccessMode {
|
||||
return true
|
||||
}
|
||||
case MATCH_BY_RESOURCE_ACTION:
|
||||
if a.Service != "*" && a.Service != target.Service {
|
||||
return false
|
||||
}
|
||||
if a.Resource != "*" && a.Resource != target.Resource {
|
||||
return false
|
||||
}
|
||||
if a.Action != "*" && a.Action != target.Action {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram id="prDIldifm9lRc8bqSxY8" name="第 1 页">
|
||||
<mxGraphModel dx="892" dy="439" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="2" value="Role" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="210" y="230" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="6" style="edgeStyle=none;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="3" target="5">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="12" style="edgeStyle=none;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="3" target="2">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="3" value="Api Permission<div>mapping</div>" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="390" y="120" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="9" style="edgeStyle=none;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="4" target="7">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="13" style="edgeStyle=none;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="4" target="2">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="4" value="View Permission<div>mapping</div>" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="390" y="340" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="Endpont" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="610" y="120" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="10" style="edgeStyle=none;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="7" target="8">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="7" value="Menu" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="610" y="340" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="8" value="Page" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="770" y="340" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
@ -1,18 +0,0 @@
|
||||
package role
|
||||
|
||||
const (
|
||||
ADMIN = "admin"
|
||||
)
|
||||
|
||||
type MATCH_BY int32
|
||||
|
||||
const (
|
||||
// 针对某一个具体的接口进行授权
|
||||
MATCH_BY_ID = iota
|
||||
// 通过标签来进行 API接口授权
|
||||
MATCH_BY_LABLE
|
||||
// 通过资源和动作来进行授权, user::list
|
||||
MATCH_BY_RESOURCE_ACTION
|
||||
// 通过资源的访问模式进行授权
|
||||
MATCH_BY_RESOURCE_ACCESS_MODE
|
||||
)
|
@ -1,110 +0,0 @@
|
||||
package impl
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/endpoint"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/role"
|
||||
"github.com/infraboard/mcube/v2/exception"
|
||||
"github.com/infraboard/mcube/v2/ioc/config/datasource"
|
||||
"github.com/infraboard/mcube/v2/types"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 添加角色关联API
|
||||
func (i *RoleServiceImpl) AddApiPermission(ctx context.Context, in *role.AddApiPermissionRequest) ([]*role.ApiPermission, error) {
|
||||
if err := in.Validate(); err != nil {
|
||||
return nil, exception.NewBadRequest("validate add api permission error, %s", err)
|
||||
}
|
||||
|
||||
perms := []*role.ApiPermission{}
|
||||
if err := datasource.DBFromCtx(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
for i := range in.Items {
|
||||
item := in.Items[i]
|
||||
perm := role.NewApiPermission(in.RoleId, item)
|
||||
if err := tx.Save(perm).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
perms = append(perms, perm)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return perms, nil
|
||||
}
|
||||
|
||||
// 查询角色关联的权限条目
|
||||
func (i *RoleServiceImpl) QueryApiPermission(ctx context.Context, in *role.QueryApiPermissionRequest) ([]*role.ApiPermission, error) {
|
||||
query := datasource.DBFromCtx(ctx).Model(&role.ApiPermission{})
|
||||
if len(in.RoleIds) > 0 {
|
||||
query = query.Where("role_id IN ?", in.RoleIds)
|
||||
}
|
||||
if len(in.ApiPermissionIds) > 0 {
|
||||
query = query.Where("id IN ?", in.ApiPermissionIds)
|
||||
}
|
||||
|
||||
perms := []*role.ApiPermission{}
|
||||
if err := query.
|
||||
Order("created_at desc").
|
||||
Find(&perms).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return perms, nil
|
||||
}
|
||||
|
||||
// 移除角色关联API
|
||||
func (i *RoleServiceImpl) RemoveApiPermission(ctx context.Context, in *role.RemoveApiPermissionRequest) ([]*role.ApiPermission, error) {
|
||||
if err := in.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
perms, err := i.QueryApiPermission(ctx, role.NewQueryApiPermissionRequest().AddRoleId(in.RoleId).AddPermissionId(in.ApiPermissionIds...))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := datasource.DBFromCtx(ctx).
|
||||
Where("role_id = ?", in.RoleId).
|
||||
Where("id IN ?", in.ApiPermissionIds).
|
||||
Delete(&role.ApiPermission{}).
|
||||
Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return perms, nil
|
||||
}
|
||||
|
||||
// 查询匹配到的Api接口列表
|
||||
func (i *RoleServiceImpl) QueryMatchedEndpoint(ctx context.Context, in *role.QueryMatchedEndpointRequest) (*types.Set[*endpoint.Endpoint], error) {
|
||||
set := types.New[*endpoint.Endpoint]()
|
||||
|
||||
// 查询角色的权限
|
||||
perms, err := i.QueryApiPermission(ctx, role.NewQueryApiPermissionRequest().AddRoleId(in.RoleIds...))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 查询服务的Endpoint列表
|
||||
endpointReq := endpoint.NewQueryEndpointRequest()
|
||||
for _, perm := range perms {
|
||||
endpointReq.WithService(perm.Service)
|
||||
}
|
||||
endpoints, err := endpoint.GetService().QueryEndpoint(ctx, endpointReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 找出能匹配的API
|
||||
endpoints.ForEach(func(t *endpoint.Endpoint) {
|
||||
for _, perm := range perms {
|
||||
if perm.IsMatch(t) {
|
||||
if !endpoint.IsEndpointExist(set, t) {
|
||||
set.Add(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return set, nil
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
package impl_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/role"
|
||||
)
|
||||
|
||||
func TestQueryApiPermission(t *testing.T) {
|
||||
req := role.NewQueryApiPermissionRequest()
|
||||
req.AddRoleId(2)
|
||||
set, err := impl.QueryApiPermission(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(set)
|
||||
}
|
||||
|
||||
func TestAddApiPermission(t *testing.T) {
|
||||
req := role.NewAddApiPermissionRequest(1)
|
||||
req.Add(role.NewResourceActionApiPermissionSpec("devcloud", "user", "list"))
|
||||
set, err := impl.AddApiPermission(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(set)
|
||||
}
|
||||
|
||||
func TestQueryMatchedEndpoint(t *testing.T) {
|
||||
req := role.NewQueryMatchedEndpointRequest()
|
||||
req.Add(1)
|
||||
set, err := impl.QueryMatchedEndpoint(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(set)
|
||||
}
|
||||
|
||||
func TestRemoveApiPermission(t *testing.T) {
|
||||
req := role.NewRemoveApiPermissionRequest(2)
|
||||
req.Add(2)
|
||||
set, err := impl.RemoveApiPermission(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Log(set)
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package impl_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/role"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/test"
|
||||
)
|
||||
|
||||
var (
|
||||
impl role.Service
|
||||
ctx = context.Background()
|
||||
)
|
||||
|
||||
func init() {
|
||||
test.DevelopmentSetUp()
|
||||
impl = role.GetService()
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package impl
|
||||
|
||||
import (
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/role"
|
||||
"github.com/infraboard/mcube/v2/ioc"
|
||||
"github.com/infraboard/mcube/v2/ioc/config/datasource"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ioc.Controller().Registry(&RoleServiceImpl{})
|
||||
}
|
||||
|
||||
var _ role.Service = (*RoleServiceImpl)(nil)
|
||||
|
||||
type RoleServiceImpl struct {
|
||||
ioc.ObjectImpl
|
||||
}
|
||||
|
||||
func (i *RoleServiceImpl) Init() error {
|
||||
if datasource.Get().AutoMigrate {
|
||||
err := datasource.DB().AutoMigrate(&role.Role{}, &role.ApiPermission{}, &role.ViewPermission{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *RoleServiceImpl) Name() string {
|
||||
return role.AppName
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
package impl
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/role"
|
||||
"github.com/infraboard/mcube/v2/exception"
|
||||
"github.com/infraboard/mcube/v2/ioc/config/datasource"
|
||||
"github.com/infraboard/mcube/v2/types"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 创建角色
|
||||
func (i *RoleServiceImpl) CreateRole(ctx context.Context, in *role.CreateRoleRequest) (*role.Role, error) {
|
||||
if err := in.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ins := role.NewRole()
|
||||
ins.CreateRoleRequest = *in
|
||||
|
||||
if err := datasource.DBFromCtx(ctx).
|
||||
Create(ins).
|
||||
Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ins, nil
|
||||
}
|
||||
|
||||
// 列表查询
|
||||
func (i *RoleServiceImpl) QueryRole(ctx context.Context, in *role.QueryRoleRequest) (*types.Set[*role.Role], error) {
|
||||
set := types.New[*role.Role]()
|
||||
|
||||
query := datasource.DBFromCtx(ctx).Model(&role.Role{})
|
||||
if len(in.RoleIds) > 0 {
|
||||
query = query.Where("id IN ?", in.RoleIds)
|
||||
in.PageSize = uint64(len(in.RoleIds))
|
||||
}
|
||||
err := query.Count(&set.Total).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = query.
|
||||
Order("created_at desc").
|
||||
Offset(int(in.ComputeOffset())).
|
||||
Limit(int(in.PageSize)).
|
||||
Find(&set.Items).
|
||||
Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return set, nil
|
||||
}
|
||||
|
||||
// 详情查询
|
||||
func (i *RoleServiceImpl) DescribeRole(ctx context.Context, in *role.DescribeRoleRequest) (*role.Role, error) {
|
||||
query := datasource.DBFromCtx(ctx)
|
||||
|
||||
ins := &role.Role{}
|
||||
if err := query.Where("id = ?", in.Id).First(ins).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, exception.NewNotFound("role %d not found", in.Id)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pm, err := i.QueryApiPermission(ctx, role.NewQueryApiPermissionRequest().AddRoleId(in.Id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ins.ApiPermissions = pm
|
||||
|
||||
return ins, nil
|
||||
}
|
||||
|
||||
// 更新角色
|
||||
func (i *RoleServiceImpl) UpdateRole(ctx context.Context, in *role.UpdateRoleRequest) (*role.Role, error) {
|
||||
descReq := role.NewDescribeRoleRequest()
|
||||
descReq.SetId(in.Id)
|
||||
ins, err := i.DescribeRole(ctx, descReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ins.CreateRoleRequest = in.CreateRoleRequest
|
||||
return ins, datasource.DBFromCtx(ctx).Where("id = ?", in.Id).Updates(ins).Error
|
||||
}
|
||||
|
||||
// 删除角色
|
||||
func (i *RoleServiceImpl) DeleteRole(ctx context.Context, in *role.DeleteRoleRequest) (*role.Role, error) {
|
||||
descReq := role.NewDescribeRoleRequest()
|
||||
descReq.SetId(in.Id)
|
||||
ins, err := i.DescribeRole(ctx, descReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ins, datasource.DBFromCtx(ctx).
|
||||
Where("id = ?", in.Id).
|
||||
Delete(&role.Role{}).
|
||||
Error
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
package impl_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/role"
|
||||
)
|
||||
|
||||
func TestQueryRole(t *testing.T) {
|
||||
req := role.NewQueryRoleRequest()
|
||||
set, err := impl.QueryRole(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(set)
|
||||
}
|
||||
|
||||
func TestDescribeRole(t *testing.T) {
|
||||
req := role.NewDescribeRoleRequest()
|
||||
req.SetId(1)
|
||||
ins, err := impl.DescribeRole(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(ins)
|
||||
}
|
||||
|
||||
func TestCreateAdminRole(t *testing.T) {
|
||||
req := role.NewCreateRoleRequest()
|
||||
req.Name = "admin"
|
||||
req.Description = "管理员"
|
||||
ins, err := impl.CreateRole(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(ins)
|
||||
}
|
||||
|
||||
func TestCreateGuestRole(t *testing.T) {
|
||||
req := role.NewCreateRoleRequest()
|
||||
req.Name = "guest"
|
||||
req.Description = "访客"
|
||||
ins, err := impl.CreateRole(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(ins)
|
||||
}
|
||||
|
||||
func TestCreateDevRole(t *testing.T) {
|
||||
req := role.NewCreateRoleRequest()
|
||||
req.Name = "dev"
|
||||
req.Description = "开发"
|
||||
ins, err := impl.CreateRole(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(ins)
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
package impl
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/role"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/view"
|
||||
"github.com/infraboard/mcube/v2/exception"
|
||||
"github.com/infraboard/mcube/v2/ioc/config/datasource"
|
||||
"github.com/infraboard/mcube/v2/types"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 添加角色关联菜单
|
||||
func (i *RoleServiceImpl) AddViewPermission(ctx context.Context, in *role.AddViewPermissionRequest) ([]*role.ViewPermission, error) {
|
||||
if err := in.Validate(); err != nil {
|
||||
return nil, exception.NewBadRequest("validate add view permission error, %s", err)
|
||||
}
|
||||
|
||||
perms := []*role.ViewPermission{}
|
||||
if err := datasource.DBFromCtx(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
for i := range in.Items {
|
||||
item := in.Items[i]
|
||||
perm := role.NewViewPermission(in.RoleId, item)
|
||||
if err := tx.Save(perm).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
perms = append(perms, perm)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return perms, nil
|
||||
}
|
||||
|
||||
// 查询角色关联的视图权限
|
||||
func (i *RoleServiceImpl) QueryViewPermission(ctx context.Context, in *role.QueryViewPermissionRequest) ([]*role.ViewPermission, error) {
|
||||
query := datasource.DBFromCtx(ctx).Model(&role.ViewPermission{})
|
||||
if len(in.RoleIds) > 0 {
|
||||
query = query.Where("role_id IN ?", in.RoleIds)
|
||||
}
|
||||
if len(in.ViewPermissionIds) > 0 {
|
||||
query = query.Where("in IN ?", in.ViewPermissionIds)
|
||||
}
|
||||
|
||||
perms := []*role.ViewPermission{}
|
||||
if err := query.Order("created_at desc").
|
||||
Where("id IN ?", in.RoleIds).
|
||||
Find(&perms).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return perms, nil
|
||||
}
|
||||
|
||||
// 移除角色关联菜单
|
||||
func (i *RoleServiceImpl) RemoveViewPermission(ctx context.Context, in *role.RemoveViewPermissionRequest) ([]*role.ViewPermission, error) {
|
||||
if err := in.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
perms, err := i.QueryViewPermission(ctx, role.NewQueryViewPermissionRequest().AddRoleId(in.RoleId).AddPermissionId(in.ViewPermissionIds...))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := datasource.DBFromCtx(ctx).
|
||||
Where("role_id = ?", in.RoleId).
|
||||
Where("id IN ?", in.ViewPermissionIds).
|
||||
Delete(&role.ViewPermission{}).
|
||||
Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return perms, nil
|
||||
}
|
||||
|
||||
// 查询能匹配到视图菜单
|
||||
func (i *RoleServiceImpl) QueryMatchedPage(ctx context.Context, in *role.QueryMatchedPageRequest) (*types.Set[*view.Menu], error) {
|
||||
return nil, nil
|
||||
}
|
@ -1,265 +0,0 @@
|
||||
package role
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/endpoint"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/view"
|
||||
"github.com/infraboard/mcube/v2/http/request"
|
||||
"github.com/infraboard/mcube/v2/ioc"
|
||||
"github.com/infraboard/mcube/v2/ioc/config/validator"
|
||||
"github.com/infraboard/mcube/v2/types"
|
||||
"github.com/infraboard/modules/iam/apps"
|
||||
)
|
||||
|
||||
const (
|
||||
AppName = "role"
|
||||
)
|
||||
|
||||
func GetService() Service {
|
||||
return ioc.Controller().Get(AppName).(Service)
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
RoleService
|
||||
ApiPermissionService
|
||||
ViewPermissionService
|
||||
}
|
||||
|
||||
// 角色管理
|
||||
type RoleService interface {
|
||||
// 创建角色
|
||||
CreateRole(context.Context, *CreateRoleRequest) (*Role, error)
|
||||
// 列表查询
|
||||
QueryRole(context.Context, *QueryRoleRequest) (*types.Set[*Role], error)
|
||||
// 详情查询
|
||||
DescribeRole(context.Context, *DescribeRoleRequest) (*Role, error)
|
||||
// 更新角色
|
||||
UpdateRole(context.Context, *UpdateRoleRequest) (*Role, error)
|
||||
// 删除角色
|
||||
DeleteRole(context.Context, *DeleteRoleRequest) (*Role, error)
|
||||
}
|
||||
|
||||
func NewQueryRoleRequest() *QueryRoleRequest {
|
||||
return &QueryRoleRequest{
|
||||
PageRequest: request.NewDefaultPageRequest(),
|
||||
RoleIds: []uint64{},
|
||||
}
|
||||
}
|
||||
|
||||
type QueryRoleRequest struct {
|
||||
*request.PageRequest
|
||||
WithMenuPermission bool `json:"with_menu_permission" form:"with_menu_permission"`
|
||||
WithApiPermission bool `json:"with_api_permission" form:"with_api_permission"`
|
||||
RoleIds []uint64 `json:"role_ids" form:"role_ids"`
|
||||
}
|
||||
|
||||
func (r *QueryRoleRequest) AddRoleId(roleIds ...uint64) *QueryRoleRequest {
|
||||
for _, rid := range roleIds {
|
||||
if !slices.Contains(r.RoleIds, rid) {
|
||||
r.RoleIds = append(r.RoleIds, rid)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func NewDescribeRoleRequest() *DescribeRoleRequest {
|
||||
return &DescribeRoleRequest{}
|
||||
}
|
||||
|
||||
type DescribeRoleRequest struct {
|
||||
apps.GetRequest
|
||||
}
|
||||
|
||||
type UpdateRoleRequest struct {
|
||||
apps.GetRequest
|
||||
CreateRoleRequest
|
||||
}
|
||||
|
||||
func NewDeleteRoleRequest() *DeleteRoleRequest {
|
||||
return &DeleteRoleRequest{}
|
||||
}
|
||||
|
||||
type DeleteRoleRequest struct {
|
||||
apps.GetRequest
|
||||
}
|
||||
|
||||
// 角色API接口管理
|
||||
type ApiPermissionService interface {
|
||||
// 查询角色关联的权限条目
|
||||
QueryApiPermission(context.Context, *QueryApiPermissionRequest) ([]*ApiPermission, error)
|
||||
// 添加角色关联API
|
||||
AddApiPermission(context.Context, *AddApiPermissionRequest) ([]*ApiPermission, error)
|
||||
// 移除角色关联API
|
||||
RemoveApiPermission(context.Context, *RemoveApiPermissionRequest) ([]*ApiPermission, error)
|
||||
// 查询匹配到的Api接口列表
|
||||
QueryMatchedEndpoint(context.Context, *QueryMatchedEndpointRequest) (*types.Set[*endpoint.Endpoint], error)
|
||||
}
|
||||
|
||||
func NewQueryApiPermissionRequest() *QueryApiPermissionRequest {
|
||||
return &QueryApiPermissionRequest{
|
||||
RoleIds: []uint64{},
|
||||
ApiPermissionIds: []uint64{},
|
||||
}
|
||||
}
|
||||
|
||||
type QueryApiPermissionRequest struct {
|
||||
RoleIds []uint64 `json:"role_ids"`
|
||||
ApiPermissionIds []uint64 `json:"api_permission_ids"`
|
||||
}
|
||||
|
||||
func (r *QueryApiPermissionRequest) AddRoleId(roleIds ...uint64) *QueryApiPermissionRequest {
|
||||
r.RoleIds = append(r.RoleIds, roleIds...)
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *QueryApiPermissionRequest) AddPermissionId(permissionIds ...uint64) *QueryApiPermissionRequest {
|
||||
r.ApiPermissionIds = append(r.ApiPermissionIds, permissionIds...)
|
||||
return r
|
||||
}
|
||||
|
||||
func NewQueryMatchedEndpointRequest() *QueryMatchedEndpointRequest {
|
||||
return &QueryMatchedEndpointRequest{
|
||||
RoleIds: []uint64{},
|
||||
}
|
||||
}
|
||||
|
||||
type QueryMatchedEndpointRequest struct {
|
||||
RoleIds []uint64 `json:"role_ids" form:"role_ids"`
|
||||
}
|
||||
|
||||
func (r *QueryMatchedEndpointRequest) Add(roleIds ...uint64) *QueryMatchedEndpointRequest {
|
||||
for _, rid := range roleIds {
|
||||
if !slices.Contains(r.RoleIds, rid) {
|
||||
r.RoleIds = append(r.RoleIds, rid)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func NewAddApiPermissionRequest(roleId uint64) *AddApiPermissionRequest {
|
||||
return &AddApiPermissionRequest{
|
||||
RoleId: roleId,
|
||||
}
|
||||
}
|
||||
|
||||
type AddApiPermissionRequest struct {
|
||||
RoleId uint64 `json:"role_id"`
|
||||
Items []*ApiPermissionSpec `json:"items"`
|
||||
}
|
||||
|
||||
func (r *AddApiPermissionRequest) Validate() error {
|
||||
return validator.Validate(r)
|
||||
}
|
||||
|
||||
func (r *AddApiPermissionRequest) Add(specs ...*ApiPermissionSpec) *AddApiPermissionRequest {
|
||||
r.Items = append(r.Items, specs...)
|
||||
return r
|
||||
}
|
||||
|
||||
func NewRemoveApiPermissionRequest(roleId uint64) *RemoveApiPermissionRequest {
|
||||
return &RemoveApiPermissionRequest{
|
||||
RoleId: roleId,
|
||||
ApiPermissionIds: []uint64{},
|
||||
}
|
||||
}
|
||||
|
||||
type RemoveApiPermissionRequest struct {
|
||||
RoleId uint64 `json:"role_id"`
|
||||
ApiPermissionIds []uint64 `json:"api_permission_ids"`
|
||||
}
|
||||
|
||||
func (r *RemoveApiPermissionRequest) Add(apiPermissionIds ...uint64) *RemoveApiPermissionRequest {
|
||||
r.ApiPermissionIds = append(r.ApiPermissionIds, apiPermissionIds...)
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *RemoveApiPermissionRequest) Validate() error {
|
||||
return validator.Validate(r)
|
||||
}
|
||||
|
||||
type UpdateApiPermissionRequest struct {
|
||||
Items []*ApiPermission `json:"items"`
|
||||
}
|
||||
|
||||
// 角色菜单管理
|
||||
type ViewPermissionService interface {
|
||||
// 查询角色关联的视图权限
|
||||
QueryViewPermission(context.Context, *QueryViewPermissionRequest) ([]*ViewPermission, error)
|
||||
// 添加角色关联菜单
|
||||
AddViewPermission(context.Context, *AddViewPermissionRequest) ([]*ViewPermission, error)
|
||||
// 移除角色关联菜单
|
||||
RemoveViewPermission(context.Context, *RemoveViewPermissionRequest) ([]*ViewPermission, error)
|
||||
// 查询能匹配到视图菜单
|
||||
QueryMatchedPage(context.Context, *QueryMatchedPageRequest) (*types.Set[*view.Menu], error)
|
||||
}
|
||||
|
||||
func NewQueryViewPermissionRequest() *QueryViewPermissionRequest {
|
||||
return &QueryViewPermissionRequest{
|
||||
RoleIds: []uint64{},
|
||||
ViewPermissionIds: []uint64{},
|
||||
}
|
||||
}
|
||||
|
||||
type QueryViewPermissionRequest struct {
|
||||
RoleIds []uint64 `json:"role_ids"`
|
||||
ViewPermissionIds []uint64 `json:"view_permission_ids"`
|
||||
}
|
||||
|
||||
func (r *QueryViewPermissionRequest) AddRoleId(roleIds ...uint64) *QueryViewPermissionRequest {
|
||||
r.RoleIds = append(r.RoleIds, roleIds...)
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *QueryViewPermissionRequest) AddPermissionId(permissionIds ...uint64) *QueryViewPermissionRequest {
|
||||
r.ViewPermissionIds = append(r.ViewPermissionIds, permissionIds...)
|
||||
return r
|
||||
}
|
||||
|
||||
func NewQueryMatchedPageRequest() *QueryMatchedPageRequest {
|
||||
return &QueryMatchedPageRequest{}
|
||||
}
|
||||
|
||||
type QueryMatchedPageRequest struct {
|
||||
apps.GetRequest
|
||||
}
|
||||
|
||||
func NewAddViewPermissionRequest() *AddViewPermissionRequest {
|
||||
return &AddViewPermissionRequest{
|
||||
Items: []*ViewPermissionSpec{},
|
||||
}
|
||||
}
|
||||
|
||||
type AddViewPermissionRequest struct {
|
||||
RoleId uint64 `json:"role_id"`
|
||||
Items []*ViewPermissionSpec `json:"items"`
|
||||
}
|
||||
|
||||
func (r *AddViewPermissionRequest) Validate() error {
|
||||
return validator.Validate(r)
|
||||
}
|
||||
|
||||
func (r *AddViewPermissionRequest) Add(specs ...*ViewPermissionSpec) *AddViewPermissionRequest {
|
||||
r.Items = append(r.Items, specs...)
|
||||
return r
|
||||
}
|
||||
|
||||
type UpdateViewPermission struct {
|
||||
Items []ViewPermission `json:"items"`
|
||||
}
|
||||
|
||||
func NewRemoveViewPermissionRequest() *RemoveViewPermissionRequest {
|
||||
return &RemoveViewPermissionRequest{
|
||||
ViewPermissionIds: []uint64{},
|
||||
}
|
||||
}
|
||||
|
||||
type RemoveViewPermissionRequest struct {
|
||||
RoleId uint64 `json:"role_id"`
|
||||
ViewPermissionIds []uint64 `json:"menu_permission_ids"`
|
||||
}
|
||||
|
||||
func (r *RemoveViewPermissionRequest) Validate() error {
|
||||
return validator.Validate(r)
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
package role
|
||||
|
||||
import (
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/endpoint"
|
||||
"github.com/infraboard/mcube/v2/ioc/config/validator"
|
||||
"github.com/infraboard/mcube/v2/tools/pretty"
|
||||
"github.com/infraboard/modules/iam/apps"
|
||||
)
|
||||
|
||||
func NewRole() *Role {
|
||||
return &Role{
|
||||
ResourceMeta: *apps.NewResourceMeta(),
|
||||
MenuPermissions: []*ViewPermission{},
|
||||
ApiPermissions: []*ApiPermission{},
|
||||
}
|
||||
}
|
||||
|
||||
type Role struct {
|
||||
// 基础数据
|
||||
apps.ResourceMeta
|
||||
// 角色创建信息
|
||||
CreateRoleRequest
|
||||
// 菜单权限
|
||||
MenuPermissions []*ViewPermission `json:"menu_permissions,omitempty" gorm:"-" description:"角色关联的菜单权限"`
|
||||
// API权限
|
||||
ApiPermissions []*ApiPermission `json:"api_permissions,omitempty" gorm:"-" description:"角色关联的API权限"`
|
||||
}
|
||||
|
||||
func (r *Role) TableName() string {
|
||||
return "roles"
|
||||
}
|
||||
|
||||
func (r *Role) String() string {
|
||||
return pretty.ToJSON(r)
|
||||
}
|
||||
|
||||
// 该角色是否允许该API访问
|
||||
func (r *Role) CheckPerm(re *endpoint.RouteEntry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewCreateRoleRequest() *CreateRoleRequest {
|
||||
return &CreateRoleRequest{
|
||||
Extras: map[string]string{},
|
||||
Enabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
type CreateRoleRequest struct {
|
||||
// 创建者ID
|
||||
CreateBy uint64 `json:"create_by" gorm:"column:create_by" description:"创建者ID" optional:"true"`
|
||||
// 角色名称
|
||||
Name string `json:"name" gorm:"column:name;type:varchar(100);index" bson:"name" description:"角色名称"`
|
||||
// 角色描述
|
||||
Description string `json:"description" gorm:"column:description;type:text" bson:"description" description:"角色描述"`
|
||||
// 是否启用
|
||||
Enabled bool `json:"enabled" bson:"enabled" gorm:"column:enabled;type:tinyint(1)" description:"是否启用" optional:"true"`
|
||||
// 标签
|
||||
Label string `json:"label" gorm:"column:label;type:varchar(200);index" description:"标签" optional:"true"`
|
||||
// 其他扩展信息
|
||||
Extras map[string]string `json:"extras" gorm:"column:extras;serializer:json;type:json" description:"其他扩展信息" optional:"true"`
|
||||
}
|
||||
|
||||
func (r *CreateRoleRequest) Validate() error {
|
||||
return validator.Validate(r)
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package role
|
||||
|
||||
import (
|
||||
"github.com/infraboard/modules/iam/apps"
|
||||
)
|
||||
|
||||
func NewViewPermission(roleId uint64, spec *ViewPermissionSpec) *ViewPermission {
|
||||
return &ViewPermission{
|
||||
ResourceMeta: *apps.NewResourceMeta(),
|
||||
RoleId: roleId,
|
||||
ViewPermissionSpec: *spec,
|
||||
}
|
||||
}
|
||||
|
||||
type ViewPermission struct {
|
||||
// 基础数据
|
||||
apps.ResourceMeta
|
||||
// 角色Id
|
||||
RoleId uint64 `json:"role_id" gorm:"column:role_id;index" description:"Role Id"`
|
||||
// Menu权限定义
|
||||
ViewPermissionSpec
|
||||
}
|
||||
|
||||
func (r *ViewPermission) TableName() string {
|
||||
return "view_permissions"
|
||||
}
|
||||
|
||||
func NewViewPermissionSpec() *ViewPermissionSpec {
|
||||
return &ViewPermissionSpec{
|
||||
Extras: map[string]string{},
|
||||
}
|
||||
}
|
||||
|
||||
type ViewPermissionSpec struct {
|
||||
// 创建者ID
|
||||
CreateBy uint64 `json:"create_by" gorm:"column:create_by" description:"创建者ID" optional:"true"`
|
||||
// 权限描述
|
||||
Description string `json:"description" gorm:"column:description;type:text" bson:"description" description:"角色描述"`
|
||||
// 页面路径
|
||||
PagePath string `json:"path_path" gorm:"column:path_path;type:varchar(200);index" bson:"path_path" description:"页面路径(可以通配)"`
|
||||
// 组件名称
|
||||
Components []string `json:"components" gorm:"column:components;type:json;serializer:json" bson:"components" description:"页面组件(可以通配)"`
|
||||
// 其他扩展信息
|
||||
Extras map[string]string `json:"extras" gorm:"column:extras;serializer:json;type:json" description:"其他扩展信息" optional:"true"`
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
# 令牌管理
|
||||
|
||||
+ 颁发访问令牌: Login
|
||||
+ 撤销访问令牌: 令牌失效了 Logout
|
||||
+ 校验访问令牌:检查令牌的合法性, 是不是伪造的
|
||||
|
||||
## 详情设计
|
||||
|
||||
字段(业务需求)
|
||||
|
||||
令牌:
|
||||
+ 过期时间
|
||||
+ 颁发时间
|
||||
+ 被颁发的人
|
||||
+ ...
|
||||
|
||||
|
||||
问题: 无刷新功能, 令牌到期了,自动退出了, 过期时间设置长一点, 长时间不过期 又有安全问题
|
||||
1. 业务功能: 令牌的刷新, 令牌过期了过后,允许用户进行刷新(需要使用刷新Token来刷新, 刷新Token也是需要有过期时间, 这个时间决定回话长度),有了刷新token用户不会出现 使用中被中断的情况, 并且长时间未使用,系统也户自动退出(刷新Token过期)
|
||||
|
||||
|
||||
## 转化为接口定义
|
||||
|
||||
```go
|
||||
type Service interface {
|
||||
// 颁发访问令牌: Login
|
||||
IssueToken(context.Context, *IssueTokenRequest) (*Token, error)
|
||||
// 撤销访问令牌: 令牌失效了 Logout
|
||||
RevolkToken(context.Context, *RevolkTokenRequest) (*Token, error)
|
||||
|
||||
// 校验访问令牌:检查令牌的合法性, 是不是伪造的
|
||||
ValiateToken(context.Context, *ValiateTokenRequest) (*Token, error)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 接口的实现
|
||||
|
||||
```go
|
||||
func TestIssueToken(t *testing.T) {
|
||||
req := token.NewIssueTokenRequest()
|
||||
req.IssueByPassword("admin", "123456")
|
||||
req.Source = token.SOURCE_WEB
|
||||
set, err := svc.IssueToken(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(set)
|
||||
}
|
||||
|
||||
func TestQueryToken(t *testing.T) {
|
||||
req := token.NewQueryTokenRequest()
|
||||
set, err := svc.QueryToken(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(set)
|
||||
}
|
||||
```
|
||||
|
@ -1,72 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/token"
|
||||
"github.com/infraboard/mcube/v2/ioc"
|
||||
"github.com/infraboard/mcube/v2/ioc/config/gorestful"
|
||||
|
||||
restfulspec "github.com/emicklei/go-restful-openapi/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ioc.Api().Registry(&TokenRestfulApiHandler{})
|
||||
}
|
||||
|
||||
type TokenRestfulApiHandler struct {
|
||||
ioc.ObjectImpl
|
||||
|
||||
// 依赖控制器
|
||||
svc token.Service
|
||||
}
|
||||
|
||||
func (h *TokenRestfulApiHandler) Name() string {
|
||||
return token.APP_NAME
|
||||
}
|
||||
|
||||
//go:embed docs/login.md
|
||||
var loginApiDocNotes string
|
||||
|
||||
func (h *TokenRestfulApiHandler) Init() error {
|
||||
h.svc = token.GetService()
|
||||
|
||||
tags := []string{"登录管理"}
|
||||
ws := gorestful.ObjectRouter(h)
|
||||
ws.Route(ws.POST("").To(h.Login).
|
||||
Doc("颁发令牌(登录)").
|
||||
Notes(loginApiDocNotes).
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Reads(token.IssueTokenRequest{}).
|
||||
Writes(token.Token{}).
|
||||
Returns(200, "OK", token.Token{}))
|
||||
|
||||
ws.Route(ws.POST("/validate").To(h.ValiateToken).
|
||||
Doc("校验令牌").
|
||||
// Metadata(permission.Auth(true)).
|
||||
// Metadata(permission.Permission(false)).
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Reads(token.ValiateTokenRequest{}).
|
||||
Writes(token.Token{}).
|
||||
Returns(200, "OK", token.Token{}))
|
||||
|
||||
// ws.Route(ws.POST("/change_namespace").To(h.ChangeNamespce).
|
||||
// Doc("切换令牌访问空间").
|
||||
// // Metadata(permission.Auth(true)).
|
||||
// // Metadata(permission.Permission(false)).
|
||||
// Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
// Reads(token.ChangeNamespceRequest{}).
|
||||
// Writes(token.Token{}).
|
||||
// Returns(200, "OK", token.Token{}))
|
||||
|
||||
ws.Route(ws.DELETE("").To(h.Logout).
|
||||
Doc("撤销令牌(退出)").
|
||||
// Metadata(permission.Auth(true)).
|
||||
// Metadata(permission.Permission(false)).
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Reads(token.IssueTokenRequest{}).
|
||||
Writes(token.Token{}).
|
||||
Returns(200, "OK", token.Token{}).
|
||||
Returns(404, "Not Found", nil))
|
||||
return nil
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
登录接口
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "123456"
|
||||
}
|
||||
```
|
@ -1,127 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/token"
|
||||
"github.com/emicklei/go-restful/v3"
|
||||
"github.com/infraboard/mcube/v2/http/restful/response"
|
||||
"github.com/infraboard/mcube/v2/ioc/config/application"
|
||||
)
|
||||
|
||||
func (h *TokenRestfulApiHandler) Login(r *restful.Request, w *restful.Response) {
|
||||
// 1. 获取用户的请求参数, 参数在Body里面
|
||||
req := token.NewIssueTokenRequest()
|
||||
|
||||
// 获取用户通过body传入的参数
|
||||
err := r.ReadEntity(req)
|
||||
if err != nil {
|
||||
response.Failed(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置当前调用者的Token
|
||||
// Private 用户自己的Token
|
||||
// 如果你是user/password 这种方式,token 直接放到body
|
||||
switch req.Issuer {
|
||||
case token.ISSUER_PRIVATE_TOKEN:
|
||||
req.Parameter.SetAccessToken(token.GetAccessTokenFromHTTP(r.Request))
|
||||
}
|
||||
|
||||
// 2. 执行逻辑
|
||||
tk, err := h.svc.IssueToken(r.Request.Context(), req)
|
||||
if err != nil {
|
||||
response.Failed(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// access_token 通过SetCookie 直接写到浏览器客户端(Web)
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: token.ACCESS_TOKEN_COOKIE_NAME,
|
||||
Value: url.QueryEscape(tk.AccessToken),
|
||||
MaxAge: 0,
|
||||
Path: "/",
|
||||
Domain: application.Get().Domain(),
|
||||
SameSite: http.SameSiteDefaultMode,
|
||||
Secure: false,
|
||||
HttpOnly: true,
|
||||
})
|
||||
// 在Header头中也添加Token
|
||||
w.Header().Set(token.ACCESS_TOKEN_RESPONSE_HEADER_NAME, tk.AccessToken)
|
||||
|
||||
// 3. Body中返回Token对象
|
||||
response.Success(w, tk)
|
||||
}
|
||||
|
||||
// func (h *TokenRestulApiHandler) ChangeNamespce(r *restful.Request, w *restful.Response) {
|
||||
// // 1. 获取用户的请求参数, 参数在Body里面
|
||||
// req := token.NewChangeNamespceRequest()
|
||||
// err := r.ReadEntity(req)
|
||||
// if err != nil {
|
||||
// response.Failed(w, err)
|
||||
// return
|
||||
// }
|
||||
|
||||
// tk := token.GetTokenFromCtx(r.Request.Context())
|
||||
// req.UserId = tk.UserId
|
||||
|
||||
// // 2. 执行逻辑
|
||||
// tk, err = h.svc.ChangeNamespce(r.Request.Context(), req)
|
||||
// if err != nil {
|
||||
// response.Failed(w, err)
|
||||
// return
|
||||
// }
|
||||
|
||||
// // 3. Body中返回Token对象
|
||||
// response.Success(w, tk)
|
||||
// }
|
||||
|
||||
// Logout HandleFunc
|
||||
func (h *TokenRestfulApiHandler) Logout(r *restful.Request, w *restful.Response) {
|
||||
req := token.NewRevolkTokenRequest(
|
||||
token.GetAccessTokenFromHTTP(r.Request),
|
||||
token.GetRefreshTokenFromHTTP(r.Request),
|
||||
)
|
||||
|
||||
tk, err := h.svc.RevolkToken(r.Request.Context(), req)
|
||||
if err != nil {
|
||||
response.Failed(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// access_token 通过SetCookie 直接写到浏览器客户端(Web)
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: token.ACCESS_TOKEN_COOKIE_NAME,
|
||||
Value: "",
|
||||
MaxAge: 0,
|
||||
Path: "/",
|
||||
Domain: application.Get().Domain(),
|
||||
SameSite: http.SameSiteDefaultMode,
|
||||
Secure: false,
|
||||
HttpOnly: true,
|
||||
})
|
||||
|
||||
// 3. 返回响应
|
||||
response.Success(w, tk)
|
||||
}
|
||||
|
||||
func (h *TokenRestfulApiHandler) ValiateToken(r *restful.Request, w *restful.Response) {
|
||||
// 1. 获取用户的请求参数, 参数在Body里面
|
||||
req := token.NewValiateTokenRequest("")
|
||||
err := r.ReadEntity(req)
|
||||
if err != nil {
|
||||
response.Failed(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 执行逻辑
|
||||
tk, err := h.svc.ValiateToken(r.Request.Context(), req)
|
||||
if err != nil {
|
||||
response.Failed(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Body中返回Token对象
|
||||
response.Success(w, tk)
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package token
|
||||
|
||||
import "github.com/infraboard/mcube/v2/exception"
|
||||
|
||||
const (
|
||||
ACCESS_TOKEN_HEADER_NAME = "Authorization"
|
||||
ACCESS_TOKEN_COOKIE_NAME = "access_token"
|
||||
ACCESS_TOKEN_RESPONSE_HEADER_NAME = "X-OAUTH-TOKEN"
|
||||
REFRESH_TOKEN_HEADER_NAME = "X-REFRUSH-TOKEN"
|
||||
)
|
||||
|
||||
// 自定义非导出类型,避免外部包直接实例化
|
||||
type tokenContextKey struct{}
|
||||
|
||||
var (
|
||||
CTX_TOKEN_KEY = tokenContextKey{}
|
||||
)
|
||||
|
||||
var (
|
||||
CookieNotFound = exception.NewUnauthorized("cookie %s not found", ACCESS_TOKEN_COOKIE_NAME)
|
||||
)
|
@ -1,36 +0,0 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram id="rJ2wD46cwpVMIQue_TYe" name="第 1 页">
|
||||
<mxGraphModel dx="934" dy="434" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="2" value="user" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="250" width="220" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="9" style="edgeStyle=none;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="3" target="2">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="10" value="token" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="9">
|
||||
<mxGeometry x="-0.0681" y="-1" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="3" value="token" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="380" y="250" width="200" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="4" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="460" y="10" width="350" height="140" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="570" y="40" width="120" height="20" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="6" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="570" y="80" width="120" height="20" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="8" value="记住登录" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="720" y="100" width="50" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
@ -1,37 +0,0 @@
|
||||
package token
|
||||
|
||||
type SOURCE int
|
||||
|
||||
const (
|
||||
// 未知
|
||||
SOURCE_UNKNOWN SOURCE = iota
|
||||
// Web
|
||||
SOURCE_WEB
|
||||
// IOS
|
||||
SOURCE_IOS
|
||||
// ANDROID
|
||||
SOURCE_ANDROID
|
||||
// PC
|
||||
SOURCE_PC
|
||||
// API 调用
|
||||
SOURCE_API SOURCE = 10
|
||||
)
|
||||
|
||||
type LOCK_TYPE int
|
||||
|
||||
const (
|
||||
// 用户退出登录
|
||||
LOCK_TYPE_REVOLK LOCK_TYPE = iota
|
||||
// 刷新Token过期, 回话中断
|
||||
LOCK_TYPE_TOKEN_EXPIRED
|
||||
// 异地登陆
|
||||
LOCK_TYPE_OTHER_PLACE_LOGGED_IN
|
||||
// 异常Ip登陆
|
||||
LOCK_TYPE_OTHER_IP_LOGGED_IN
|
||||
)
|
||||
|
||||
type DESCRIBE_BY int
|
||||
|
||||
const (
|
||||
DESCRIBE_BY_ACCESS_TOKEN DESCRIBE_BY = iota
|
||||
)
|
@ -1,56 +0,0 @@
|
||||
package impl
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/token"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/user"
|
||||
"github.com/infraboard/mcube/v2/ioc"
|
||||
"github.com/infraboard/mcube/v2/ioc/config/datasource"
|
||||
"github.com/infraboard/mcube/v2/ioc/config/log"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ioc.Controller().Registry(&TokenServiceImpl{
|
||||
AutoRefresh: true,
|
||||
RereshTTLSecond: 1 * 60 * 60,
|
||||
})
|
||||
}
|
||||
|
||||
var _ token.Service = (*TokenServiceImpl)(nil)
|
||||
|
||||
type TokenServiceImpl struct {
|
||||
ioc.ObjectImpl
|
||||
user user.Service
|
||||
log *zerolog.Logger
|
||||
// policy policy.PermissionService
|
||||
|
||||
// 自动刷新, 直接刷新Token的过期时间,而不是生成一个新Token
|
||||
AutoRefresh bool `json:"auto_refresh" toml:"auto_refresh" yaml:"auto_refresh" env:"AUTO_REFRESH"`
|
||||
// 刷新TTL
|
||||
RereshTTLSecond uint64 `json:"refresh_ttl" toml:"refresh_ttl" yaml:"refresh_ttl" env:"REFRESH_TTL"`
|
||||
// Api最多多少个, 这种Token往往过期时间比较长, 为了安全不要申请太多
|
||||
MaxActiveApiToken uint8 `json:"max_active_api_token" toml:"max_active_api_token" yaml:"max_active_api_token" env:"MAX_ACTIVE_API_TOKEN"`
|
||||
|
||||
refreshDuration time.Duration
|
||||
}
|
||||
|
||||
func (i *TokenServiceImpl) Init() error {
|
||||
i.log = log.Sub(i.Name())
|
||||
i.user = user.GetService()
|
||||
// i.policy = policy.GetService()
|
||||
i.refreshDuration = time.Duration(i.RereshTTLSecond) * time.Second
|
||||
|
||||
if datasource.Get().AutoMigrate {
|
||||
err := datasource.DB().AutoMigrate(&token.Token{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *TokenServiceImpl) Name() string {
|
||||
return token.APP_NAME
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
package impl_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/token"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/test"
|
||||
)
|
||||
|
||||
var (
|
||||
svc token.Service
|
||||
ctx = context.Background()
|
||||
)
|
||||
|
||||
func init() {
|
||||
test.DevelopmentSetUp()
|
||||
svc = token.GetService()
|
||||
}
|
@ -1,205 +0,0 @@
|
||||
package impl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/token"
|
||||
"github.com/infraboard/mcube/v2/desense"
|
||||
"github.com/infraboard/mcube/v2/exception"
|
||||
"github.com/infraboard/mcube/v2/ioc/config/datasource"
|
||||
"github.com/infraboard/mcube/v2/types"
|
||||
)
|
||||
|
||||
// 登录接口(颁发Token)
|
||||
func (i *TokenServiceImpl) IssueToken(ctx context.Context, in *token.IssueTokenRequest) (*token.Token, error) {
|
||||
// 颁发Token
|
||||
// user/password
|
||||
// ldap
|
||||
// 飞书,企业微信 ...
|
||||
issuer := token.GetIssuer(in.Issuer)
|
||||
if issuer == nil {
|
||||
return nil, exception.NewBadRequest("issuer %s no support", in.Issuer)
|
||||
}
|
||||
tk, err := issuer.IssueToken(ctx, in.Parameter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tk.SetIssuer(in.Issuer).SetSource(in.Source)
|
||||
|
||||
// 判断当前数据库有没有已经存在的Token
|
||||
activeTokenQueryReq := token.NewQueryTokenRequest().
|
||||
AddUserId(tk.UserId).
|
||||
SetSource(in.Source).
|
||||
SetActive(true)
|
||||
tks, err := i.QueryToken(ctx, activeTokenQueryReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch in.Source {
|
||||
// 每个端只能有1个活跃登录
|
||||
case token.SOURCE_WEB, token.SOURCE_IOS, token.SOURCE_ANDROID, token.SOURCE_PC:
|
||||
if tks.Len() > 0 {
|
||||
i.log.Debug().Msgf("use exist active token: %s", desense.Default().DeSense(tk.AccessToken, "4", "3"))
|
||||
return tks.Items[0], nil
|
||||
}
|
||||
case token.SOURCE_API:
|
||||
if tks.Len() > int(i.MaxActiveApiToken) {
|
||||
return nil, exception.NewBadRequest("max active api token overflow")
|
||||
}
|
||||
}
|
||||
|
||||
if tk.NamespaceId == 0 {
|
||||
tk.NamespaceId = 1
|
||||
}
|
||||
|
||||
// 保持Token
|
||||
if err := datasource.DBFromCtx(ctx).
|
||||
Create(tk).
|
||||
Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tk, nil
|
||||
}
|
||||
|
||||
// 校验Token 是给内部中间层使用 身份校验层
|
||||
func (i *TokenServiceImpl) ValiateToken(ctx context.Context, req *token.ValiateTokenRequest) (*token.Token, error) {
|
||||
// 1. 查询Token (是不是我们这个系统颁发的)
|
||||
tk := token.NewToken()
|
||||
err := datasource.DBFromCtx(ctx).
|
||||
Where("access_token = ?", req.AccessToken).
|
||||
First(tk).
|
||||
Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2.1 判断Ak是否过期
|
||||
if err := tk.IsAccessTokenExpired(); err != nil {
|
||||
// 判断刷新Token是否过期
|
||||
if err := tk.IsRreshTokenExpired(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 如果开启了自动刷新
|
||||
if i.AutoRefresh {
|
||||
tk.SetRefreshAt(time.Now())
|
||||
tk.SetExpiredAtByDuration(i.refreshDuration, 4)
|
||||
if err := datasource.DBFromCtx(ctx).Save(tk); err != nil {
|
||||
i.log.Error().Msgf("auto refresh token error, %s", err.Error)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tk, nil
|
||||
}
|
||||
|
||||
func (i *TokenServiceImpl) DescribeToken(ctx context.Context, in *token.DescribeTokenRequest) (*token.Token, error) {
|
||||
query := datasource.DBFromCtx(ctx)
|
||||
switch in.DescribeBy {
|
||||
case token.DESCRIBE_BY_ACCESS_TOKEN:
|
||||
query = query.Where("access_token = ?", in.DescribeValue)
|
||||
default:
|
||||
return nil, exception.NewBadRequest("unspport describe type %s", in.DescribeValue)
|
||||
}
|
||||
|
||||
tk := token.NewToken()
|
||||
if err := query.First(tk).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tk, nil
|
||||
}
|
||||
|
||||
// 退出接口(销毁Token)
|
||||
func (i *TokenServiceImpl) RevolkToken(ctx context.Context, in *token.RevolkTokenRequest) (*token.Token, error) {
|
||||
tk, err := i.DescribeToken(ctx, token.NewDescribeTokenRequest(in.AccessToken))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tk.CheckRefreshToken(in.RefreshToken); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tk.Lock(token.LOCK_TYPE_REVOLK, "user revolk token")
|
||||
err = datasource.DBFromCtx(ctx).Model(&token.Token{}).
|
||||
Where("access_token = ?", in.AccessToken).
|
||||
Where("refresh_token = ?", in.RefreshToken).
|
||||
Updates(tk.Status.ToMap()).
|
||||
Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tk, err
|
||||
}
|
||||
|
||||
// 查询已经颁发出去的Token
|
||||
func (i *TokenServiceImpl) QueryToken(ctx context.Context, in *token.QueryTokenRequest) (*types.Set[*token.Token], error) {
|
||||
set := types.New[*token.Token]()
|
||||
query := datasource.DBFromCtx(ctx).Model(&token.Token{})
|
||||
|
||||
if in.Active != nil {
|
||||
if *in.Active {
|
||||
query = query.
|
||||
Where("lock_at IS NULL AND refresh_token_expired_at > ?", time.Now())
|
||||
} else {
|
||||
query = query.
|
||||
Where("lock_at IS NOT NULL OR refresh_token_expired_at <= ?", time.Now())
|
||||
}
|
||||
}
|
||||
if in.Source != nil {
|
||||
query = query.Where("source = ?", *in.Source)
|
||||
}
|
||||
if len(in.UserIds) > 0 {
|
||||
query = query.Where("user_id IN ?", in.UserIds)
|
||||
}
|
||||
|
||||
// 查询总量
|
||||
err := query.Count(&set.Total).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = query.
|
||||
Order("issue_at desc").
|
||||
Offset(int(in.ComputeOffset())).
|
||||
Limit(int(in.PageSize)).
|
||||
Find(&set.Items).
|
||||
Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return set, nil
|
||||
}
|
||||
|
||||
// 用户切换空间
|
||||
// func (i *TokenServiceImpl) ChangeNamespce(ctx context.Context, in *token.ChangeNamespceRequest) (*token.Token, error) {
|
||||
// set, err := i.policy.QueryNamespace(ctx, policy.NewQueryNamespaceRequest().SetUserId(in.UserId).SetNamespaceId(in.NamespaceId))
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
// ns := set.First()
|
||||
// if ns == nil {
|
||||
// return nil, exception.NewPermissionDeny("你没有该空间访问权限")
|
||||
// }
|
||||
|
||||
// // 更新Token
|
||||
// tk, err := i.DescribeToken(ctx, token.NewDescribeTokenRequest(in.AccessToken))
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// tk.NamespaceId = ns.Id
|
||||
// tk.NamespaceName = ns.Name
|
||||
|
||||
// // 保存状态
|
||||
// if err := datasource.DBFromCtx(ctx).
|
||||
// Updates(tk).
|
||||
// Error; err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// return tk, nil
|
||||
// }
|
@ -1,27 +0,0 @@
|
||||
package impl_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/token"
|
||||
)
|
||||
|
||||
func TestIssueToken(t *testing.T) {
|
||||
req := token.NewIssueTokenRequest()
|
||||
req.IssueByPassword("admin", "123456")
|
||||
req.Source = token.SOURCE_WEB
|
||||
set, err := svc.IssueToken(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(set)
|
||||
}
|
||||
|
||||
func TestQueryToken(t *testing.T) {
|
||||
req := token.NewQueryTokenRequest()
|
||||
set, err := svc.QueryToken(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(set)
|
||||
}
|
@ -1,171 +0,0 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/infraboard/mcube/v2/http/request"
|
||||
"github.com/infraboard/mcube/v2/ioc"
|
||||
"github.com/infraboard/mcube/v2/types"
|
||||
)
|
||||
|
||||
const (
|
||||
APP_NAME = "token"
|
||||
)
|
||||
|
||||
func GetService() Service {
|
||||
return ioc.Controller().Get(APP_NAME).(Service)
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
// 颁发访问令牌: Login
|
||||
IssueToken(context.Context, *IssueTokenRequest) (*Token, error)
|
||||
// 撤销访问令牌: 令牌失效了 Logout
|
||||
RevolkToken(context.Context, *RevolkTokenRequest) (*Token, error)
|
||||
// 查询已经颁发出去的Token
|
||||
QueryToken(context.Context, *QueryTokenRequest) (*types.Set[*Token], error)
|
||||
|
||||
// 查询Token详情
|
||||
DescribeToken(context.Context, *DescribeTokenRequest) (*Token, error)
|
||||
// 校验访问令牌:检查令牌的合法性, 是不是伪造的
|
||||
ValiateToken(context.Context, *ValiateTokenRequest) (*Token, error)
|
||||
}
|
||||
|
||||
func NewDescribeTokenRequest(accessToken string) *DescribeTokenRequest {
|
||||
return &DescribeTokenRequest{
|
||||
DescribeBy: DESCRIBE_BY_ACCESS_TOKEN,
|
||||
DescribeValue: accessToken,
|
||||
}
|
||||
}
|
||||
|
||||
type DescribeTokenRequest struct {
|
||||
DescribeBy DESCRIBE_BY `json:"describe_by"`
|
||||
DescribeValue string `json:"describe_value"`
|
||||
}
|
||||
|
||||
func NewQueryTokenRequest() *QueryTokenRequest {
|
||||
return &QueryTokenRequest{
|
||||
PageRequest: request.NewDefaultPageRequest(),
|
||||
UserIds: []uint64{},
|
||||
}
|
||||
}
|
||||
|
||||
type QueryTokenRequest struct {
|
||||
*request.PageRequest
|
||||
// 当前可用的没过期的Token
|
||||
Active *bool `json:"active"`
|
||||
// 用户来源
|
||||
Source *SOURCE `json:"source"`
|
||||
// Uids
|
||||
UserIds []uint64 `json:"user_ids"`
|
||||
}
|
||||
|
||||
func (r *QueryTokenRequest) SetActive(v bool) *QueryTokenRequest {
|
||||
r.Active = &v
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *QueryTokenRequest) SetSource(v SOURCE) *QueryTokenRequest {
|
||||
r.Source = &v
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *QueryTokenRequest) AddUserId(uids ...uint64) *QueryTokenRequest {
|
||||
r.UserIds = append(r.UserIds, uids...)
|
||||
return r
|
||||
}
|
||||
|
||||
func NewIssueTokenRequest() *IssueTokenRequest {
|
||||
return &IssueTokenRequest{
|
||||
Parameter: make(IssueParameter),
|
||||
}
|
||||
}
|
||||
|
||||
// 用户会给我们 用户的身份凭证,用于换取Token
|
||||
type IssueTokenRequest struct {
|
||||
// 端类型
|
||||
Source SOURCE `json:"source"`
|
||||
// 认证方式
|
||||
Issuer string `json:"issuer"`
|
||||
// 参数
|
||||
Parameter IssueParameter `json:"parameter"`
|
||||
}
|
||||
|
||||
func (i *IssueTokenRequest) IssueByPassword(username, password string) {
|
||||
i.Issuer = ISSUER_PASSWORD
|
||||
i.Parameter.SetUsername(username)
|
||||
i.Parameter.SetPassword(password)
|
||||
}
|
||||
|
||||
func NewIssueParameter() IssueParameter {
|
||||
return make(IssueParameter)
|
||||
}
|
||||
|
||||
type IssueParameter map[string]any
|
||||
|
||||
/*
|
||||
password issuer parameter
|
||||
*/
|
||||
|
||||
func (p IssueParameter) Username() string {
|
||||
return GetIssueParameterValue[string](p, "username")
|
||||
}
|
||||
|
||||
func (p IssueParameter) Password() string {
|
||||
return GetIssueParameterValue[string](p, "password")
|
||||
}
|
||||
|
||||
func (p IssueParameter) SetUsername(v string) IssueParameter {
|
||||
p["username"] = v
|
||||
return p
|
||||
}
|
||||
|
||||
func (p IssueParameter) SetPassword(v string) IssueParameter {
|
||||
p["password"] = v
|
||||
return p
|
||||
}
|
||||
|
||||
/*
|
||||
private token issuer parameter
|
||||
*/
|
||||
|
||||
func (p IssueParameter) AccessToken() string {
|
||||
return GetIssueParameterValue[string](p, "access_token")
|
||||
}
|
||||
|
||||
func (p IssueParameter) ExpireTTL() time.Duration {
|
||||
return time.Second * time.Duration(GetIssueParameterValue[int64](p, "expired_ttl"))
|
||||
}
|
||||
|
||||
func (p IssueParameter) SetAccessToken(v string) IssueParameter {
|
||||
p["access_token"] = v
|
||||
return p
|
||||
}
|
||||
|
||||
func (p IssueParameter) SetExpireTTL(v int64) IssueParameter {
|
||||
p["expired_ttl"] = v
|
||||
return p
|
||||
}
|
||||
|
||||
func NewRevolkTokenRequest(at, rk string) *RevolkTokenRequest {
|
||||
return &RevolkTokenRequest{
|
||||
AccessToken: at,
|
||||
RefreshToken: rk,
|
||||
}
|
||||
}
|
||||
|
||||
// 万一的Token泄露, 不知道refresh_token,也没法推出
|
||||
type RevolkTokenRequest struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
func NewValiateTokenRequest(accessToken string) *ValiateTokenRequest {
|
||||
return &ValiateTokenRequest{
|
||||
AccessToken: accessToken,
|
||||
}
|
||||
}
|
||||
|
||||
type ValiateTokenRequest struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
ISSUER_LDAP = "ldap"
|
||||
ISSUER_FEISHU = "feishu"
|
||||
ISSUER_PASSWORD = "password"
|
||||
ISSUER_PRIVATE_TOKEN = "private_token"
|
||||
)
|
||||
|
||||
var issuers = map[string]Issuer{}
|
||||
|
||||
func RegistryIssuer(name string, p Issuer) {
|
||||
issuers[name] = p
|
||||
}
|
||||
|
||||
func GetIssuer(name string) Issuer {
|
||||
fmt.Println(issuers)
|
||||
return issuers[name]
|
||||
}
|
||||
|
||||
type Issuer interface {
|
||||
IssueToken(context.Context, IssueParameter) (*Token, error)
|
||||
}
|
||||
|
||||
var (
|
||||
charlist = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
)
|
||||
|
||||
// MakeBearer https://tools.ietf.org/html/rfc6750#section-2.1
|
||||
// b64token = 1*( ALPHA / DIGIT /"-" / "." / "_" / "~" / "+" / "/" ) *"="
|
||||
func MakeBearer(lenth int) string {
|
||||
t := make([]byte, 0)
|
||||
for range lenth {
|
||||
rn := rand.IntN(len(charlist))
|
||||
t = append(t, charlist[rn])
|
||||
}
|
||||
return string(t)
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package token_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/token"
|
||||
)
|
||||
|
||||
func TestMakeBearer(t *testing.T) {
|
||||
t.Log(token.MakeBearer(24))
|
||||
t.Log(token.MakeBearer(24))
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
package password
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/token"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/user"
|
||||
"github.com/infraboard/mcube/v2/exception"
|
||||
"github.com/infraboard/mcube/v2/ioc"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ioc.Config().Registry(&PasswordTokenIssuer{
|
||||
ExpiredTTLSecond: 1 * 60 * 60,
|
||||
})
|
||||
}
|
||||
|
||||
type PasswordTokenIssuer struct {
|
||||
ioc.ObjectImpl
|
||||
// 通过用户模块 来判断用户凭证是否正确
|
||||
user user.Service
|
||||
|
||||
// Password颁发的Token 过去时间由系统配置, 不允许用户自己设置
|
||||
ExpiredTTLSecond int `json:"expired_ttl_second" toml:"expired_ttl_second" yaml:"expired_ttl_second" env:"EXPIRED_TTL_SECOND"`
|
||||
|
||||
expiredDuration time.Duration
|
||||
}
|
||||
|
||||
func (p *PasswordTokenIssuer) Name() string {
|
||||
return "password_token_issuer"
|
||||
}
|
||||
|
||||
func (p *PasswordTokenIssuer) Init() error {
|
||||
p.user = user.GetService()
|
||||
p.expiredDuration = time.Duration(p.ExpiredTTLSecond) * time.Second
|
||||
|
||||
token.RegistryIssuer(token.ISSUER_PASSWORD, p)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PasswordTokenIssuer) IssueToken(ctx context.Context, parameter token.IssueParameter) (*token.Token, error) {
|
||||
// 1. 查询用户
|
||||
uReq := user.NewDescribeUserRequestByUserName(parameter.Username())
|
||||
u, err := p.user.DescribeUser(ctx, uReq)
|
||||
if err != nil {
|
||||
if exception.IsNotFoundError(err) {
|
||||
return nil, exception.NewUnauthorized("%s", err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 比对密码
|
||||
err = u.CheckPassword(parameter.Password())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 颁发token
|
||||
tk := token.NewToken()
|
||||
tk.UserId = u.Id
|
||||
tk.UserName = u.UserName
|
||||
tk.IsAdmin = u.IsAdmin
|
||||
|
||||
tk.SetExpiredAtByDuration(p.expiredDuration, 4)
|
||||
return tk, nil
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package password_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/token"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/test"
|
||||
)
|
||||
|
||||
func TestPasswordIssuer(t *testing.T) {
|
||||
issuer := token.GetIssuer(token.ISSUER_PASSWORD)
|
||||
tk, err := issuer.IssueToken(context.Background(), token.NewIssueParameter().SetUsername("admin").SetPassword("123456"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(tk)
|
||||
}
|
||||
|
||||
func init() {
|
||||
test.DevelopmentSetUp()
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package privatetoken_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/token"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/test"
|
||||
)
|
||||
|
||||
func TestPasswordIssuer(t *testing.T) {
|
||||
issuer := token.GetIssuer(token.ISSUER_PRIVATE_TOKEN)
|
||||
tk, err := issuer.IssueToken(context.Background(), token.NewIssueParameter().SetAccessToken("LccvuTwISJRheu8PtqAFTJBy").SetExpireTTL(24*60*60))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(tk)
|
||||
}
|
||||
|
||||
func init() {
|
||||
test.DevelopmentSetUp()
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
package privatetoken
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/token"
|
||||
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/user"
|
||||
"github.com/infraboard/mcube/v2/exception"
|
||||
"github.com/infraboard/mcube/v2/ioc"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ioc.Config().Registry(&PrivateTokenIssuer{})
|
||||
}
|
||||
|
||||
type PrivateTokenIssuer struct {
|
||||
ioc.ObjectImpl
|
||||
|
||||
user user.Service
|
||||
token token.Service
|
||||
}
|
||||
|
||||
func (p *PrivateTokenIssuer) Name() string {
|
||||
return "private_token_issuer"
|
||||
}
|
||||
|
||||
func (p *PrivateTokenIssuer) Init() error {
|
||||
p.user = user.GetService()
|
||||
p.token = token.GetService()
|
||||
|
||||
token.RegistryIssuer(token.ISSUER_PRIVATE_TOKEN, p)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PrivateTokenIssuer) IssueToken(ctx context.Context, parameter token.IssueParameter) (*token.Token, error) {
|
||||
// 1. 校验Token合法
|
||||
oldTk, err := p.token.ValiateToken(ctx, token.NewValiateTokenRequest(parameter.AccessToken()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 查询用户
|
||||
uReq := user.NewDescribeUserRequestById(oldTk.UserIdString())
|
||||
u, err := p.user.DescribeUser(ctx, uReq)
|
||||
if err != nil {
|
||||
if exception.IsNotFoundError(err) {
|
||||
return nil, exception.NewUnauthorized("%s", err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !u.EnabledApi {
|
||||
return nil, exception.NewPermissionDeny("未开启接口登录")
|
||||
}
|
||||
|
||||
// 3. 颁发token
|
||||
tk := token.NewToken()
|
||||
tk.UserId = u.Id
|
||||
tk.UserName = u.UserName
|
||||
tk.IsAdmin = u.IsAdmin
|
||||
|
||||
expiredTTL := parameter.ExpireTTL()
|
||||
if expiredTTL > 0 {
|
||||
tk.SetExpiredAtByDuration(expiredTTL, 4)
|
||||
}
|
||||
return tk, nil
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
package issuers
|
||||
|
||||
import (
|
||||
_ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token/issuers/password"
|
||||
_ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token/issuers/private_token"
|
||||
)
|
@ -1,218 +0,0 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/infraboard/mcube/v2/exception"
|
||||
"github.com/infraboard/mcube/v2/tools/pretty"
|
||||
)
|
||||
|
||||
func GetAccessTokenFromHTTP(r *http.Request) string {
|
||||
// 先从Token中获取
|
||||
tk := r.Header.Get(ACCESS_TOKEN_HEADER_NAME)
|
||||
|
||||
// 1. 获取Token
|
||||
if tk == "" {
|
||||
cookie, err := r.Cookie(ACCESS_TOKEN_COOKIE_NAME)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
// ?token=xxxx
|
||||
tk, _ = url.QueryUnescape(cookie.Value)
|
||||
} else {
|
||||
// 处理 带格式: Bearer <Your API key>
|
||||
ft := strings.Split(tk, " ")
|
||||
if len(ft) > 1 {
|
||||
tk = ft[1]
|
||||
}
|
||||
}
|
||||
return tk
|
||||
}
|
||||
|
||||
// 从上下文中 提取 用户身份信息
|
||||
func GetTokenFromCtx(ctx context.Context) *Token {
|
||||
if v := ctx.Value(CTX_TOKEN_KEY); v != nil {
|
||||
return v.(*Token)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetRefreshTokenFromHTTP(r *http.Request) string {
|
||||
// 先从Token中获取
|
||||
tk := r.Header.Get(REFRESH_TOKEN_HEADER_NAME)
|
||||
return tk
|
||||
}
|
||||
|
||||
func NewToken() *Token {
|
||||
tk := &Token{
|
||||
// 生产一个UUID的字符串
|
||||
AccessToken: MakeBearer(24),
|
||||
RefreshToken: MakeBearer(32),
|
||||
IssueAt: time.Now(),
|
||||
Status: NewStatus(),
|
||||
Extras: map[string]string{},
|
||||
Scope: map[string]string{},
|
||||
}
|
||||
|
||||
return tk
|
||||
}
|
||||
|
||||
// 需要存储到数据库里面的对象(表)
|
||||
|
||||
type Token struct {
|
||||
// 在添加数据需要, 主键
|
||||
Id uint64 `json:"id" gorm:"column:id;type:uint;primary_key;"`
|
||||
// 用户来源
|
||||
Source SOURCE `json:"source" gorm:"column:source;type:tinyint(1);index" description:"用户来源"`
|
||||
// 颁发器, 办法方式(user/pass )
|
||||
Issuer string `json:"issuer" gorm:"column:issuer;type:varchar(100);index" description:"颁发器"`
|
||||
// 该Token属于哪个用户
|
||||
UserId uint64 `json:"user_id" gorm:"column:user_id;index" description:"持有该Token的用户Id"`
|
||||
// 用户名
|
||||
UserName string `json:"user_name" gorm:"column:user_name;type:varchar(100);not null;index" description:"持有该Token的用户名称"`
|
||||
// 是不是管理员
|
||||
IsAdmin bool `json:"is_admin" gorm:"column:is_admin;type:tinyint(1)" description:"是不是管理员"`
|
||||
// 令牌生效空间Id
|
||||
NamespaceId uint64 `json:"namespace_id" gorm:"column:namespace_id;type:uint;index" description:"令牌所属空间Id"`
|
||||
// 令牌生效空间名称
|
||||
NamespaceName string `json:"namespace_name" gorm:"column:namespace_name;type:varchar(100);index" description:"令牌所属空间"`
|
||||
// 访问范围定义, 鉴权完成后补充
|
||||
Scope map[string]string `json:"scope" gorm:"column:scope;type:varchar(100);serializer:json" description:"令牌访问范围定义"`
|
||||
// 颁发给用户的访问令牌(用户需要携带Token来访问接口)
|
||||
AccessToken string `json:"access_token" gorm:"column:access_token;type:varchar(100);not null;uniqueIndex" description:"访问令牌"`
|
||||
// 访问令牌过期时间
|
||||
AccessTokenExpiredAt *time.Time `json:"access_token_expired_at" gorm:"column:access_token_expired_at;type:timestamp;index" description:"访问令牌的过期时间"`
|
||||
// 刷新Token
|
||||
RefreshToken string `json:"refresh_token" gorm:"column:refresh_token;type:varchar(100);not null;uniqueIndex" description:"刷新令牌"`
|
||||
// 刷新Token过期时间
|
||||
RefreshTokenExpiredAt *time.Time `json:"refresh_token_expired_at" gorm:"column:refresh_token_expired_at;type:timestamp;index" description:"刷新令牌的过期时间"`
|
||||
// 创建时间
|
||||
IssueAt time.Time `json:"issue_at" gorm:"column:issue_at;type:timestamp;default:current_timestamp;not null;index" description:"令牌颁发时间"`
|
||||
// 更新时间
|
||||
RefreshAt *time.Time `json:"refresh_at" gorm:"column:refresh_at;type:timestamp" description:"令牌刷新时间"`
|
||||
// 令牌状态
|
||||
Status *Status `json:"status" gorm:"embedded" modelDescription:"令牌状态"`
|
||||
// 其他扩展信息
|
||||
Extras map[string]string `json:"extras" gorm:"column:extras;serializer:json;type:json" description:"其他扩展信息"`
|
||||
}
|
||||
|
||||
func (t *Token) TableName() string {
|
||||
return "tokens"
|
||||
}
|
||||
|
||||
// 判断访问令牌是否过期,没设置代表用不过期
|
||||
func (t *Token) IsAccessTokenExpired() error {
|
||||
if t.AccessTokenExpiredAt != nil {
|
||||
// now expiredTime
|
||||
expiredSeconds := time.Since(*t.AccessTokenExpiredAt).Seconds()
|
||||
if expiredSeconds > 0 {
|
||||
return exception.NewAccessTokenExpired("access token %s 过期了 %f秒",
|
||||
t.AccessToken, expiredSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 判断刷新Token是否过期
|
||||
func (t *Token) IsRreshTokenExpired() error {
|
||||
if t.RefreshTokenExpiredAt != nil {
|
||||
expiredSeconds := time.Since(*t.RefreshTokenExpiredAt).Seconds()
|
||||
if expiredSeconds > 0 {
|
||||
return exception.NewRefreshTokenExpired("refresh token %s 过期了 %f秒",
|
||||
t.RefreshToken, expiredSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 刷新Token的过期时间 是一个系统配置, 刷新token的过期时间 > 访问token的时间
|
||||
// 给一些默认设置: 刷新token的过期时间 = 访问token的时间 * 4
|
||||
func (t *Token) SetExpiredAtByDuration(duration time.Duration, refreshMulti uint) {
|
||||
t.SetAccessTokenExpiredAt(time.Now().Add(duration))
|
||||
t.SetRefreshTokenExpiredAt(time.Now().Add(duration * time.Duration(refreshMulti)))
|
||||
}
|
||||
|
||||
func (t *Token) SetAccessTokenExpiredAt(v time.Time) {
|
||||
t.AccessTokenExpiredAt = &v
|
||||
}
|
||||
|
||||
func (t *Token) SetRefreshAt(v time.Time) {
|
||||
t.RefreshAt = &v
|
||||
}
|
||||
|
||||
func (t *Token) AccessTokenExpiredTTL() int {
|
||||
if t.AccessTokenExpiredAt != nil {
|
||||
return int(t.AccessTokenExpiredAt.Sub(t.IssueAt).Seconds())
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (t *Token) SetRefreshTokenExpiredAt(v time.Time) {
|
||||
t.RefreshTokenExpiredAt = &v
|
||||
}
|
||||
|
||||
func (t *Token) String() string {
|
||||
return pretty.ToJSON(t)
|
||||
}
|
||||
|
||||
func (t *Token) SetIssuer(issuer string) *Token {
|
||||
t.Issuer = issuer
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *Token) SetSource(source SOURCE) *Token {
|
||||
t.Source = source
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *Token) UserIdString() string {
|
||||
return fmt.Sprintf("%d", t.UserId)
|
||||
}
|
||||
|
||||
func (t *Token) CheckRefreshToken(refreshToken string) error {
|
||||
if t.RefreshToken != refreshToken {
|
||||
return exception.NewPermissionDeny("refresh token not conrect")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Token) Lock(l LOCK_TYPE, reason string) {
|
||||
if t.Status == nil {
|
||||
t.Status = NewStatus()
|
||||
}
|
||||
t.Status.LockType = l
|
||||
t.Status.LockReason = reason
|
||||
t.Status.SetLockAt(time.Now())
|
||||
}
|
||||
|
||||
func NewStatus() *Status {
|
||||
return &Status{}
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
// 冻结时间
|
||||
LockAt *time.Time `json:"lock_at" bson:"lock_at" gorm:"column:lock_at;type:timestamp;index" description:"冻结时间"`
|
||||
// 冻结类型
|
||||
LockType LOCK_TYPE `json:"lock_type" bson:"lock_type" gorm:"column:lock_type;type:tinyint(1)" description:"冻结类型 0:用户退出登录, 1:刷新Token过期, 回话中断, 2:异地登陆, 异常Ip登陆" enum:"0|1|2|3"`
|
||||
// 冻结原因
|
||||
LockReason string `json:"lock_reason" bson:"lock_reason" gorm:"column:lock_reason;type:text" description:"冻结原因"`
|
||||
}
|
||||
|
||||
func (s *Status) SetLockAt(v time.Time) {
|
||||
s.LockAt = &v
|
||||
}
|
||||
|
||||
func (s *Status) ToMap() map[string]any {
|
||||
return map[string]any{
|
||||
"lock_at": s.LockAt,
|
||||
"lock_type": s.LockType,
|
||||
"lock_reason": s.LockReason,
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user