feat: done

This commit is contained in:
2025-12-18 00:21:49 +08:00
parent 7261c0fcc3
commit bfce71b56d
15 changed files with 331 additions and 17 deletions

View File

@@ -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
}

View File

@@ -1,6 +1,7 @@
package super
import (
"quyun/v2/app/middlewares"
"quyun/v2/providers/app"
"quyun/v2/providers/jwt"
@@ -26,11 +27,13 @@ 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,
middlewares: middlewares,
tenant: tenant,
user: user,
}

View File

@@ -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
@@ -19,6 +21,7 @@ import (
// @provider contracts.HttpRoute atom.GroupRoutes
type Routes struct {
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"),

View File

@@ -0,0 +1,9 @@
package super
func (r *Routes) Path() string {
return "/super"
}
func (r *Routes) Middlewares() []any {
return []any{}
}

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -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,
}
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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")

View File

@@ -0,0 +1,8 @@
package consts
const (
CtxKeyTenant = "tenant"
CtxKeyClaims = "claims"
CtxKeyUser = "user"
CtxKeyTenantUser = "tenant_user"
)

View File

@@ -18,3 +18,12 @@ Content-Type: application/json
{
"duration": 7
}
### 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