/* * Copyright (c) Mengning Software. 2025. All rights reserved. * Authors: DevStar Team, panshuxiao * Create: 2025-11-19 * Description: User settings handlers for AppStore UI. */ // Copyright 2024 The Devstar Authors. All rights reserved. // SPDX-License-Identifier: MIT package setting import ( "io" "net/http" "strings" appstore_model "code.gitea.io/gitea/models/appstore" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/services/appstore" "code.gitea.io/gitea/services/context" ) const ( tplSettingsAppStore templates.TplName = "user/settings/appstore" ) // AppStore displays the app store page func AppStore(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings.appstore") ctx.Data["PageIsSettingsAppStore"] = true ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) ctx.HTML(http.StatusOK, tplSettingsAppStore) } // AppStoreAPI handles API requests for app store data func AppStoreAPI(ctx *context.Context) { action := ctx.PathParam("action") switch action { case "apps": handleGetApps(ctx) case "categories": handleGetCategories(ctx) case "tags": handleGetTags(ctx) default: ctx.JSON(http.StatusBadRequest, map[string]interface{}{ "error": "Invalid action", }) } } // handleGetApps returns the list of apps from database func handleGetApps(ctx *context.Context) { // Get query parameters category := ctx.FormString("category") tag := ctx.FormString("tag") search := ctx.FormString("search") deployment := ctx.FormString("deployment") // docker, kubernetes source := ctx.FormString("source") // local | devstar manager := appstore.NewManager(ctx, ctx.Doer.ID) var apps []appstore.App var err error if source == "devstar" { apps, err = manager.ListAppsFromDevstar() if err == nil { // 与本地一致的过滤逻辑(先拿全量,再在服务端筛选) if category != "" || tag != "" || search != "" || deployment != "" { var filtered []appstore.App for _, a := range apps { if category != "" && a.Category != category { continue } if tag != "" { matched := false for _, t := range a.Tags { if t == tag { matched = true break } } if !matched { continue } } if deployment != "" { // 处理部署类型过滤,包括 'both' 类型 appDeployment := a.DeploymentType if appDeployment == "" { // 如果没有 deployment_type 字段,尝试从其他字段获取 if a.Deploy.Type != "" { appDeployment = a.Deploy.Type } else { appDeployment = "docker" // 默认值 } } if appDeployment != "both" && appDeployment != deployment { continue } } if search != "" { low := strings.ToLower(search) nameOk := strings.Contains(strings.ToLower(a.Name), low) descOk := strings.Contains(strings.ToLower(a.Description), low) authorOk := strings.Contains(strings.ToLower(a.Author), low) if !(nameOk || descOk || authorOk) { continue } } filtered = append(filtered, a) } apps = filtered } } } else { if search != "" { // Convert tag parameter to tags slice var tags []string if tag != "" { tags = strings.Split(tag, ",") } apps, err = manager.SearchApps(search, category, tags) } else { apps, err = manager.ListApps() // Filter by category, tag and deployment if specified if category != "" || tag != "" || deployment != "" { var filteredApps []appstore.App for _, app := range apps { matchCategory := category == "" || app.Category == category matchTag := tag == "" if tag != "" { for _, appTag := range app.Tags { if appTag == tag { matchTag = true break } } } // 处理部署类型过滤,包括 'both' 类型 matchDeployment := deployment == "" if deployment != "" { appDeployment := app.DeploymentType if appDeployment == "" { // 如果没有 deployment_type 字段,尝试从其他字段获取 if app.Deploy.Type != "" { appDeployment = app.Deploy.Type } else { appDeployment = "docker" // 默认值 } } matchDeployment = appDeployment == "both" || appDeployment == deployment } if matchCategory && matchTag && matchDeployment { filteredApps = append(filteredApps, app) } } apps = filteredApps } } } if err != nil { ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ "error": err.Error(), }) return } ctx.JSON(http.StatusOK, map[string]interface{}{ "apps": apps, }) } // handleGetCategories returns the list of categories func handleGetCategories(ctx *context.Context) { categories, err := appstore_model.GetCategories(ctx) if err != nil { ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ "error": err.Error(), }) return } ctx.JSON(http.StatusOK, map[string]interface{}{ "categories": categories, }) } // handleGetTags returns the list of tags func handleGetTags(ctx *context.Context) { tags, err := appstore_model.GetTags(ctx) if err != nil { ctx.JSON(http.StatusBadRequest, map[string]interface{}{ "error": err.Error(), }) return } ctx.JSON(http.StatusOK, map[string]interface{}{ "tags": tags, }) } // AppStoreStatus handles app instance status requests func AppStoreStatus(ctx *context.Context) { appID := ctx.PathParam("app_id") if appID == "" { ctx.JSON(http.StatusBadRequest, map[string]interface{}{ "error": "应用ID不能为空", }) return } manager := appstore.NewManager(ctx, ctx.Doer.ID) status, err := manager.GetAppStatus(appID) if err != nil { if appErr, ok := err.(*appstore.AppStoreError); ok { ctx.JSON(http.StatusNotFound, map[string]interface{}{ "error": appErr.Message, }) } else { ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ "error": "获取应用状态失败", }) } return } ctx.JSON(http.StatusOK, status) } // AppStoreInstall handles app installation func AppStoreInstall(ctx *context.Context) { // 检查用户登录状态 if !ctx.IsSigned { ctx.JSON(401, map[string]string{"error": "未登录"}) return } if ctx.Req.Method != "POST" { ctx.JSON(405, map[string]string{"error": "Method Not Allowed"}) return } // 解析表单数据 appID := ctx.FormString("app_id") configJSON := ctx.FormString("config") installTarget := ctx.FormString("install_target") k8sURL := ctx.FormString("k8s_url") token := ctx.FormString("k8s_token") if appID == "" { ctx.JSON(400, map[string]string{"error": "应用ID不能为空"}) return } // 权限校验:仅管理员可选择默认位置 if installTarget == "local" && (ctx.Doer == nil || !ctx.Doer.IsAdmin) { ctx.JSON(403, map[string]string{"error": "仅管理员可选择默认位置"}) return } // 创建 manager 并执行安装 manager := appstore.NewManager(ctx, ctx.Doer.ID) if err := manager.InstallApp(appID, configJSON, installTarget, k8sURL, token); err != nil { // 根据错误类型返回相应的状态码和消息 if appErr, ok := err.(*appstore.AppStoreError); ok { switch appErr.Code { case "APP_NOT_FOUND", "CONFIG_MERGE_ERROR": ctx.JSON(400, map[string]string{"error": appErr.Message + ": " + appErr.Details}) case "KUBERNETES_INSTALL_ERROR": ctx.JSON(500, map[string]string{"error": appErr.Message + ": " + appErr.Details}) case "NOT_IMPLEMENTED": ctx.JSON(501, map[string]string{"error": appErr.Message}) default: ctx.JSON(500, map[string]string{"error": appErr.Message}) } } else { ctx.JSON(500, map[string]string{"error": "安装失败: " + err.Error()}) } return } // 安装成功 if installTarget == "kubeconfig" && k8sURL != "" && token != "" { ctx.Flash.Success("应用已成功安装到自定义位置") } else { ctx.Flash.Success("应用已成功安装到默认位置") } // 优先根据表单的 redirect_to 回跳(前端会带上当前路径) if redirectTo := ctx.FormString("redirect_to"); redirectTo != "" { ctx.RedirectToCurrentSite(redirectTo) } else { ctx.Redirect(setting.AppSubURL + "/user/settings/appstore") } } // AppStoreUpdate handles app update func AppStoreUpdate(ctx *context.Context) { // 检查用户登录状态 if !ctx.IsSigned { ctx.JSON(401, map[string]string{"error": "未登录"}) return } if ctx.Req.Method != "POST" { ctx.JSON(405, map[string]string{"error": "Method Not Allowed"}) return } // 解析表单数据 appID := ctx.FormString("app_id") configJSON := ctx.FormString("config") installTarget := ctx.FormString("install_target") k8sURL := ctx.FormString("k8s_url") token := ctx.FormString("k8s_token") if appID == "" { ctx.JSON(400, map[string]string{"error": "应用ID不能为空"}) return } // 权限校验:仅管理员可选择默认位置 if installTarget == "local" && (ctx.Doer == nil || !ctx.Doer.IsAdmin) { ctx.JSON(403, map[string]string{"error": "仅管理员可选择默认位置"}) return } // 创建 manager 并执行更新 manager := appstore.NewManager(ctx, ctx.Doer.ID) if err := manager.UpdateInstalledApp(appID, configJSON, installTarget, k8sURL, token); err != nil { // 根据错误类型返回相应的状态码和消息 if appErr, ok := err.(*appstore.AppStoreError); ok { switch appErr.Code { case "APP_NOT_FOUND", "CONFIG_MERGE_ERROR": ctx.JSON(400, map[string]string{"error": appErr.Message + ": " + appErr.Details}) case "KUBERNETES_UPDATE_ERROR": ctx.JSON(500, map[string]string{"error": appErr.Message + ": " + appErr.Details}) case "NOT_IMPLEMENTED": ctx.JSON(501, map[string]string{"error": appErr.Message}) default: ctx.JSON(500, map[string]string{"error": appErr.Message}) } } else { ctx.JSON(500, map[string]string{"error": "更新失败: " + err.Error()}) } return } // 更新成功 if installTarget == "kubeconfig" && k8sURL != "" && token != "" { ctx.Flash.Success("应用已成功更新到自定义位置") } else { ctx.Flash.Success("应用已成功更新到默认位置") } // 优先根据表单的 redirect_to 回跳(前端会带上当前路径) if redirectTo := ctx.FormString("redirect_to"); redirectTo != "" { ctx.RedirectToCurrentSite(redirectTo) } else { ctx.Redirect(setting.AppSubURL + "/user/settings/appstore") } } // AppStoreUninstall handles app uninstallation func AppStoreUninstall(ctx *context.Context) { // 检查用户登录状态 if !ctx.IsSigned { ctx.JSON(401, map[string]string{"error": "未登录"}) return } if ctx.Req.Method != "POST" { ctx.JSON(405, map[string]string{"error": "Method Not Allowed"}) return } // 解析表单数据 appID := ctx.FormString("app_id") if appID == "" { ctx.JSON(400, map[string]string{"error": "应用ID不能为空"}) return } // 创建 manager 并执行卸载 // UninstallApp 会自动从数据库读取保存的 Kubernetes 凭据判断是外部集群还是本地集群 manager := appstore.NewManager(ctx, ctx.Doer.ID) if err := manager.UninstallApp(appID); err != nil { // 根据错误类型返回相应的状态码和消息 if appErr, ok := err.(*appstore.AppStoreError); ok { switch appErr.Code { case "APP_NOT_FOUND": ctx.JSON(400, map[string]string{"error": appErr.Message + ": " + appErr.Details}) case "KUBERNETES_UNINSTALL_ERROR": ctx.JSON(500, map[string]string{"error": appErr.Message + ": " + appErr.Details}) case "NOT_IMPLEMENTED": ctx.JSON(501, map[string]string{"error": appErr.Message}) default: ctx.JSON(500, map[string]string{"error": appErr.Message}) } } else { ctx.JSON(500, map[string]string{"error": "卸载失败: " + err.Error()}) } return } // 卸载成功 ctx.Flash.Success("应用已成功卸载") // 根据来源页面决定重定向位置 referer := ctx.Req.Header.Get("Referer") if strings.Contains(referer, "/admin/appstore") { ctx.Redirect(setting.AppSubURL + "/admin/appstore") } else { ctx.Redirect(setting.AppSubURL + "/user/settings/appstore") } } // AppStoreResume handles app resuming (restoring original replica count) func AppStoreResume(ctx *context.Context) { // 检查用户登录状态 if !ctx.IsSigned { ctx.JSON(401, map[string]string{"error": "未登录"}) return } if ctx.Req.Method != "POST" { ctx.JSON(405, map[string]string{"error": "Method Not Allowed"}) return } // 解析表单数据 appID := ctx.FormString("app_id") installTarget := ctx.FormString("install_target") k8sURL := ctx.FormString("k8s_url") token := ctx.FormString("k8s_token") if appID == "" { ctx.JSON(400, map[string]string{"error": "应用ID不能为空"}) return } // 创建 manager 并执行恢复 manager := appstore.NewManager(ctx, ctx.Doer.ID) if err := manager.ResumeApp(appID, installTarget, k8sURL, token); err != nil { // 根据错误类型返回相应的状态码和消息 if appErr, ok := err.(*appstore.AppStoreError); ok { switch appErr.Code { case "APP_NOT_FOUND": ctx.JSON(400, map[string]string{"error": appErr.Message + ": " + appErr.Details}) case "KUBERNETES_RESUME_ERROR": ctx.JSON(500, map[string]string{"error": appErr.Message + ": " + appErr.Details}) case "UNSUPPORTED_DEPLOYMENT_TYPE": ctx.JSON(400, map[string]string{"error": appErr.Message}) default: ctx.JSON(500, map[string]string{"error": appErr.Message}) } } else { ctx.JSON(500, map[string]string{"error": "恢复失败: " + err.Error()}) } return } // 恢复成功 if installTarget == "kubeconfig" && k8sURL != "" && token != "" { ctx.Flash.Success("应用已成功在指定位置恢复") } else { ctx.Flash.Success("应用已成功恢复") } // 根据来源页面决定重定向位置 referer := ctx.Req.Header.Get("Referer") if strings.Contains(referer, "/admin/appstore") { ctx.Redirect(setting.AppSubURL + "/admin/appstore") } else { ctx.Redirect(setting.AppSubURL + "/user/settings/appstore") } } // AppStoreStop handles app stopping (setting replicas to 0) func AppStoreStop(ctx *context.Context) { // 检查用户登录状态 if !ctx.IsSigned { ctx.JSON(401, map[string]string{"error": "未登录"}) return } if ctx.Req.Method != "POST" { ctx.JSON(405, map[string]string{"error": "Method Not Allowed"}) return } // 解析表单数据 appID := ctx.FormString("app_id") installTarget := ctx.FormString("install_target") k8sURL := ctx.FormString("k8s_url") token := ctx.FormString("k8s_token") if appID == "" { ctx.JSON(400, map[string]string{"error": "应用ID不能为空"}) return } // 创建 manager 并执行暂停 manager := appstore.NewManager(ctx, ctx.Doer.ID) if err := manager.StopApp(appID, installTarget, k8sURL, token); err != nil { // 根据错误类型返回相应的状态码和消息 if appErr, ok := err.(*appstore.AppStoreError); ok { switch appErr.Code { case "APP_NOT_FOUND": ctx.JSON(400, map[string]string{"error": appErr.Message + ": " + appErr.Details}) case "KUBERNETES_STOP_ERROR": ctx.JSON(500, map[string]string{"error": appErr.Message + ": " + appErr.Details}) case "UNSUPPORTED_DEPLOYMENT_TYPE": ctx.JSON(400, map[string]string{"error": appErr.Message}) default: ctx.JSON(500, map[string]string{"error": appErr.Message}) } } else { ctx.JSON(500, map[string]string{"error": "暂停失败: " + err.Error()}) } return } // 暂停成功 if installTarget == "kubeconfig" && k8sURL != "" && token != "" { ctx.Flash.Success("应用已成功在指定位置暂停") } else { ctx.Flash.Success("应用已成功暂停") } // 根据来源页面决定重定向位置 referer := ctx.Req.Header.Get("Referer") if strings.Contains(referer, "/admin/appstore") { ctx.Redirect(setting.AppSubURL + "/admin/appstore") } else { ctx.Redirect(setting.AppSubURL + "/user/settings/appstore") } } // AppStoreConfigure handles app configuration func AppStoreConfigure(ctx *context.Context) { // TODO: Load app configuration form ctx.Data["Title"] = "App Configuration" ctx.Data["PageIsSettingsAppStore"] = true ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) ctx.HTML(http.StatusOK, tplSettingsAppStore) } // AppStoreConfigurePost handles app configuration form submission func AppStoreConfigurePost(ctx *context.Context) { // 检查用户登录状态 if !ctx.IsSigned { ctx.JSON(401, map[string]string{"error": "未登录"}) return } // TODO: Handle configuration form submission ctx.Flash.Success("App configuration feature coming soon") if redirectTo := ctx.FormString("redirect_to"); redirectTo != "" { ctx.RedirectToCurrentSite(redirectTo) } else { ctx.Redirect(setting.AppSubURL + "/user/settings/appstore") } } // 添加应用API func AddAppAPI(ctx *context.Context) { // 检查 CSRF token if !ctx.IsSigned { ctx.JSON(401, map[string]string{"error": "未登录"}) return } manager := appstore.NewManager(ctx, ctx.Doer.ID) if ctx.Req.Method != "POST" { ctx.JSON(405, map[string]string{"error": "Method Not Allowed"}) return } defer ctx.Req.Body.Close() jsonBytes, err := io.ReadAll(ctx.Req.Body) if err != nil { ctx.JSON(400, map[string]string{"error": "读取请求体失败"}) return } if err := manager.AddAppFromJSON(jsonBytes); err != nil { ctx.JSON(400, map[string]string{"error": err.Error()}) return } ctx.JSON(200, map[string]string{"msg": "ok"}) } // AppStoreDelete handles app deletion from database func AppStoreDelete(ctx *context.Context) { // 检查用户登录状态 if !ctx.IsSigned { ctx.JSON(401, map[string]string{"error": "未登录"}) return } if ctx.Req.Method != "POST" { ctx.JSON(405, map[string]string{"error": "Method Not Allowed"}) return } // 从路径参数获取应用ID appID := ctx.PathParam("app_id") if appID == "" { ctx.JSON(400, map[string]string{"error": "应用ID不能为空"}) return } // 创建 manager 并执行删除 manager := appstore.NewManager(ctx, ctx.Doer.ID) if err := manager.RemoveApp(appID); err != nil { // 根据错误类型返回相应的状态码和消息 if appErr, ok := err.(*appstore.AppStoreError); ok { switch appErr.Code { case "APP_NOT_FOUND": ctx.JSON(400, map[string]string{"error": appErr.Message + ": " + appErr.Details}) case "DATABASE_ERROR": ctx.JSON(500, map[string]string{"error": appErr.Message + ": " + appErr.Details}) default: ctx.JSON(500, map[string]string{"error": appErr.Message}) } } else { ctx.JSON(500, map[string]string{"error": "删除失败: " + err.Error()}) } return } // 删除成功 ctx.Flash.Success("应用已成功删除") // 根据来源页面决定重定向位置 referer := ctx.Req.Header.Get("Referer") if strings.Contains(referer, "/admin/appstore") { ctx.Redirect(setting.AppSubURL + "/admin/appstore") } else { ctx.Redirect(setting.AppSubURL + "/user/settings/appstore") } }