修改了应用商店安装位置的选项

修复了应用商店的用户登录
This commit is contained in:
panshuxiao
2025-08-26 16:55:49 +08:00
repo.diff.parent 85591ff46d
repo.diff.commit c672ed12cf
repo.diff.stats_desc%!(EXTRA int=6, int=155, int=217)

repo.diff.view_file

@@ -1,4 +1,4 @@
FROM golang:1.23 AS builder
FROM golang:1.24 AS builder
WORKDIR /workspace
@@ -14,7 +14,8 @@ ENV HTTP_PROXY=""
ENV HTTPS_PROXY=""
ENV http_proxy=""
ENV https_proxy=""
ENV GOPROXY=https://goproxy.cn,direct
ENV GOPROXY=https://goproxy.io,direct
ENV GOSUMDB=sum.golang.org
# 下载依赖
RUN go mod download

repo.diff.view_file

@@ -1,51 +0,0 @@
package client
import (
"context"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
appsv1 "code.gitea.io/gitea/modules/k8s/api/devcontainer/v1"
)
// DevStarClient 提供操作 DevContainerApp 资源的方法
type DevStarClient struct {
client client.Client
}
// NewDevStarClient 创建一个新的客户端
func NewDevStarClient(c client.Client) *DevStarClient {
return &DevStarClient{
client: c,
}
}
// GetDevContainerApp 获取 DevContainerApp 资源
func (c *DevStarClient) GetDevContainerApp(ctx context.Context, name, namespace string) (*appsv1.DevcontainerApp, error) {
app := &appsv1.DevcontainerApp{}
err := c.client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, app)
return app, err
}
// CreateDevContainerApp 创建 DevContainerApp 资源
func (c *DevStarClient) CreateDevContainerApp(ctx context.Context, app *appsv1.DevcontainerApp) error {
return c.client.Create(ctx, app)
}
// UpdateDevContainerApp 更新 DevContainerApp 资源
func (c *DevStarClient) UpdateDevContainerApp(ctx context.Context, app *appsv1.DevcontainerApp) error {
return c.client.Update(ctx, app)
}
// DeleteDevContainerApp 删除 DevContainerApp 资源
func (c *DevStarClient) DeleteDevContainerApp(ctx context.Context, app *appsv1.DevcontainerApp) error {
return c.client.Delete(ctx, app)
}
// ListDevContainerApps 列出 DevContainerApp 资源
func (c *DevStarClient) ListDevContainerApps(ctx context.Context, namespace string) (*appsv1.DevcontainerAppList, error) {
list := &appsv1.DevcontainerAppList{}
err := c.client.List(ctx, list, client.InNamespace(namespace))
return list, err
}

repo.diff.view_file

@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"strings"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -67,7 +68,21 @@ func GetKubernetesClient(ctx context.Context, kubeconfig []byte, contextName str
}
applyClientDefaults(config)
return dynamicclient.NewForConfig(config)
// 强制跳过 TLS 证书校验(无论 kubeconfig 是否声明 insecure-skip-tls-verify
// 同时清空 CA 配置
config.TLSClientConfig.Insecure = true
config.TLSClientConfig.CAData = nil
config.TLSClientConfig.CAFile = ""
// 尝试创建客户端如果TLS验证失败则自动跳过验证
client, err := dynamicclient.NewForConfig(config)
if err != nil {
// 再次兜底:若识别为 TLS 错误,已 Insecure无需再次设置否则将错误上抛
return nil, fmt.Errorf("failed to create k8s client: %v", err)
}
return client, nil
}
// restConfigFromKubeconfigBytes 基于 kubeconfig 内容构造 *rest.Config支持指定 context为空则使用 current-context
@@ -401,3 +416,36 @@ func ListDevcontainers(ctx context.Context, client dynamic_client.Interface, opt
}
return devcontainerList, nil
}
// isTLSCertificateError 检查错误是否是TLS证书验证错误
func isTLSCertificateError(err error) bool {
if err == nil {
return false
}
errStr := err.Error()
// 检查常见的TLS证书验证错误尽量宽松覆盖更多 x509 报错文案)
tlsErrorPatterns := []string{
"tls: failed to verify certificate",
"x509:",
"x509: certificate",
"cannot validate certificate",
"doesn't contain any IP SANs",
"certificate is valid for",
"certificate signed by unknown authority",
"unknown authority",
"self-signed certificate",
"certificate has expired",
"certificate is not valid",
"invalid certificate",
}
for _, pattern := range tlsErrorPatterns {
if strings.Contains(errStr, pattern) {
return true
}
}
return false
}

