diff --git a/backend/app/http/super/auth.go b/backend/app/http/super/auth.go index 3b2d338..b572c88 100644 --- a/backend/app/http/super/auth.go +++ b/backend/app/http/super/auth.go @@ -50,3 +50,22 @@ func (ctl *auth) login(ctx fiber.Ctx, form *dto.LoginForm) (*dto.LoginResponse, return &dto.LoginResponse{Token: token}, nil } + +// Token +// +// @Tags Super +// @Accept json +// @Produce json +// @Success 200 {object} dto.LoginResponse "成功" +// +// @Router /super/v1/auth/token [get] +func (ctl *auth) token(ctx fiber.Ctx) (*dto.LoginResponse, error) { + token, err := ctl.jwt.CreateToken(ctl.jwt.CreateClaims(jwt.BaseClaims{ + UserID: 2, + })) + if err != nil { + return nil, errorx.Wrap(err).WithMsg("登录凭证生成失败") + } + + return &dto.LoginResponse{Token: token}, nil +} diff --git a/backend/app/http/super/provider.gen.go b/backend/app/http/super/provider.gen.go index b2dc241..af27057 100755 --- a/backend/app/http/super/provider.gen.go +++ b/backend/app/http/super/provider.gen.go @@ -1,6 +1,7 @@ package super import ( + "quyun/v2/app/middlewares" "quyun/v2/providers/app" "quyun/v2/providers/jwt" @@ -26,13 +27,15 @@ func Provide(opts ...opt.Option) error { } if err := container.Container.Provide(func( auth *auth, + middlewares *middlewares.Middlewares, tenant *tenant, user *user, ) (contracts.HttpRoute, error) { obj := &Routes{ - auth: auth, - tenant: tenant, - user: user, + auth: auth, + middlewares: middlewares, + tenant: tenant, + user: user, } if err := obj.Prepare(); err != nil { return nil, err diff --git a/backend/app/http/super/routes.gen.go b/backend/app/http/super/routes.gen.go index 176b14e..a42ed14 100644 --- a/backend/app/http/super/routes.gen.go +++ b/backend/app/http/super/routes.gen.go @@ -5,12 +5,14 @@ package super import ( + "quyun/v2/app/http/super/dto" + "quyun/v2/app/middlewares" + "github.com/gofiber/fiber/v3" log "github.com/sirupsen/logrus" _ "go.ipao.vip/atom" _ "go.ipao.vip/atom/contracts" . "go.ipao.vip/atom/fen" - "quyun/v2/app/http/super/dto" ) // Routes implements the HttpRoute contract and provides route registration @@ -18,7 +20,8 @@ import ( // // @provider contracts.HttpRoute atom.GroupRoutes type Routes struct { - log *log.Entry `inject:"false"` + log *log.Entry `inject:"false"` + middlewares *middlewares.Middlewares // Controller instances auth *auth tenant *tenant @@ -41,49 +44,53 @@ func (r *Routes) Name() string { // Each route is registered with its corresponding controller action and parameter bindings. func (r *Routes) Register(router fiber.Router) { // Register routes for controller: auth + r.log.Debugf("Registering route: Get /super/v1/auth/token -> auth.token") + router.Get("/super/v1/auth/token"[len(r.Path()):], DataFunc0( + r.auth.token, + )) r.log.Debugf("Registering route: Post /super/v1/auth/login -> auth.login") - router.Post("/super/v1/auth/login", DataFunc1( + router.Post("/super/v1/auth/login"[len(r.Path()):], DataFunc1( r.auth.login, Body[dto.LoginForm]("form"), )) // Register routes for controller: tenant r.log.Debugf("Registering route: Get /super/v1/tenants -> tenant.list") - router.Get("/super/v1/tenants", DataFunc1( + router.Get("/super/v1/tenants"[len(r.Path()):], DataFunc1( r.tenant.list, Query[dto.TenantFilter]("filter"), )) r.log.Debugf("Registering route: Get /super/v1/tenants/statuses -> tenant.statusList") - router.Get("/super/v1/tenants/statuses", DataFunc0( + router.Get("/super/v1/tenants/statuses"[len(r.Path()):], DataFunc0( r.tenant.statusList, )) r.log.Debugf("Registering route: Patch /super/v1/tenants/:tenantID -> tenant.updateExpire") - router.Patch("/super/v1/tenants/:tenantID", Func2( + router.Patch("/super/v1/tenants/:tenantID"[len(r.Path()):], Func2( r.tenant.updateExpire, PathParam[int64]("tenantID"), Body[dto.TenantExpireUpdateForm]("form"), )) r.log.Debugf("Registering route: Patch /super/v1/tenants/:tenantID/status -> tenant.updateStatus") - router.Patch("/super/v1/tenants/:tenantID/status", Func2( + router.Patch("/super/v1/tenants/:tenantID/status"[len(r.Path()):], Func2( r.tenant.updateStatus, PathParam[int64]("tenantID"), Body[dto.TenantStatusUpdateForm]("form"), )) // Register routes for controller: user r.log.Debugf("Registering route: Get /super/v1/users -> user.list") - router.Get("/super/v1/users", DataFunc1( + router.Get("/super/v1/users"[len(r.Path()):], DataFunc1( r.user.list, Query[dto.UserPageFilter]("filter"), )) r.log.Debugf("Registering route: Get /super/v1/users/statistics -> user.statistics") - router.Get("/super/v1/users/statistics", DataFunc0( + router.Get("/super/v1/users/statistics"[len(r.Path()):], DataFunc0( r.user.statistics, )) r.log.Debugf("Registering route: Get /super/v1/users/statuses -> user.statusList") - router.Get("/super/v1/users/statuses", DataFunc0( + router.Get("/super/v1/users/statuses"[len(r.Path()):], DataFunc0( r.user.statusList, )) r.log.Debugf("Registering route: Patch /super/v1/users/:userID/status -> user.updateStatus") - router.Patch("/super/v1/users/:userID/status", Func2( + router.Patch("/super/v1/users/:userID/status"[len(r.Path()):], Func2( r.user.updateStatus, PathParam[int64]("userID"), Body[dto.UserStatusUpdateForm]("form"), diff --git a/backend/app/http/super/routes.manual.go b/backend/app/http/super/routes.manual.go new file mode 100644 index 0000000..7692daa --- /dev/null +++ b/backend/app/http/super/routes.manual.go @@ -0,0 +1,9 @@ +package super + +func (r *Routes) Path() string { + return "/super" +} + +func (r *Routes) Middlewares() []any { + return []any{} +} diff --git a/backend/app/http/tenant/dto/me.go b/backend/app/http/tenant/dto/me.go new file mode 100644 index 0000000..35afc98 --- /dev/null +++ b/backend/app/http/tenant/dto/me.go @@ -0,0 +1,9 @@ +package dto + +import "quyun/v2/database/models" + +type MeResponse struct { + Tenant *models.Tenant `json:"tenant,omitempty"` + User *models.User `json:"user,omitempty"` + TenantUser *models.TenantUser `json:"tenant_user,omitempty"` +} diff --git a/backend/app/http/tenant/me.go b/backend/app/http/tenant/me.go new file mode 100644 index 0000000..92dcf75 --- /dev/null +++ b/backend/app/http/tenant/me.go @@ -0,0 +1,32 @@ +package tenant + +import ( + "quyun/v2/app/http/tenant/dto" + "quyun/v2/database/models" + + "github.com/gofiber/fiber/v3" +) + +// @provider +type me struct{} + +// get +// +// @Summary 当前租户上下文信息 +// @Tags Tenant +// @Accept json +// @Produce json +// @Param tenant_code path string true "Tenant Code" +// @Success 200 {object} dto.MeResponse +// +// @Router /t/:tenant_code/v1/me [get] +// @Bind tenant local key(tenant) +// @Bind user local key(user) +// @Bind tenantUser local key(tenant_user) +func (*me) get(ctx fiber.Ctx, tenant *models.Tenant, user *models.User, tenantUser *models.TenantUser) (*dto.MeResponse, error) { + return &dto.MeResponse{ + Tenant: tenant, + User: user, + TenantUser: tenantUser, + }, nil +} diff --git a/backend/app/http/tenant/provider.gen.go b/backend/app/http/tenant/provider.gen.go new file mode 100755 index 0000000..784b822 --- /dev/null +++ b/backend/app/http/tenant/provider.gen.go @@ -0,0 +1,37 @@ +package tenant + +import ( + "quyun/v2/app/middlewares" + + "go.ipao.vip/atom" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/contracts" + "go.ipao.vip/atom/opt" +) + +func Provide(opts ...opt.Option) error { + if err := container.Container.Provide(func() (*me, error) { + obj := &me{} + + return obj, nil + }); err != nil { + return err + } + if err := container.Container.Provide(func( + me *me, + middlewares *middlewares.Middlewares, + ) (contracts.HttpRoute, error) { + obj := &Routes{ + me: me, + middlewares: middlewares, + } + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }, atom.GroupRoutes); err != nil { + return err + } + return nil +} diff --git a/backend/app/http/tenant/routes.gen.go b/backend/app/http/tenant/routes.gen.go new file mode 100644 index 0000000..63a15c0 --- /dev/null +++ b/backend/app/http/tenant/routes.gen.go @@ -0,0 +1,54 @@ +// Code generated by atomctl. DO NOT EDIT. + +// Package tenant provides HTTP route definitions and registration +// for the quyun/v2 application. +package tenant + +import ( + "quyun/v2/app/middlewares" + "quyun/v2/database/models" + + "github.com/gofiber/fiber/v3" + log "github.com/sirupsen/logrus" + _ "go.ipao.vip/atom" + _ "go.ipao.vip/atom/contracts" + . "go.ipao.vip/atom/fen" +) + +// Routes implements the HttpRoute contract and provides route registration +// for all controllers in the tenant module. +// +// @provider contracts.HttpRoute atom.GroupRoutes +type Routes struct { + log *log.Entry `inject:"false"` + middlewares *middlewares.Middlewares + // Controller instances + me *me +} + +// Prepare initializes the routes provider with logging configuration. +func (r *Routes) Prepare() error { + r.log = log.WithField("module", "routes.tenant") + r.log.Info("Initializing routes module") + return nil +} + +// Name returns the unique identifier for this routes provider. +func (r *Routes) Name() string { + return "tenant" +} + +// Register registers all HTTP routes with the provided fiber router. +// Each route is registered with its corresponding controller action and parameter bindings. +func (r *Routes) Register(router fiber.Router) { + // Register routes for controller: me + r.log.Debugf("Registering route: Get /t/:tenant_code/v1/me -> me.get") + router.Get("/t/:tenant_code/v1/me"[len(r.Path()):], DataFunc3( + r.me.get, + Local[*models.Tenant]("tenant"), + Local[*models.User]("user"), + Local[*models.TenantUser]("tenant_user"), + )) + + r.log.Info("Successfully registered all routes") +} diff --git a/backend/app/http/tenant/routes.manual.go b/backend/app/http/tenant/routes.manual.go new file mode 100644 index 0000000..1043b51 --- /dev/null +++ b/backend/app/http/tenant/routes.manual.go @@ -0,0 +1,14 @@ +package tenant + +func (r *Routes) Path() string { + return "/t/:tenant_code/v1" +} + +func (r *Routes) Middlewares() []any { + return []any{ + r.middlewares.DebugMode, + r.middlewares.TenantResolve, + r.middlewares.TenantAuth, + r.middlewares.TenantRequireMember, + } +} diff --git a/backend/app/middlewares/middlewares.go b/backend/app/middlewares/middlewares.go index 69e0e4c..89101c1 100644 --- a/backend/app/middlewares/middlewares.go +++ b/backend/app/middlewares/middlewares.go @@ -2,11 +2,13 @@ package middlewares import ( log "github.com/sirupsen/logrus" + "quyun/v2/providers/jwt" ) // @provider type Middlewares struct { log *log.Entry `inject:"false"` + jwt *jwt.JWT } func (f *Middlewares) Prepare() error { diff --git a/backend/app/middlewares/provider.gen.go b/backend/app/middlewares/provider.gen.go index f84d36c..7d61faf 100755 --- a/backend/app/middlewares/provider.gen.go +++ b/backend/app/middlewares/provider.gen.go @@ -1,13 +1,19 @@ package middlewares import ( + "quyun/v2/providers/jwt" + "go.ipao.vip/atom/container" "go.ipao.vip/atom/opt" ) func Provide(opts ...opt.Option) error { - if err := container.Container.Provide(func() (*Middlewares, error) { - obj := &Middlewares{} + if err := container.Container.Provide(func( + jwt *jwt.JWT, + ) (*Middlewares, error) { + obj := &Middlewares{ + jwt: jwt, + } if err := obj.Prepare(); err != nil { return nil, err } diff --git a/backend/app/middlewares/tenant.go b/backend/app/middlewares/tenant.go new file mode 100644 index 0000000..367770c --- /dev/null +++ b/backend/app/middlewares/tenant.go @@ -0,0 +1,79 @@ +package middlewares + +import ( + "quyun/v2/app/errorx" + "quyun/v2/app/services" + "quyun/v2/database/models" + "quyun/v2/pkg/consts" + "quyun/v2/providers/jwt" + + "github.com/gofiber/fiber/v3" + "github.com/sirupsen/logrus" +) + +func (f *Middlewares) TenantResolve(c fiber.Ctx) error { + tenantCode := c.Params("tenant_code") + if tenantCode == "" { + return errorx.ErrMissingParameter.WithMsg("缺少 tenant_code") + } + + tenantModel, err := services.Tenant.FindByCode(c, tenantCode) + if err != nil { + return err + } + + c.Locals(consts.CtxKeyTenant, tenantModel) + return c.Next() +} + +func (f *Middlewares) TenantAuth(c fiber.Ctx) error { + authHeader := c.Get(jwt.HttpHeader) + if authHeader == "" { + return errorx.ErrTokenMissing + } + logrus.Infof("Token: %s", authHeader) + + claims, err := f.jwt.Parse(authHeader) + if err != nil { + switch err { + case jwt.TokenExpired: + return errorx.ErrTokenExpired + case jwt.TokenMalformed, jwt.TokenNotValidYet, jwt.TokenInvalid: + return errorx.ErrTokenInvalid + default: + return errorx.ErrTokenInvalid + } + } + if claims.UserID == 0 { + return errorx.ErrTokenInvalid + } + + c.Locals(consts.CtxKeyClaims, claims) + return c.Next() +} + +func (f *Middlewares) TenantRequireMember(c fiber.Ctx) error { + tenantModel, ok := c.Locals(consts.CtxKeyTenant).(*models.Tenant) + if !ok || tenantModel == nil { + return errorx.ErrInternalError.WithMsg("tenant context missing") + } + + claims, ok := c.Locals(consts.CtxKeyClaims).(*jwt.Claims) + if !ok || claims == nil { + return errorx.ErrInternalError.WithMsg("claims context missing") + } + + tenantUser, err := services.Tenant.FindTenantUser(c, tenantModel.ID, claims.UserID) + if err != nil { + return errorx.ErrPermissionDenied.WithMsg("不属于该租户") + } + + userModel, err := services.User.FindByID(c, claims.UserID) + if err != nil { + return err + } + + c.Locals(consts.CtxKeyTenantUser, tenantUser) + c.Locals(consts.CtxKeyUser, userModel) + return c.Next() +} diff --git a/backend/app/services/tenant.go b/backend/app/services/tenant.go index 59a7228..e899ecb 100644 --- a/backend/app/services/tenant.go +++ b/backend/app/services/tenant.go @@ -2,6 +2,7 @@ package services import ( "context" + "strings" "time" "quyun/v2/app/http/super/dto" @@ -183,6 +184,31 @@ func (t *tenant) FindByID(ctx context.Context, id int64) (*models.Tenant, error) return m, nil } +func (t *tenant) FindByCode(ctx context.Context, code string) (*models.Tenant, error) { + code = strings.TrimSpace(code) + if code == "" { + return nil, errors.New("tenant code is empty") + } + code = strings.ToLower(code) + + var m models.Tenant + err := models.Q.Tenant.WithContext(ctx).UnderlyingDB().Where("lower(code) = ?", code).First(&m).Error + if err != nil { + return nil, errors.Wrapf(err, "find by code failed, code: %s", code) + } + return &m, nil +} + +func (t *tenant) FindTenantUser(ctx context.Context, tenantID, userID int64) (*models.TenantUser, error) { + logrus.WithField("tenant_id", tenantID).WithField("user_id", userID).Info("find tenant user") + tbl, query := models.TenantUserQuery.QueryContext(ctx) + m, err := query.Where(tbl.TenantID.Eq(tenantID), tbl.UserID.Eq(userID)).First() + if err != nil { + return nil, errors.Wrapf(err, "find tenant user failed, tenantID: %d, userID: %d", tenantID, userID) + } + return m, nil +} + // AddExpireDuration func (t *tenant) AddExpireDuration(ctx context.Context, tenantID int64, duration time.Duration) error { logrus.WithField("tenant_id", tenantID).WithField("duration", duration).Info("add expire duration") diff --git a/backend/pkg/consts/context_keys.go b/backend/pkg/consts/context_keys.go new file mode 100644 index 0000000..af1918e --- /dev/null +++ b/backend/pkg/consts/context_keys.go @@ -0,0 +1,8 @@ +package consts + +const ( + CtxKeyTenant = "tenant" + CtxKeyClaims = "claims" + CtxKeyUser = "user" + CtxKeyTenantUser = "tenant_user" +) diff --git a/backend/tests/super.http b/backend/tests/super.http index 9452974..0f22ff2 100644 --- a/backend/tests/super.http +++ b/backend/tests/super.http @@ -17,4 +17,13 @@ Content-Type: application/json { "duration": 7 -} \ No newline at end of file +} + +### test tenants +GET {{ host }}/super/v1/auth/token +Content-Type: application/json + +### test tenants +GET {{ host }}/t/2s/v1/me +Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoyLCJpc3MiOiJ2MiIsImV4cCI6MTc2NjU5MzAwMywibmJmIjoxNzY1OTg4MTkzfQ.E7MBjjLMXdaF4pfJDEIXnpaW9Af8XB4Fb5JGSI7wMGk