Compare commits

...

3 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
31d0673489 Add test for reverse proxy fields
Co-authored-by: mserico <140243407+mserico@users.noreply.github.com>
2026-02-15 18:19:51 +00:00
copilot-swe-agent[bot]
7adb279460 Add reverse proxy fields to Application model and UI
Co-authored-by: mserico <140243407+mserico@users.noreply.github.com>
2026-02-15 18:14:30 +00:00
copilot-swe-agent[bot]
20ac6e209f Initial plan 2026-02-15 18:02:46 +00:00
7 changed files with 2208 additions and 2938 deletions

View File

@@ -154,6 +154,13 @@ type Application struct {
FailedSigninLimit int `json:"failedSigninLimit"`
FailedSigninFrozenTime int `json:"failedSigninFrozenTime"`
CodeResendTimeout int `json:"codeResendTimeout"`
// Reverse proxy fields
Domain string `xorm:"varchar(100)" json:"domain"`
OtherDomains []string `xorm:"varchar(1000)" json:"otherDomains"`
UpstreamHost string `xorm:"varchar(100)" json:"upstreamHost"`
SslMode string `xorm:"varchar(100)" json:"sslMode"`
SslCert string `xorm:"varchar(100)" json:"sslCert"`
}
func GetApplicationCount(owner, field, value string) (int64, error) {

View File

@@ -0,0 +1,63 @@
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"encoding/json"
"testing"
)
func TestApplicationReverseProxyFields(t *testing.T) {
// Test that reverse proxy fields can be marshaled and unmarshaled
app := &Application{
Owner: "admin",
Name: "test-app",
Domain: "blog.example.com",
OtherDomains: []string{"www.blog.example.com", "blog2.example.com"},
UpstreamHost: "localhost:8080",
SslMode: "HTTPS Only",
SslCert: "cert-test",
}
// Marshal to JSON
data, err := json.Marshal(app)
if err != nil {
t.Fatalf("Failed to marshal application: %v", err)
}
// Unmarshal from JSON
var app2 Application
err = json.Unmarshal(data, &app2)
if err != nil {
t.Fatalf("Failed to unmarshal application: %v", err)
}
// Verify fields
if app2.Domain != app.Domain {
t.Errorf("Domain mismatch: expected %s, got %s", app.Domain, app2.Domain)
}
if len(app2.OtherDomains) != len(app.OtherDomains) {
t.Errorf("OtherDomains length mismatch: expected %d, got %d", len(app.OtherDomains), len(app2.OtherDomains))
}
if app2.UpstreamHost != app.UpstreamHost {
t.Errorf("UpstreamHost mismatch: expected %s, got %s", app.UpstreamHost, app2.UpstreamHost)
}
if app2.SslMode != app.SslMode {
t.Errorf("SslMode mismatch: expected %s, got %s", app.SslMode, app2.SslMode)
}
if app2.SslCert != app.SslCert {
t.Errorf("SslCert mismatch: expected %s, got %s", app.SslCert, app2.SslCert)
}
}

1
web/.gitignore vendored
View File

@@ -21,3 +21,4 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json

View File