repo.diff.view_file

@@ -9,6 +9,7 @@ import (
"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"
@@ -23,6 +24,7 @@ const (
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)
}
@@ -222,6 +224,12 @@ func AppStoreInstall(ctx *context.Context) {
return
}
// 权限校验:仅管理员可选择默认位置
if installTarget == "local" && (ctx.Doer == nil || !ctx.Doer.IsAdmin) {
ctx.JSON(403, map[string]string{"error": "仅管理员可选择默认位置"})
return
}
// 创建 manager 并执行安装
manager := appstore.NewManager(ctx)
if err := manager.InstallApp(appID, configJSON, installTarget, kubeconfig, kubeconfigContext); err != nil {
@@ -245,7 +253,7 @@ func AppStoreInstall(ctx *context.Context) {
// 安装成功
if installTarget == "kubeconfig" && kubeconfig != "" {
ctx.Flash.Success("应用已成功安装到外部 Kubernetes 集群")
ctx.Flash.Success("应用已成功安装到指定位置")
} else {
ctx.Flash.Success("应用配置已准备完成,本地安装功能开发中")
}
@@ -294,7 +302,7 @@ func AppStoreUninstall(ctx *context.Context) {
// 卸载成功
if installTarget == "kubeconfig" && kubeconfig != "" {
ctx.Flash.Success("应用已成功从外部 Kubernetes 集群卸载")
ctx.Flash.Success("应用已成功从指定位置卸载")
} else {
ctx.Flash.Success("本地卸载功能开发中")
}
@@ -307,6 +315,7 @@ 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)
}

repo.diff.view_file

@@ -702,6 +702,16 @@ func registerWebRoutes(m *web.Router) {
m.Get("", user_setting.BlockedUsers)
m.Post("", web.Bind(forms.BlockUserForm{}), user_setting.BlockedUsersPost)
})
// appstore
m.Group("/appstore", func() {
m.Get("", user_setting.AppStore)
m.Get("/api/{action}", user_setting.AppStoreAPI)
m.Post("/api/add", user_setting.AddAppAPI)
m.Post("/install/{app_id}", user_setting.AppStoreInstall)
m.Post("/uninstall/{app_id}", user_setting.AppStoreUninstall)
m.Get("/configure/{app_id}", user_setting.AppStoreConfigure)
m.Post("/configure/{app_id}", user_setting.AppStoreConfigurePost)
})
}, reqSignIn, ctxDataSet("PageIsUserSettings", true, "EnablePackages", setting.Packages.Enabled, "EnableNotifyMail", setting.Service.EnableNotifyMail))
m.Group("/user", func() {
@@ -1697,14 +1707,4 @@ func registerWebRoutes(m *web.Router) {
defer routing.RecordFuncInfo(ctx, routing.GetFuncInfo(ctx.NotFound, "WebNotFound"))()
ctx.NotFound(nil)
})
m.Group("/user/settings/appstore", func() {
m.Get("", user_setting.AppStore)
m.Get("/api/{action}", user_setting.AppStoreAPI)
m.Post("/api/add", user_setting.AddAppAPI)
m.Post("/install/{app_id}", user_setting.AppStoreInstall)
m.Post("/uninstall/{app_id}", user_setting.AppStoreUninstall)
m.Get("/configure/{app_id}", user_setting.AppStoreConfigure)
m.Post("/configure/{app_id}", user_setting.AppStoreConfigurePost)
})
}

repo.diff.view_file

