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("*")) }