@@ -1373,6 +1373,68 @@ class ApplicationEditPage extends React.Component {
</Col>
</Row>
</React.Fragment>
)}
{this.state.activeMenuKey === "reverse-proxy" && (
<React.Fragment>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:Domain"), i18next.t("application:Domain - Tooltip"))} :
</Col>
<Col span={21} >
<Input value={this.state.application.domain} placeholder="e.g., blog.example.com" onChange={e => {
this.updateApplicationField("domain", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:Other domains"), i18next.t("application:Other domains - Tooltip"))} :
</Col>
<Col span={21} >
<UrlTable
title={i18next.t("application:Other domains")}
table={this.state.application.otherDomains}
onUpdateTable={(value) => {this.updateApplicationField("otherDomains", value);}}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:Upstream host"), i18next.t("application:Upstream host - Tooltip"))} :
</Col>
<Col span={21} >
<Input value={this.state.application.upstreamHost} placeholder="e.g., localhost:8080 or 192.168.1.100:3000" onChange={e => {
this.updateApplicationField("upstreamHost", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:SSL mode"), i18next.t("application:SSL mode - Tooltip"))} :
</Col>
<Col span={21} >
<Select virtual={false} style={{width: "100%"}} value={this.state.application.sslMode} onChange={(value => {this.updateApplicationField("sslMode", value);})}>
<Option value="">{i18next.t("general:None")}</Option>
<Option value="HTTP">HTTP</Option>
<Option value="HTTPS and HTTP">HTTPS and HTTP</Option>
<Option value="HTTPS Only">HTTPS Only</Option>
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 3}>
{Setting.getLabel(i18next.t("application:SSL cert"), i18next.t("application:SSL cert - Tooltip"))} :
</Col>
<Col span={21} >
<Select virtual={false} style={{width: "100%"}} value={this.state.application.sslCert} onChange={(value => {this.updateApplicationField("sslCert", value);})}>
<Option value="">{i18next.t("general:None")}</Option>
{
this.state.certs.map((cert, index) => <Option key={index} value={cert.name}>{cert.name}</Option>)
}
</Select>
</Col>
</Row>
</React.Fragment>
)}</>;
}
@@ -1405,6 +1467,7 @@ class ApplicationEditPage extends React.Component {
{label: i18next.t("application:Providers"), key: "providers"},
{label: i18next.t("application:UI Customization"), key: "ui-customization"},
{label: i18next.t("application:Security"), key: "security"},
{label: i18next.t("application:Reverse Proxy"), key: "reverse-proxy"},
]}
/>
</Header>
@@ -1428,6 +1491,7 @@ class ApplicationEditPage extends React.Component {
<Menu.Item key="providers">{i18next.t("application:Providers")}</Menu.Item>
<Menu.Item key="ui-customization">{i18next.t("application:UI Customization")}</Menu.Item>
<Menu.Item key="security">{i18next.t("application:Security")}</Menu.Item>
<Menu.Item key="reverse-proxy">{i18next.t("application:Reverse Proxy")}</Menu.Item>
</Menu>
</Sider>) : null
}

View File

@@ -168,7 +168,18 @@
"Use Email as NameID": "Use Email as NameID",
"Use Email as NameID - Tooltip": "Use Email as NameID",
"Vertical": "Vertical",
"You are unexpected to see this prompt page": "You are unexpected to see this prompt page"
"You are unexpected to see this prompt page": "You are unexpected to see this prompt page",
"Reverse Proxy": "Reverse Proxy",
"Domain": "Domain",
"Domain - Tooltip": "The public-facing domain for this application (e.g., blog.example.com)",
"Other domains": "Other domains",
"Other domains - Tooltip": "Additional domains that should also route to this application",
"Upstream host": "Upstream host",
"Upstream host - Tooltip": "The upstream backend address to forward requests to (e.g., localhost:8080 or 192.168.1.100:3000)",
"SSL mode": "SSL mode",
"SSL mode - Tooltip": "Choose the SSL/TLS mode for this application: HTTP, HTTPS and HTTP, or HTTPS Only",
"SSL cert": "SSL cert",
"SSL cert - Tooltip": "The SSL certificate to use for TLS termination"
},
"cert": {
"Bit size": "Bit size",
@@ -570,7 +581,7 @@
"Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page"
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page"
},
"home": {
"New users past 30 days": "New users past 30 days",

View File

@@ -168,7 +168,18 @@
"Use Email as NameID": "使用邮箱作为NameID",
"Use Email as NameID - Tooltip": "使用邮箱作为NameID",
"Vertical": "垂直",
"You are unexpected to see this prompt page": "错误:该提醒页面不应出现"
"You are unexpected to see this prompt page": "错误:该提醒页面不应出现",
"Reverse Proxy": "反向代理",
"Domain": "域名",
"Domain - Tooltip": "此应用程序的公共域名例如blog.example.com",
"Other domains": "其他域名",
"Other domains - Tooltip": "也应路由到此应用程序的其他域名",
"Upstream host": "上游主机",
"Upstream host - Tooltip": "要转发请求的上游后端地址例如localhost:8080 或 192.168.1.100:3000",
"SSL mode": "SSL 模式",
"SSL mode - Tooltip": "为此应用程序选择 SSL/TLS 模式HTTP、HTTPS 和 HTTP 或仅 HTTPS",
"SSL cert": "SSL 证书",
"SSL cert - Tooltip": "用于 TLS 终止的 SSL 证书"
},
"cert": {
"Bit size": "位大小",
@@ -570,7 +581,7 @@
"Physical": "实体组",
"Show all": "显示全部",
"Virtual": "虚拟组",
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -\u003e [Groups] page": "您需要先删除所有子组。您可以在 [组织] -\u003e [群组] 页面左侧的群组树中查看子组"
"You need to delete all subgroups first. You can view the subgroups in the left group tree of the [Organizations] -> [Groups] page": "您需要先删除所有子组。您可以在 [组织] -> [群组] 页面左侧的群组树中查看子组"
},
"home": {
"New users past 30 days": "过去 30 天新增的用户",

File diff suppressed because it is too large Load Diff