This commit is contained in:
2025-12-15 17:55:32 +08:00
commit 28ab17324d
170 changed files with 18373 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
package super
import (
"quyun/v2/providers/app"
"github.com/gofiber/fiber/v3"
)
// @provider
type authController struct {
app *app.Config
}
func (s *authController) auth(ctx fiber.Ctx) error {
// user,err:=
return nil
}

View File

@@ -0,0 +1,345 @@
package super
import (
"database/sql"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"quyun/v2/app/errorx"
"quyun/v2/providers/app"
"github.com/gofiber/fiber/v3"
)
// @provider
type SuperController struct {
DB *sql.DB
App *app.Config
}
func (s *SuperController) auth(ctx fiber.Ctx) error {
token := ""
if s.App != nil && s.App.Super != nil {
token = strings.TrimSpace(s.App.Super.Token)
}
if token == "" {
if s.App != nil && s.App.IsDevMode() {
return nil
}
return errorx.ErrUnauthorized.WithMsg("missing super admin token")
}
auth := strings.TrimSpace(ctx.Get(fiber.HeaderAuthorization))
const prefix = "Bearer "
if !strings.HasPrefix(auth, prefix) {
return errorx.ErrUnauthorized.WithMsg("invalid authorization")
}
if strings.TrimSpace(strings.TrimPrefix(auth, prefix)) != token {
return errorx.ErrUnauthorized.WithMsg("invalid token")
}
return nil
}
type SuperStatisticsResp struct {
TenantsTotal int64 `json:"tenants_total"`
TenantsEnabled int64 `json:"tenants_enabled"`
TenantAdminsTotal int64 `json:"tenant_admins_total"`
TenantAdminsExpired int64 `json:"tenant_admins_expired"`
}
// Statistics
//
// @Router /super/v1/statistics [get]
func (s *SuperController) Statistics(ctx fiber.Ctx) error {
if err := s.auth(ctx); err != nil {
return err
}
var out SuperStatisticsResp
if err := s.DB.QueryRowContext(ctx.Context(), `SELECT count(*) FROM tenants`).Scan(&out.TenantsTotal); err != nil {
return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
}
if err := s.DB.QueryRowContext(ctx.Context(), `SELECT count(*) FROM tenants WHERE status = 0`).Scan(&out.TenantsEnabled); err != nil {
return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
}
if err := s.DB.QueryRowContext(ctx.Context(), `SELECT count(*) FROM user_roles WHERE role_code = 'tenant_admin'`).Scan(&out.TenantAdminsTotal); err != nil {
return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
}
if err := s.DB.QueryRowContext(ctx.Context(), `SELECT count(*) FROM user_roles WHERE role_code = 'tenant_admin' AND expires_at IS NOT NULL AND expires_at <= now()`).Scan(&out.TenantAdminsExpired); err != nil {
return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
}
return ctx.JSON(out)
}
type TenantsQuery struct {
Page int `query:"page"`
Limit int `query:"limit"`
Keyword string `query:"keyword"`
}
type SuperTenantRow struct {
ID int64 `json:"id"`
TenantCode string `json:"tenant_code"`
TenantUUID string `json:"tenant_uuid"`
Name string `json:"name"`
Status int16 `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
TenantAdminCount int64 `json:"tenant_admin_count"`
TenantAdminExpireAt *time.Time `json:"tenant_admin_expire_at,omitempty"`
}
// Tenants
//
// @Router /super/v1/tenants [get]
// @Bind query query
func (s *SuperController) Tenants(ctx fiber.Ctx, query *TenantsQuery) error {
if err := s.auth(ctx); err != nil {
return err
}
page := 1
limit := 20
keyword := ""
if query != nil {
if query.Page > 0 {
page = query.Page
}
if query.Limit > 0 {
limit = query.Limit
}
keyword = strings.TrimSpace(query.Keyword)
}
if limit > 200 {
limit = 200
}
offset := (page - 1) * limit
where := "1=1"
args := []any{}
if keyword != "" {
where += " AND (lower(t.tenant_code) LIKE $1 OR lower(t.name) LIKE $1)"
args = append(args, "%"+strings.ToLower(keyword)+"%")
}
countSQL := "SELECT count(*) FROM tenants t WHERE " + where
var total int64
if err := s.DB.QueryRowContext(ctx.Context(), countSQL, args...).Scan(&total); err != nil {
return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
}
args = append(args, limit, offset)
limitArg := "$" + strconv.Itoa(len(args)-1)
offsetArg := "$" + strconv.Itoa(len(args))
sqlStr := `
SELECT
t.id, t.tenant_code, t.tenant_uuid, t.name, t.status, t.created_at, t.updated_at,
COALESCE(a.admin_count, 0) AS admin_count,
a.max_expires_at
FROM tenants t
LEFT JOIN (
SELECT tenant_id,
count(*) AS admin_count,
max(expires_at) AS max_expires_at
FROM user_roles
WHERE role_code = 'tenant_admin' AND tenant_id IS NOT NULL
GROUP BY tenant_id
) a ON a.tenant_id = t.id
WHERE ` + where + `
ORDER BY t.id DESC
LIMIT ` + limitArg + ` OFFSET ` + offsetArg
rows, err := s.DB.QueryContext(ctx.Context(), sqlStr, args...)
if err != nil {
return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
}
defer rows.Close()
items := make([]SuperTenantRow, 0, limit)
for rows.Next() {
var (
it SuperTenantRow
uuidStr string
)
if err := rows.Scan(
&it.ID,
&it.TenantCode,
&uuidStr,
&it.Name,
&it.Status,
&it.CreatedAt,
&it.UpdatedAt,
&it.TenantAdminCount,
&it.TenantAdminExpireAt,
); err != nil {
return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
}
it.TenantUUID = uuidStr
items = append(items, it)
}
if err := rows.Err(); err != nil {
return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
}
return ctx.JSON(fiber.Map{
"items": items,
"total": total,
"page": page,
"limit": limit,
})
}
type RoleRow struct {
Code string `json:"code"`
Name string `json:"name"`
Status int16 `json:"status"`
UpdatedAt time.Time `json:"updated_at"`
}
// Roles
//
// @Router /super/v1/roles [get]
func (s *SuperController) Roles(ctx fiber.Ctx) error {
if err := s.auth(ctx); err != nil {
return err
}
rows, err := s.DB.QueryContext(ctx.Context(), `SELECT code, name, status, updated_at FROM roles ORDER BY code`)
if err != nil {
return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
}
defer rows.Close()
items := make([]RoleRow, 0, 8)
for rows.Next() {
var it RoleRow
if err := rows.Scan(&it.Code, &it.Name, &it.Status, &it.UpdatedAt); err != nil {
return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
}
items = append(items, it)
}
if err := rows.Err(); err != nil {
return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
}
return ctx.JSON(fiber.Map{"items": items})
}
type UpdateRoleReq struct {
Name *string `json:"name"`
Status *int16 `json:"status"`
}
// UpdateRole
//
// @Router /super/v1/roles/:code [put]
// @Bind code path
// @Bind req body
func (s *SuperController) UpdateRole(ctx fiber.Ctx, code string, req *UpdateRoleReq) error {
if err := s.auth(ctx); err != nil {
return err
}
code = strings.TrimSpace(code)
if code == "" {
return errorx.ErrInvalidParameter.WithMsg("missing code")
}
if req == nil {
return errorx.ErrInvalidJSON
}
switch code {
case "user", "tenant_admin", "super_admin":
default:
return errorx.ErrInvalidParameter.WithMsg("unknown role code")
}
set := make([]string, 0, 2)
args := make([]any, 0, 3)
i := 1
if req.Name != nil {
set = append(set, "name = $"+strconv.Itoa(i))
args = append(args, strings.TrimSpace(*req.Name))
i++
}
if req.Status != nil {
set = append(set, "status = $"+strconv.Itoa(i))
args = append(args, *req.Status)
i++
}
if len(set) == 0 {
return ctx.JSON(fiber.Map{"ok": true})
}
set = append(set, "updated_at = now()")
args = append(args, code)
sqlStr := "UPDATE roles SET " + strings.Join(set, ", ") + " WHERE code = $" + strconv.Itoa(i)
res, err := s.DB.ExecContext(ctx.Context(), sqlStr, args...)
if err != nil {
return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
}
aff, _ := res.RowsAffected()
if aff == 0 {
return fiber.ErrNotFound
}
return ctx.JSON(fiber.Map{"ok": true})
}
func resolveDistDir(primary, fallback string) string {
if st, err := os.Stat(primary); err == nil && st.IsDir() {
return primary
}
return fallback
}
func sendIndex(ctx fiber.Ctx, distDir string) error {
indexPath := filepath.Join(distDir, "index.html")
if st, err := os.Stat(indexPath); err == nil && !st.IsDir() {
return ctx.SendFile(indexPath)
}
return fiber.ErrNotFound
}
func sendAssetOrIndex(ctx fiber.Ctx, distDir, rel string) error {
rel = filepath.Clean(strings.TrimSpace(rel))
if rel == "." || rel == "/" {
rel = ""
}
if strings.HasPrefix(rel, "..") {
return fiber.ErrBadRequest
}
if rel != "" {
assetPath := filepath.Join(distDir, rel)
if st, err := os.Stat(assetPath); err == nil && !st.IsDir() {
return ctx.SendFile(assetPath)
}
}
return sendIndex(ctx, distDir)
}
// SuperIndex
//
// @Router /super [get]
func (s *SuperController) SuperIndex(ctx fiber.Ctx) error {
dist := resolveDistDir("frontend/superadmin/dist", "../frontend/superadmin/dist")
return sendIndex(ctx, dist)
}
// SuperWildcard
//
// @Router /super/* [get]
func (s *SuperController) SuperWildcard(ctx fiber.Ctx) error {
dist := resolveDistDir("frontend/superadmin/dist", "../frontend/superadmin/dist")
return sendAssetOrIndex(ctx, dist, ctx.Params("*"))
}

View File

@@ -0,0 +1,54 @@
package super
import (
"database/sql"
"quyun/v2/providers/app"
"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(
app *app.Config,
) (*authController, error) {
obj := &authController{
app: app,
}
return obj, nil
}); err != nil {
return err
}
if err := container.Container.Provide(func(
App *app.Config,
DB *sql.DB,
) (*SuperController, error) {
obj := &SuperController{
App: App,
DB: DB,
}
return obj, nil
}); err != nil {
return err
}
if err := container.Container.Provide(func(
superController *SuperController,
) (contracts.HttpRoute, error) {
obj := &Routes{
superController: superController,
}
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,70 @@
// Code generated by atomctl. DO NOT EDIT.
// Package super provides HTTP route definitions and registration
// for the quyun/v2 application.
package super
import (
"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 super module.
//
// @provider contracts.HttpRoute atom.GroupRoutes
type Routes struct {
log *log.Entry `inject:"false"`
// Controller instances
superController *SuperController
}
// Prepare initializes the routes provider with logging configuration.
func (r *Routes) Prepare() error {
r.log = log.WithField("module", "routes.super")
r.log.Info("Initializing routes module")
return nil
}
// Name returns the unique identifier for this routes provider.
func (r *Routes) Name() string {
return "super"
}
// 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: SuperController
r.log.Debugf("Registering route: Get /super -> superController.SuperIndex")
router.Get("/super", Func0(
r.superController.SuperIndex,
))
r.log.Debugf("Registering route: Get /super/* -> superController.SuperWildcard")
router.Get("/super/*", Func0(
r.superController.SuperWildcard,
))
r.log.Debugf("Registering route: Get /super/v1/roles -> superController.Roles")
router.Get("/super/v1/roles", Func0(
r.superController.Roles,
))
r.log.Debugf("Registering route: Get /super/v1/statistics -> superController.Statistics")
router.Get("/super/v1/statistics", Func0(
r.superController.Statistics,
))
r.log.Debugf("Registering route: Get /super/v1/tenants -> superController.Tenants")
router.Get("/super/v1/tenants", Func1(
r.superController.Tenants,
Query[TenantsQuery]("query"),
))
r.log.Debugf("Registering route: Put /super/v1/roles/:code -> superController.UpdateRole")
router.Put("/super/v1/roles/:code", Func2(
r.superController.UpdateRole,
PathParam[string]("code"),
Body[UpdateRoleReq]("req"),
))
r.log.Info("Successfully registered all routes")
}