diff --git a/backend/app/commands/http/http.go b/backend/app/commands/http/http.go index a0b33c4..4d30027 100644 --- a/backend/app/commands/http/http.go +++ b/backend/app/commands/http/http.go @@ -6,16 +6,15 @@ import ( "sort" "strings" - "go.ipao.vip/atom" - "go.ipao.vip/atom/container" - "go.ipao.vip/atom/contracts" "quyun/v2/app/commands" "quyun/v2/app/errorx" "quyun/v2/app/http/api" "quyun/v2/app/http/super" "quyun/v2/app/http/web" "quyun/v2/app/jobs" + "quyun/v2/app/services" "quyun/v2/app/tenancy" + "quyun/v2/database" _ "quyun/v2/docs" "quyun/v2/providers/app" "quyun/v2/providers/http" @@ -24,6 +23,10 @@ import ( "quyun/v2/providers/jwt" "quyun/v2/providers/postgres" + "go.ipao.vip/atom" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/contracts" + "github.com/gofiber/fiber/v3/middleware/favicon" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -36,6 +39,8 @@ func defaultProviders() container.Providers { postgres.DefaultProvider(), jwt.DefaultProvider(), job.DefaultProvider(), + database.DefaultProvider(), + {Provider: services.Provide}, {Provider: api.Provide}, {Provider: super.Provide}, {Provider: web.Provide}, diff --git a/backend/app/errorx/app_error.go b/backend/app/errorx/app_error.go index ed70ea7..dadec8c 100644 --- a/backend/app/errorx/app_error.go +++ b/backend/app/errorx/app_error.go @@ -40,6 +40,11 @@ func (e *AppError) WithMsg(msg string) *AppError { return e } +func (e *AppError) WithMsgf(format string, args ...any) *AppError { + msg := fmt.Sprintf(format, args...) + return e.WithMsg(msg) +} + // WithSQL 记录SQL信息 func (e *AppError) WithSQL(sql string) *AppError { e.sql = sql diff --git a/backend/app/http/super/auth.go b/backend/app/http/super/auth.go index 5aeb7ae..3729a28 100644 --- a/backend/app/http/super/auth.go +++ b/backend/app/http/super/auth.go @@ -1,7 +1,11 @@ package super import ( + "quyun/v2/app/errorx" + "quyun/v2/app/http/super/dto" + "quyun/v2/app/services" "quyun/v2/providers/app" + "quyun/v2/providers/jwt" "github.com/gofiber/fiber/v3" ) @@ -9,9 +13,27 @@ import ( // @provider type authController struct { app *app.Config + jwt *jwt.JWT } -func (s *authController) auth(ctx fiber.Ctx) error { - // user,err:= - return nil +// @Router /super/v1/auth/login [post] +// @Bind form body +func (ctl *authController) login(ctx fiber.Ctx, form *dto.LoginForm) (*dto.LoginResponse, error) { + m, err := services.User.FindByUsername(ctx, form.Username) + if err != nil { + return nil, errorx.Wrap(err).WithMsg("用户名或密码错误") + } + + if ok := m.ComparePassword(ctx, form.Password); !ok { + return nil, errorx.Wrap(err).WithMsg("用户名或密码错误") + } + + token, err := ctl.jwt.CreateToken(ctl.jwt.CreateClaims(jwt.BaseClaims{ + UserID: m.ID, + })) + if err != nil { + return nil, errorx.Wrap(err).WithMsg("登录凭证生成失败") + } + + return &dto.LoginResponse{Token: token}, nil } diff --git a/backend/app/http/super/controller.go b/backend/app/http/super/controller.go deleted file mode 100644 index de01cd7..0000000 --- a/backend/app/http/super/controller.go +++ /dev/null @@ -1,345 +0,0 @@ -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("*")) -} diff --git a/backend/app/http/super/dto/auth.go b/backend/app/http/super/dto/auth.go new file mode 100644 index 0000000..1eef1a0 --- /dev/null +++ b/backend/app/http/super/dto/auth.go @@ -0,0 +1,10 @@ +package dto + +type LoginForm struct { + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` +} + +type LoginResponse struct { + Token string `json:"token,omitempty"` +} diff --git a/backend/app/http/super/provider.gen.go b/backend/app/http/super/provider.gen.go index 957d649..2e01ffb 100755 --- a/backend/app/http/super/provider.gen.go +++ b/backend/app/http/super/provider.gen.go @@ -1,9 +1,8 @@ package super import ( - "database/sql" - "quyun/v2/providers/app" + "quyun/v2/providers/jwt" "go.ipao.vip/atom" "go.ipao.vip/atom/container" @@ -14,9 +13,11 @@ import ( func Provide(opts ...opt.Option) error { if err := container.Container.Provide(func( app *app.Config, + jwt *jwt.JWT, ) (*authController, error) { obj := &authController{ app: app, + jwt: jwt, } return obj, nil @@ -24,23 +25,10 @@ func Provide(opts ...opt.Option) error { 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, + authController *authController, ) (contracts.HttpRoute, error) { obj := &Routes{ - superController: superController, + authController: authController, } 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 b59254b..2edb55a 100644 --- a/backend/app/http/super/routes.gen.go +++ b/backend/app/http/super/routes.gen.go @@ -10,6 +10,7 @@ import ( _ "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,7 +20,7 @@ import ( type Routes struct { log *log.Entry `inject:"false"` // Controller instances - superController *SuperController + authController *authController } // Prepare initializes the routes provider with logging configuration. @@ -37,33 +38,11 @@ func (r *Routes) Name() string { // 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"), + // Register routes for controller: authController + r.log.Debugf("Registering route: Post /super/v1/auth/login -> authController.login") + router.Post("/super/v1/auth/login", DataFunc1( + r.authController.login, + Body[dto.LoginForm]("form"), )) r.log.Info("Successfully registered all routes") diff --git a/backend/super.http b/backend/super.http new file mode 100644 index 0000000..cf2bb02 --- /dev/null +++ b/backend/super.http @@ -0,0 +1,12 @@ +@host = http://localhost:8080 + + + +## Login +POST {{ host }}/super/v1/auth/login +Content-Type: application/json + +{ + "username":"aa", + "password":"bb" +} \ No newline at end of file