@@ -1,4 +1,5 @@
{{template "user/settings/layout_head" .}}
{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings appstore")}}
<meta name="_csrf" content="{{.CsrfToken}}">
<style>
/* 应用商店卡片样式 - 强制覆盖 */
@@ -143,13 +144,7 @@
<button class="ui button" id="btn-source-devstar">DevStar 应用商店</button>
</div>
</div>
<div>
<div class="ui basic buttons">
<button class="ui button" onclick="openInstallTargetModal()">
<i class="server icon"></i> 安装位置
</button>
</div>
</div>
<div></div>
</div>
</h2>
</div>
@@ -247,6 +242,37 @@
<div class="description">
<form class="ui form" id="install-form">
<!-- Configuration fields will be dynamically generated here -->
<div id="install-dynamic-fields"></div>
<h4 class="ui dividing header" style="margin-top:1em;">安装位置</h4>
<div id="install-target-inline">
<div class="grouped fields">
<label>选择安装位置</label>
{{if .IsAdmin}}
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="installTargetRadio" value="local" checked>
<label>默认位置</label>
</div>
</div>
{{end}}
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="installTargetRadio" value="kubeconfig" {{if not .IsAdmin}}checked{{end}}>
<label>自定义位置Kubeconfig</label>
</div>
</div>
</div>
<div id="kubeconfig-fields" style="display:none;">
<div class="field">
<label>Kubeconfig粘贴内容</label>
<textarea id="kubeconfig-content" rows="8" placeholder="粘贴 kubeconfig 内容"></textarea>
</div>
<div class="field">
<label>Context 名称(可选)</label>
<input type="text" id="kubeconfig-context" placeholder="不填则使用 current-context">
</div>
</div>
</div>
</form>
</div>
</div>
@@ -319,42 +345,7 @@
</div>
<!-- 安装位置设置 Modal -->
<div class="ui modal" id="install-target-modal">
<div class="header">安装位置</div>
<div class="content">
<div class="ui form">
<div class="grouped fields">
<label>选择安装位置</label>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="installTargetRadio" value="local" checked>
<label>本机(默认)</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="installTargetRadio" value="kubeconfig">
<label>外部集群Kubeconfig</label>
</div>
</div>
</div>
<div id="kubeconfig-fields" style="display:none;">
<div class="field">
<label>Kubeconfig粘贴内容</label>
<textarea id="kubeconfig-content" rows="8" placeholder="粘贴 kubeconfig 内容"></textarea>
</div>
<div class="field">
<label>Context 名称(可选)</label>
<input type="text" id="kubeconfig-context" placeholder="不填则使用 current-context">
</div>
</div>
</div>
</div>
<div class="actions">
<div class="ui button" onclick="closeInstallTargetModal()">取消</div>
<div class="ui primary button" onclick="saveInstallTarget()">保存</div>
</div>
</div>
<!-- 已合并到安装窗口,移除原独立 Modal -->
<script>
let allApps = [];
@@ -400,75 +391,34 @@ function setupSourceToggle() {
}
function setupInstallTargetUI() {
// 使用原生JavaScript初始化radio button事件
const radioButtons = document.querySelectorAll('#install-target-modal input[type="radio"]');
// 使用原生JavaScript初始化radio button事件(安装窗口内)
const radioButtons = document.querySelectorAll('#install-modal input[name="installTargetRadio"]');
radioButtons.forEach(radio => {
radio.addEventListener('change', function() {
const kubeconfigFields = document.getElementById('kubeconfig-fields');
const kubeconfigFields = document.querySelector('#install-modal #kubeconfig-fields');
if (kubeconfigFields) {
kubeconfigFields.style.display = (this.value === 'kubeconfig') ? 'block' : 'none';
}
// 同步到原有全局变量,保持功能不变
installTarget = this.value;
});
});
}
function openInstallTargetModal() {
// 预填当前状态
const radios = document.getElementsByName('installTargetRadio');
for (const r of radios) {
r.checked = (r.value === installTarget);
// kubeconfig 内容变化时,同步到全局变量
const kcContentEl = document.querySelector('#install-modal #kubeconfig-content');
if (kcContentEl) {
kcContentEl.addEventListener('input', function() {
installKubeconfigContent = this.value.trim();
});
}
const kubeconfigFields = document.getElementById('kubeconfig-fields');
if (kubeconfigFields) {
kubeconfigFields.style.display = (installTarget === 'kubeconfig') ? 'block' : 'none';
}
const kubeconfigContent = document.getElementById('kubeconfig-content');
const kubeconfigContext = document.getElementById('kubeconfig-context');
if (kubeconfigContent) {
kubeconfigContent.value = installKubeconfigContent || '';
}
if (kubeconfigContext) {
kubeconfigContext.value = installKubeconfigContext || '';
}
// 显示modal
const modal = document.getElementById('install-target-modal');
if (modal) {
modal.style.display = 'block';
const kcContextEl = document.querySelector('#install-modal #kubeconfig-context');
if (kcContextEl) {
kcContextEl.addEventListener('input', function() {
installKubeconfigContext = this.value.trim();
});
}
}
function closeInstallTargetModal() {
// 隐藏modal
const modal = document.getElementById('install-target-modal');
if (modal) {
modal.style.display = 'none';
}
}
function saveInstallTarget() {
const radios = document.getElementsByName('installTargetRadio');
let selected = 'local';
for (const r of radios) {
if (r.checked) { selected = r.value; break; }
}
installTarget = selected;
if (installTarget === 'kubeconfig') {
installKubeconfigContent = document.getElementById('kubeconfig-content').value.trim();
installKubeconfigContext = document.getElementById('kubeconfig-context').value.trim();
if (!installKubeconfigContent) {
alert('请输入 kubeconfig 内容');
return;
}
} else {
installKubeconfigContent = '';
installKubeconfigContext = '';
}
closeInstallTargetModal();
}
// 旧的安装位置弹窗相关函数已移除openInstallTargetModal、closeInstallTargetModal、saveInstallTarget
function setupEventListeners() {
// Category filter
@@ -765,12 +715,11 @@ function showInstallModal(appId) {
// Generate configuration form
const form = document.getElementById('install-form');
form.innerHTML = '';
const dyn = document.getElementById('install-dynamic-fields');
dyn.innerHTML = '';
// 兼容后端 config/schema 为 null 的情况
let configSchema = null;
let configDefaults = null;
if (app.config && app.config.schema) {
configSchema = app.config.schema;
configDefaults = app.config.default || {};
@@ -778,11 +727,8 @@ function showInstallModal(appId) {
configSchema = app.Config.Schema;
configDefaults = app.Config.Default || {};
}
// 新增健壮性处理
if (!configSchema || typeof configSchema !== 'object') {
// 默认渲染一个端口输入框
form.innerHTML = `
dyn.innerHTML = `
<div class="field">
<label>port <span style="color: red;">*</span></label>
<input type="number" name="port" value="80" required>
@@ -790,7 +736,7 @@ function showInstallModal(appId) {
`;
} else {
Object.entries(configSchema).forEach(([key, config]) => {
if (!config) return; // 防御
if (!config) return;
let field = document.createElement('div');
field.className = 'field';
let inputHtml = '';
@@ -823,25 +769,27 @@ function showInstallModal(appId) {
${config.description ? `<div class="ui small text">${config.description}</div>` : ''}
${inputHtml}
`;
form.appendChild(field);
dyn.appendChild(field);
});
// 使用原生JavaScript初始化组件
setTimeout(() => {
// 初始化dropdown
const dropdowns = form.querySelectorAll('.ui.dropdown');
dropdowns.forEach(dropdown => {
// 这里可以添加dropdown的初始化逻辑
});
// 初始化checkbox
dropdowns.forEach(() => {});
const checkboxes = form.querySelectorAll('.ui.checkbox input[type="checkbox"]');
checkboxes.forEach(checkbox => {
// 这里可以添加checkbox的初始化逻辑
});
checkboxes.forEach(() => {});
}, 100);
}
// 追加安装位置区块已保留在模板中,这里只同步状态
const radios = document.querySelectorAll('#install-modal input[name="installTargetRadio"]');
radios.forEach(r => { r.checked = (r.value === installTarget); });
const kubeconfigFields = document.querySelector('#install-modal #kubeconfig-fields');
if (kubeconfigFields) {
kubeconfigFields.style.display = (installTarget === 'kubeconfig') ? 'block' : 'none';
}
const kcContent = document.querySelector('#install-modal #kubeconfig-content');
const kcContext = document.querySelector('#install-modal #kubeconfig-context');
if (kcContent) kcContent.value = installKubeconfigContent || '';
if (kcContext) kcContext.value = installKubeconfigContext || '';
// 显示modal
const modal = document.getElementById('install-modal');
if (modal) {
modal.style.display = 'block';
@@ -873,14 +821,10 @@ function showInstallModalFromDetails() {
function installApp() {
if (!currentApp) return;
// Collect form data
const form = document.getElementById('install-form');
const formData = new FormData(form);
const config = {};
for (let [key, value] of formData.entries()) {
// Handle checkbox values
const input = form.querySelector(`[name="${key}"]`);
if (input && input.type === 'checkbox') {
config[key] = input.checked;
@@ -890,86 +834,72 @@ function installApp() {
config[key] = value;
}
}
// Send installation request
const appId = currentApp.id || currentApp.app_id;
// 根据用户选择的部署分类设置 Deploy.Type
const currentDeploymentFilter = document.querySelector('.deployment-filter .active');
const deploymentType = currentDeploymentFilter?.getAttribute('data-deployment');
// 从安装窗口读取安装位置
const selectedTarget = document.querySelector('#install-modal input[name="installTargetRadio"]:checked');
installTarget = selectedTarget ? selectedTarget.value : 'local';
if (installTarget === 'kubeconfig') {
// 外部集群安装:设置为 kubernetes
const kcContentEl = document.querySelector('#install-modal #kubeconfig-content');
const kcContextEl = document.querySelector('#install-modal #kubeconfig-context');
installKubeconfigContent = kcContentEl ? kcContentEl.value.trim() : '';
installKubeconfigContext = kcContextEl ? kcContextEl.value.trim() : '';
if (!installKubeconfigContent) {
alert('请输入 kubeconfig 内容');
return;
}
config.deploy = { type: 'kubernetes' };
} else {
// 本地安装:根据当前选择的部署分类设置
if (deploymentType === 'docker') {
config.deploy = { type: 'docker' };
} else if (deploymentType === 'kubernetes') {
config.deploy = { type: 'kubernetes' };
} else {
// 如果选择"all",根据应用自身类型决定
if (currentApp.deployment_type === 'both') {
// 对于支持两种部署方式的应用,默认选择 Docker
config.deploy = { type: 'docker' };
} else {
config.deploy = { type: currentApp.deployment_type || 'docker' };
}
}
}
console.log('Installing app:', appId, 'with config:', config, 'target:', installTarget);
// Create a form and submit it to the install endpoint
const installForm = document.createElement('form');
installForm.method = 'POST';
installForm.action = `/user/settings/appstore/install/${appId}`;
// Add CSRF token
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = '_csrf';
csrfInput.value = document.querySelector('meta[name="_csrf"]')?.content || '';
installForm.appendChild(csrfInput);
// Add app ID
const appIdInput = document.createElement('input');
appIdInput.type = 'hidden';
appIdInput.name = 'app_id';
appIdInput.value = appId;
installForm.appendChild(appIdInput);
// Add config as JSON
const configInput = document.createElement('input');
configInput.type = 'hidden';
configInput.name = 'config';
configInput.value = JSON.stringify(config);
installForm.appendChild(configInput);
// Add install target
const targetInput = document.createElement('input');
targetInput.type = 'hidden';
targetInput.name = 'install_target';
targetInput.value = installTarget;
installForm.appendChild(targetInput);
if (installTarget === 'kubeconfig') {
const kcInput = document.createElement('input');
kcInput.type = 'hidden';
kcInput.name = 'kubeconfig';
kcInput.value = installKubeconfigContent;
installForm.appendChild(kcInput);
const kctxInput = document.createElement('input');
kctxInput.type = 'hidden';
kctxInput.name = 'kubeconfig_context';
kctxInput.value = installKubeconfigContext;
installForm.appendChild(kctxInput);
}
document.body.appendChild(installForm);
installForm.submit();
closeInstallModal();
}
@@ -989,6 +919,7 @@ function closeAddAppModal() {
}
}
async function submitAddApp() {
const jsonText = document.getElementById('add-app-json').value.trim();
let appData;
try {