346 lines
8.9 KiB
Go
346 lines
8.9 KiB
Go
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("*"))
|
|
}
|