mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-07 14:14:44 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a826add87 | |||
| 63611dc932 |
@@ -0,0 +1,80 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"clicd/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type webSSHOriginSettingsRequest struct {
|
||||||
|
Origins []string `json:"origins"`
|
||||||
|
WebSSHAllowedOrigins []string `json:"webssh_allowed_origins"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type webSSHOriginSettingsResponse struct {
|
||||||
|
Origins []string `json:"origins"`
|
||||||
|
CurrentOrigin string `json:"current_origin,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleWebSSHOriginSettings(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: webSSHOriginSettingsStatus(r)})
|
||||||
|
case http.MethodPut:
|
||||||
|
updateWebSSHOriginSettings(w, r)
|
||||||
|
default:
|
||||||
|
jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateWebSSHOriginSettings(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req webSSHOriginSettingsRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid request body"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
origins := req.Origins
|
||||||
|
if len(origins) == 0 && len(req.WebSSHAllowedOrigins) > 0 {
|
||||||
|
origins = req.WebSSHAllowedOrigins
|
||||||
|
}
|
||||||
|
normalized, err := config.NormalizeAllowedOrigins(origins)
|
||||||
|
if err != nil {
|
||||||
|
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
config.AppConfig.WebSSHAllowedOrigins = normalized
|
||||||
|
if err := config.SaveConfig(); err != nil {
|
||||||
|
jsonResponse(w, http.StatusInternalServerError, APIResponse{Success: false, Message: "Save Origin allowlist failed"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
auditRequest(r, "settings.webssh_origins", "WebSSH Origin", "origins="+strings.Join(normalized, ","), true, "")
|
||||||
|
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "Origin allowlist saved", Data: webSSHOriginSettingsStatus(r)})
|
||||||
|
}
|
||||||
|
|
||||||
|
func webSSHOriginSettingsStatus(r *http.Request) webSSHOriginSettingsResponse {
|
||||||
|
origins := config.AppConfig.WebSSHAllowedOrigins
|
||||||
|
if origins == nil {
|
||||||
|
origins = []string{}
|
||||||
|
}
|
||||||
|
return webSSHOriginSettingsResponse{
|
||||||
|
Origins: origins,
|
||||||
|
CurrentOrigin: requestOrigin(r),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestOrigin(r *http.Request) string {
|
||||||
|
host := strings.TrimSpace(r.Host)
|
||||||
|
if host == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
scheme := "http"
|
||||||
|
if r.TLS != nil {
|
||||||
|
scheme = "https"
|
||||||
|
}
|
||||||
|
if forwarded := strings.TrimSpace(r.Header.Get("X-Forwarded-Proto")); forwarded != "" {
|
||||||
|
scheme = strings.ToLower(strings.Split(forwarded, ",")[0])
|
||||||
|
}
|
||||||
|
return scheme + "://" + host
|
||||||
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"strings"
|
"clicd/internal/config"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
@@ -17,19 +16,6 @@ var upgrader = websocket.Upgrader{
|
|||||||
if origin == "" {
|
if origin == "" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
originURL, err := url.Parse(origin)
|
return config.IsOriginAllowed(origin, r.Host)
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
originHost := strings.ToLower(stripPort(originURL.Host))
|
|
||||||
requestHost := strings.ToLower(stripPort(r.Host))
|
|
||||||
return originHost != "" && originHost == requestHost
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func stripPort(host string) string {
|
|
||||||
if parsedHost, _, err := net.SplitHostPort(host); err == nil {
|
|
||||||
return parsedHost
|
|
||||||
}
|
|
||||||
return strings.Trim(host, "[]")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -363,6 +363,7 @@ type ClicdConfig struct {
|
|||||||
Snapshots []Snapshot `json:"snapshots"`
|
Snapshots []Snapshot `json:"snapshots"`
|
||||||
PublicIPv4Pool []PublicIPv4Assignment `json:"public_ipv4_pool"`
|
PublicIPv4Pool []PublicIPv4Assignment `json:"public_ipv4_pool"`
|
||||||
PublicIPv6Prefixes []PublicIPv6Prefix `json:"public_ipv6_prefixes"`
|
PublicIPv6Prefixes []PublicIPv6Prefix `json:"public_ipv6_prefixes"`
|
||||||
|
WebSSHAllowedOrigins []string `json:"webssh_allowed_origins"`
|
||||||
SecurityAutoShutdown bool `json:"security_auto_shutdown"`
|
SecurityAutoShutdown bool `json:"security_auto_shutdown"`
|
||||||
Language string `json:"language"`
|
Language string `json:"language"`
|
||||||
SSL SSLConfig `json:"ssl"`
|
SSL SSLConfig `json:"ssl"`
|
||||||
@@ -480,23 +481,24 @@ func InitConfig() (*ClicdConfig, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AppConfig = &ClicdConfig{
|
AppConfig = &ClicdConfig{
|
||||||
AdminUser: adminUser,
|
AdminUser: adminUser,
|
||||||
AdminPassHash: string(hash),
|
AdminPassHash: string(hash),
|
||||||
JWTSecret: jwtSecret,
|
JWTSecret: jwtSecret,
|
||||||
Port: 8999,
|
Port: 8999,
|
||||||
DataDir: dataDir,
|
DataDir: dataDir,
|
||||||
Containers: []Container{},
|
Containers: []Container{},
|
||||||
NextContainerID: 1,
|
NextContainerID: 1,
|
||||||
NextVNCPort: 5900,
|
NextVNCPort: 5900,
|
||||||
NextSSHPort: 22000,
|
NextSSHPort: 22000,
|
||||||
SetupComplete: false,
|
SetupComplete: false,
|
||||||
SubUsers: []SubUser{},
|
SubUsers: []SubUser{},
|
||||||
AuditLogs: []AuditLog{},
|
AuditLogs: []AuditLog{},
|
||||||
Tasks: []SavedTask{},
|
Tasks: []SavedTask{},
|
||||||
LoginLogs: []SavedLoginLog{},
|
LoginLogs: []SavedLoginLog{},
|
||||||
Snapshots: []Snapshot{},
|
Snapshots: []Snapshot{},
|
||||||
PublicIPv4Pool: []PublicIPv4Assignment{},
|
PublicIPv4Pool: []PublicIPv4Assignment{},
|
||||||
PublicIPv6Prefixes: []PublicIPv6Prefix{},
|
PublicIPv6Prefixes: []PublicIPv6Prefix{},
|
||||||
|
WebSSHAllowedOrigins: []string{},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := SaveConfig(); err != nil {
|
if err := SaveConfig(); err != nil {
|
||||||
@@ -555,6 +557,13 @@ func normalizeConfigDefaults(dataDir string) bool {
|
|||||||
AppConfig.PublicIPv6Prefixes = make([]PublicIPv6Prefix, 0)
|
AppConfig.PublicIPv6Prefixes = make([]PublicIPv6Prefix, 0)
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
|
if AppConfig.WebSSHAllowedOrigins == nil {
|
||||||
|
AppConfig.WebSSHAllowedOrigins = make([]string, 0)
|
||||||
|
changed = true
|
||||||
|
} else if normalized, err := NormalizeAllowedOrigins(AppConfig.WebSSHAllowedOrigins); err == nil && strings.Join(normalized, "\n") != strings.Join(AppConfig.WebSSHAllowedOrigins, "\n") {
|
||||||
|
AppConfig.WebSSHAllowedOrigins = normalized
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
if AppConfig.SubUsers == nil {
|
if AppConfig.SubUsers == nil {
|
||||||
AppConfig.SubUsers = make([]SubUser, 0)
|
AppConfig.SubUsers = make([]SubUser, 0)
|
||||||
changed = true
|
changed = true
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NormalizeAllowedOrigin accepts a browser Origin value such as
|
||||||
|
// https://www.example.com and returns a canonical form for exact matching.
|
||||||
|
func NormalizeAllowedOrigin(value string) (string, error) {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
u, err := url.Parse(value)
|
||||||
|
if err != nil || u.Scheme == "" || u.Host == "" {
|
||||||
|
return "", fmt.Errorf("Origin must include scheme and host: %s", value)
|
||||||
|
}
|
||||||
|
scheme := strings.ToLower(u.Scheme)
|
||||||
|
if scheme != "http" && scheme != "https" {
|
||||||
|
return "", fmt.Errorf("Origin scheme must be http or https: %s", value)
|
||||||
|
}
|
||||||
|
if (u.Path != "" && u.Path != "/") || u.RawQuery != "" || u.Fragment != "" {
|
||||||
|
return "", fmt.Errorf("Origin must not include path, query, or fragment: %s", value)
|
||||||
|
}
|
||||||
|
host := normalizeOriginHostPort(u.Host, scheme)
|
||||||
|
if host == "" {
|
||||||
|
return "", fmt.Errorf("Origin host is required: %s", value)
|
||||||
|
}
|
||||||
|
return scheme + "://" + host, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NormalizeAllowedOrigins(values []string) ([]string, error) {
|
||||||
|
result := make([]string, 0, len(values))
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, value := range values {
|
||||||
|
origin, err := NormalizeAllowedOrigin(value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if origin == "" || seen[origin] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[origin] = true
|
||||||
|
result = append(result, origin)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsOriginAllowed(origin string, requestHost string) bool {
|
||||||
|
origin = strings.TrimSpace(origin)
|
||||||
|
if origin == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if isSameRequestOrigin(origin, requestHost) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
normalized, err := NormalizeAllowedOrigin(origin)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if AppConfig == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, allowed := range AppConfig.WebSSHAllowedOrigins {
|
||||||
|
allowed, err := NormalizeAllowedOrigin(allowed)
|
||||||
|
if err == nil && normalized == allowed {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSameRequestOrigin(origin string, requestHost string) bool {
|
||||||
|
u, err := url.Parse(origin)
|
||||||
|
if err != nil || u.Host == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
originHost := normalizeHostOnly(u.Hostname())
|
||||||
|
host := normalizeHostOnly(requestHost)
|
||||||
|
if originHost == "" || host == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if originHost == host {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return isLoopbackHost(originHost) && isLoopbackHost(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeOriginHostPort(raw string, scheme string) string {
|
||||||
|
host := raw
|
||||||
|
port := ""
|
||||||
|
if h, p, err := net.SplitHostPort(raw); err == nil {
|
||||||
|
host = h
|
||||||
|
port = p
|
||||||
|
}
|
||||||
|
host = normalizeHostOnly(host)
|
||||||
|
if host == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if (scheme == "https" && port == "443") || (scheme == "http" && port == "80") {
|
||||||
|
port = ""
|
||||||
|
}
|
||||||
|
if port != "" {
|
||||||
|
return net.JoinHostPort(host, port)
|
||||||
|
}
|
||||||
|
if strings.Contains(host, ":") && net.ParseIP(host) != nil {
|
||||||
|
return "[" + host + "]"
|
||||||
|
}
|
||||||
|
return host
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeHostOnly(raw string) string {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if h, _, err := net.SplitHostPort(raw); err == nil {
|
||||||
|
raw = h
|
||||||
|
}
|
||||||
|
raw = strings.Trim(raw, "[]")
|
||||||
|
if ip := net.ParseIP(raw); ip != nil {
|
||||||
|
return strings.ToLower(ip.String())
|
||||||
|
}
|
||||||
|
return strings.TrimSuffix(strings.ToLower(raw), ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLoopbackHost(host string) bool {
|
||||||
|
if host == "localhost" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
ip := net.ParseIP(host)
|
||||||
|
return ip != nil && ip.IsLoopback()
|
||||||
|
}
|
||||||
@@ -426,6 +426,9 @@ func loadConfigFromDB() (*ClicdConfig, bool, error) {
|
|||||||
if raw := strings.TrimSpace(meta["public_ipv6_prefixes"]); raw != "" {
|
if raw := strings.TrimSpace(meta["public_ipv6_prefixes"]); raw != "" {
|
||||||
_ = json.Unmarshal([]byte(raw), &cfg.PublicIPv6Prefixes)
|
_ = json.Unmarshal([]byte(raw), &cfg.PublicIPv6Prefixes)
|
||||||
}
|
}
|
||||||
|
if raw := strings.TrimSpace(meta["webssh_allowed_origins"]); raw != "" {
|
||||||
|
_ = json.Unmarshal([]byte(raw), &cfg.WebSSHAllowedOrigins)
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.Containers, err = loadContainers(); err != nil {
|
if cfg.Containers, err = loadContainers(); err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
@@ -524,6 +527,7 @@ func saveMeta(tx *sql.Tx) error {
|
|||||||
sslCertificatesJSON, _ := json.Marshal(AppConfig.SSLCertificates)
|
sslCertificatesJSON, _ := json.Marshal(AppConfig.SSLCertificates)
|
||||||
publicIPv4PoolJSON, _ := json.Marshal(AppConfig.PublicIPv4Pool)
|
publicIPv4PoolJSON, _ := json.Marshal(AppConfig.PublicIPv4Pool)
|
||||||
publicIPv6PrefixesJSON, _ := json.Marshal(AppConfig.PublicIPv6Prefixes)
|
publicIPv6PrefixesJSON, _ := json.Marshal(AppConfig.PublicIPv6Prefixes)
|
||||||
|
webSSHAllowedOriginsJSON, _ := json.Marshal(AppConfig.WebSSHAllowedOrigins)
|
||||||
values := map[string]string{
|
values := map[string]string{
|
||||||
"admin_user": AppConfig.AdminUser,
|
"admin_user": AppConfig.AdminUser,
|
||||||
"admin_pass_hash": AppConfig.AdminPassHash,
|
"admin_pass_hash": AppConfig.AdminPassHash,
|
||||||
@@ -540,6 +544,7 @@ func saveMeta(tx *sql.Tx) error {
|
|||||||
"ssl_certificates": string(sslCertificatesJSON),
|
"ssl_certificates": string(sslCertificatesJSON),
|
||||||
"public_ipv4_pool": string(publicIPv4PoolJSON),
|
"public_ipv4_pool": string(publicIPv4PoolJSON),
|
||||||
"public_ipv6_prefixes": string(publicIPv6PrefixesJSON),
|
"public_ipv6_prefixes": string(publicIPv6PrefixesJSON),
|
||||||
|
"webssh_allowed_origins": string(webSSHAllowedOriginsJSON),
|
||||||
"schema_version": "1",
|
"schema_version": "1",
|
||||||
"updated_at": time.Now().Format("2006-01-02 15:04:05"),
|
"updated_at": time.Now().Format("2006-01-02 15:04:05"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"clicd/internal/api"
|
"clicd/internal/api"
|
||||||
@@ -19,7 +17,7 @@ var webFS http.FileSystem
|
|||||||
// corsMiddleware adds CORS headers
|
// corsMiddleware adds CORS headers
|
||||||
func corsMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
func corsMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if origin := r.Header.Get("Origin"); origin != "" && isAllowedOrigin(origin, r.Host) {
|
if origin := r.Header.Get("Origin"); origin != "" && config.IsOriginAllowed(origin, r.Host) {
|
||||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||||
w.Header().Set("Vary", "Origin")
|
w.Header().Set("Vary", "Origin")
|
||||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
@@ -28,7 +26,7 @@ func corsMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key")
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key")
|
||||||
|
|
||||||
if r.Method == http.MethodOptions {
|
if r.Method == http.MethodOptions {
|
||||||
if origin := r.Header.Get("Origin"); origin != "" && !isAllowedOrigin(origin, r.Host) {
|
if origin := r.Header.Get("Origin"); origin != "" && !config.IsOriginAllowed(origin, r.Host) {
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -40,34 +38,6 @@ func corsMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isAllowedOrigin(origin string, requestHost string) bool {
|
|
||||||
u, err := url.Parse(origin)
|
|
||||||
if err != nil || u.Host == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
originHost := normalizeHost(u.Host)
|
|
||||||
host := normalizeHost(requestHost)
|
|
||||||
if originHost == host {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return isLoopbackHost(originHost) && isLoopbackHost(host)
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeHost(host string) string {
|
|
||||||
if h, _, err := net.SplitHostPort(host); err == nil {
|
|
||||||
return strings.ToLower(h)
|
|
||||||
}
|
|
||||||
return strings.ToLower(host)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isLoopbackHost(host string) bool {
|
|
||||||
if host == "localhost" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
ip := net.ParseIP(host)
|
|
||||||
return ip != nil && ip.IsLoopback()
|
|
||||||
}
|
|
||||||
|
|
||||||
// setupRoutes configures API and static routes
|
// setupRoutes configures API and static routes
|
||||||
func setupRoutes(mux *http.ServeMux) {
|
func setupRoutes(mux *http.ServeMux) {
|
||||||
// API routes
|
// API routes
|
||||||
@@ -78,6 +48,7 @@ func setupRoutes(mux *http.ServeMux) {
|
|||||||
mux.HandleFunc("/api/change-username", corsMiddleware(api.AdminMiddleware(api.HandleAdminUsernameChange)))
|
mux.HandleFunc("/api/change-username", corsMiddleware(api.AdminMiddleware(api.HandleAdminUsernameChange)))
|
||||||
mux.HandleFunc("/api/login-logs", corsMiddleware(api.AdminMiddleware(api.HandleLoginLogs)))
|
mux.HandleFunc("/api/login-logs", corsMiddleware(api.AdminMiddleware(api.HandleLoginLogs)))
|
||||||
mux.HandleFunc("/api/ssl", corsMiddleware(api.AdminMiddleware(api.HandleSSLSettings)))
|
mux.HandleFunc("/api/ssl", corsMiddleware(api.AdminMiddleware(api.HandleSSLSettings)))
|
||||||
|
mux.HandleFunc("/api/webssh-origins", corsMiddleware(api.AdminMiddleware(api.HandleWebSSHOriginSettings)))
|
||||||
mux.HandleFunc("/api/containers", corsMiddleware(api.AuthMiddleware(api.SubUserMiddleware(api.HandleContainers))))
|
mux.HandleFunc("/api/containers", corsMiddleware(api.AuthMiddleware(api.SubUserMiddleware(api.HandleContainers))))
|
||||||
mux.HandleFunc("/api/containers/list", corsMiddleware(api.AuthMiddleware(api.SubUserMiddleware(api.HandleContainerListAlias))))
|
mux.HandleFunc("/api/containers/list", corsMiddleware(api.AuthMiddleware(api.SubUserMiddleware(api.HandleContainerListAlias))))
|
||||||
mux.HandleFunc("/api/containers/", corsMiddleware(api.AuthMiddleware(api.SubUserMiddleware(api.HandleSingleContainer))))
|
mux.HandleFunc("/api/containers/", corsMiddleware(api.AuthMiddleware(api.SubUserMiddleware(api.HandleSingleContainer))))
|
||||||
@@ -148,6 +119,7 @@ func setupRoutes(mux *http.ServeMux) {
|
|||||||
mux.HandleFunc("/api/v1/audit-logs", corsMiddleware(api.AuthMiddleware(api.HandleAuditLogs)))
|
mux.HandleFunc("/api/v1/audit-logs", corsMiddleware(api.AuthMiddleware(api.HandleAuditLogs)))
|
||||||
mux.HandleFunc("/api/v1/login-logs", corsMiddleware(api.AuthMiddleware(api.HandleLoginLogs)))
|
mux.HandleFunc("/api/v1/login-logs", corsMiddleware(api.AuthMiddleware(api.HandleLoginLogs)))
|
||||||
mux.HandleFunc("/api/v1/ssl", corsMiddleware(api.AdminMiddleware(api.HandleSSLSettings)))
|
mux.HandleFunc("/api/v1/ssl", corsMiddleware(api.AdminMiddleware(api.HandleSSLSettings)))
|
||||||
|
mux.HandleFunc("/api/v1/webssh-origins", corsMiddleware(api.AdminMiddleware(api.HandleWebSSHOriginSettings)))
|
||||||
mux.HandleFunc("/api/v1/security/alerts", corsMiddleware(api.AuthMiddleware(api.ScopeMiddleware("security:read", api.HandleSecurityAlerts))))
|
mux.HandleFunc("/api/v1/security/alerts", corsMiddleware(api.AuthMiddleware(api.ScopeMiddleware("security:read", api.HandleSecurityAlerts))))
|
||||||
mux.HandleFunc("/api/v1/security/check", corsMiddleware(api.AuthMiddleware(api.ScopeMiddleware("security:check", api.HandleSecurityCheck))))
|
mux.HandleFunc("/api/v1/security/check", corsMiddleware(api.AuthMiddleware(api.ScopeMiddleware("security:check", api.HandleSecurityCheck))))
|
||||||
mux.HandleFunc("/api/v1/security/logs", corsMiddleware(api.AuthMiddleware(api.ScopeMiddleware("security:read", api.HandleSecurityLogs))))
|
mux.HandleFunc("/api/v1/security/logs", corsMiddleware(api.AuthMiddleware(api.ScopeMiddleware("security:read", api.HandleSecurityLogs))))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package version
|
package version
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Version = "1.1.10"
|
Version = "1.1.11"
|
||||||
Repo = "MengMengCode/CLICD"
|
Repo = "MengMengCode/CLICD"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "clicd-frontend",
|
"name": "clicd-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.1.10",
|
"version": "1.1.11",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export default function Login() {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-center text-xs text-gray-400 mt-6">CLICD v1.1.10</p>
|
<p className="text-center text-xs text-gray-400 mt-6">CLICD v1.1.11</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
+114
-22
@@ -1,13 +1,16 @@
|
|||||||
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react'
|
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react'
|
||||||
import { Clock, Globe, Lock, LogIn, Monitor, RefreshCw, ShieldCheck, Upload, UserCog } from 'lucide-react'
|
import { Clock, Globe, Lock, LogIn, Monitor, RefreshCw, ShieldCheck, Terminal, Upload, UserCog } from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
changePassword,
|
changePassword,
|
||||||
changeUsername,
|
changeUsername,
|
||||||
getLoginLogs,
|
getLoginLogs,
|
||||||
getSSLSettings,
|
getSSLSettings,
|
||||||
|
getWebSSHOriginSettings,
|
||||||
LoginLog,
|
LoginLog,
|
||||||
SSLSettings,
|
SSLSettings,
|
||||||
updateSSLSettings,
|
updateSSLSettings,
|
||||||
|
updateWebSSHOriginSettings,
|
||||||
|
WebSSHOriginSettings,
|
||||||
} from '../services/api'
|
} from '../services/api'
|
||||||
import { useDialog } from '../components/Dialog'
|
import { useDialog } from '../components/Dialog'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
@@ -33,6 +36,9 @@ export default function Settings() {
|
|||||||
const [keyPEM, setKeyPEM] = useState('')
|
const [keyPEM, setKeyPEM] = useState('')
|
||||||
const [applyNow, setApplyNow] = useState(true)
|
const [applyNow, setApplyNow] = useState(true)
|
||||||
const [savingSSL, setSavingSSL] = useState(false)
|
const [savingSSL, setSavingSSL] = useState(false)
|
||||||
|
const [webSSHOrigins, setWebSSHOrigins] = useState<WebSSHOriginSettings | null>(null)
|
||||||
|
const [webSSHOriginsText, setWebSSHOriginsText] = useState('')
|
||||||
|
const [savingWebSSHOrigins, setSavingWebSSHOrigins] = useState(false)
|
||||||
|
|
||||||
const fetchLogs = useCallback(async () => {
|
const fetchLogs = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -60,12 +66,25 @@ export default function Settings() {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const fetchWebSSHOrigins = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const res = await getWebSSHOriginSettings()
|
||||||
|
const data = res.data.data
|
||||||
|
if (!data) return
|
||||||
|
setWebSSHOrigins(data)
|
||||||
|
setWebSSHOriginsText((data.origins || []).join('\n'))
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchLogs()
|
fetchLogs()
|
||||||
fetchSSL()
|
fetchSSL()
|
||||||
|
fetchWebSSHOrigins()
|
||||||
const timer = setInterval(fetchLogs, 15000)
|
const timer = setInterval(fetchLogs, 15000)
|
||||||
return () => clearInterval(timer)
|
return () => clearInterval(timer)
|
||||||
}, [fetchLogs, fetchSSL])
|
}, [fetchLogs, fetchSSL, fetchWebSSHOrigins])
|
||||||
|
|
||||||
const handleSSLModeChange = (mode: SSLSettings['mode']) => {
|
const handleSSLModeChange = (mode: SSLSettings['mode']) => {
|
||||||
setSSLMode(mode)
|
setSSLMode(mode)
|
||||||
@@ -101,6 +120,25 @@ export default function Settings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSaveWebSSHOrigins = async () => {
|
||||||
|
setSavingWebSSHOrigins(true)
|
||||||
|
try {
|
||||||
|
const origins = webSSHOriginsText.split(/\r?\n/).map(item => item.trim()).filter(Boolean)
|
||||||
|
const res = await updateWebSSHOriginSettings(origins)
|
||||||
|
const data = res.data.data
|
||||||
|
if (data) {
|
||||||
|
setWebSSHOrigins(data)
|
||||||
|
setWebSSHOriginsText((data.origins || []).join('\n'))
|
||||||
|
}
|
||||||
|
dialog.alert('完成', 'Origin 白名单已保存')
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const e = err as { response?: { data?: { message?: string } } }
|
||||||
|
dialog.alert('失败', e.response?.data?.message || 'Origin 白名单保存失败')
|
||||||
|
} finally {
|
||||||
|
setSavingWebSSHOrigins(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleSaveAccount = async () => {
|
const handleSaveAccount = async () => {
|
||||||
if (!oldPwd) {
|
if (!oldPwd) {
|
||||||
dialog.alert('提示', '请输入当前密码以确认修改')
|
dialog.alert('提示', '请输入当前密码以确认修改')
|
||||||
@@ -159,26 +197,37 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid items-start gap-6 xl:grid-cols-[minmax(0,1.15fr)_minmax(360px,0.85fr)]">
|
<div className="grid items-start gap-6 xl:grid-cols-[minmax(0,1.15fr)_minmax(360px,0.85fr)]">
|
||||||
<SSLCard
|
<div className="space-y-6">
|
||||||
ssl={ssl}
|
<SSLCard
|
||||||
sslEnabled={sslEnabled}
|
ssl={ssl}
|
||||||
sslMode={sslMode}
|
sslEnabled={sslEnabled}
|
||||||
sslTarget={sslTarget}
|
sslMode={sslMode}
|
||||||
sslEmail={sslEmail}
|
sslTarget={sslTarget}
|
||||||
certPEM={certPEM}
|
sslEmail={sslEmail}
|
||||||
keyPEM={keyPEM}
|
certPEM={certPEM}
|
||||||
applyNow={applyNow}
|
keyPEM={keyPEM}
|
||||||
savingSSL={savingSSL}
|
applyNow={applyNow}
|
||||||
onRefresh={fetchSSL}
|
savingSSL={savingSSL}
|
||||||
onEnabledChange={setSSLEnabled}
|
onRefresh={fetchSSL}
|
||||||
onModeChange={handleSSLModeChange}
|
onEnabledChange={setSSLEnabled}
|
||||||
onTargetChange={setSSLTarget}
|
onModeChange={handleSSLModeChange}
|
||||||
onEmailChange={setSSLEmail}
|
onTargetChange={setSSLTarget}
|
||||||
onCertChange={setCertPEM}
|
onEmailChange={setSSLEmail}
|
||||||
onKeyChange={setKeyPEM}
|
onCertChange={setCertPEM}
|
||||||
onApplyNowChange={setApplyNow}
|
onKeyChange={setKeyPEM}
|
||||||
onSave={handleSaveSSL}
|
onApplyNowChange={setApplyNow}
|
||||||
/>
|
onSave={handleSaveSSL}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<WebSSHOriginCard
|
||||||
|
settings={webSSHOrigins}
|
||||||
|
originsText={webSSHOriginsText}
|
||||||
|
saving={savingWebSSHOrigins}
|
||||||
|
onOriginsTextChange={setWebSSHOriginsText}
|
||||||
|
onRefresh={fetchWebSSHOrigins}
|
||||||
|
onSave={handleSaveWebSSHOrigins}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="rounded-lg border border-gray-200 bg-white p-5">
|
<div className="rounded-lg border border-gray-200 bg-white p-5">
|
||||||
<h2 className="mb-4 flex items-center gap-2 text-sm font-semibold text-black">
|
<h2 className="mb-4 flex items-center gap-2 text-sm font-semibold text-black">
|
||||||
@@ -232,6 +281,49 @@ interface SSLCardProps {
|
|||||||
onSave: () => void
|
onSave: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface WebSSHOriginCardProps {
|
||||||
|
settings: WebSSHOriginSettings | null
|
||||||
|
originsText: string
|
||||||
|
saving: boolean
|
||||||
|
onOriginsTextChange: (value: string) => void
|
||||||
|
onRefresh: () => void
|
||||||
|
onSave: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function WebSSHOriginCard(props: WebSSHOriginCardProps) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-gray-200 bg-white p-5">
|
||||||
|
<div className="mb-4 flex items-center justify-between gap-3">
|
||||||
|
<h2 className="flex items-center gap-2 text-sm font-semibold text-black">
|
||||||
|
<Terminal className="h-4 w-4" />WebSSH Origin 白名单
|
||||||
|
</h2>
|
||||||
|
<button onClick={props.onRefresh} className="rounded-md border border-gray-200 p-1.5 text-gray-500 hover:bg-gray-50" title="刷新">
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-xs text-gray-500">允许的 Origin</label>
|
||||||
|
<textarea
|
||||||
|
value={props.originsText}
|
||||||
|
onChange={(e) => props.onOriginsTextChange(e.target.value)}
|
||||||
|
rows={5}
|
||||||
|
className="w-full rounded-md border border-gray-300 bg-white px-3 py-2 font-mono text-xs text-black"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border border-gray-100 bg-gray-50 p-3 text-xs text-gray-600">
|
||||||
|
<div className="truncate font-mono" title={props.settings?.current_origin || ''}>当前面板来源:{props.settings?.current_origin || '-'}</div>
|
||||||
|
<div className="mt-1">默认允许当前面板来源和本机回环来源;额外域名每行填写一个完整 Origin。</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={props.onSave} disabled={props.saving} className="inline-flex w-full items-center justify-center gap-2 rounded-md bg-black px-4 py-2 text-sm text-white hover:bg-gray-800 disabled:opacity-50">
|
||||||
|
<Upload className="h-4 w-4" />
|
||||||
|
{props.saving ? '保存中...' : '保存 Origin 白名单'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function SSLCard(props: SSLCardProps) {
|
function SSLCard(props: SSLCardProps) {
|
||||||
const selectedSSL = props.ssl?.mode_certificates?.[props.sslMode]
|
const selectedSSL = props.ssl?.mode_certificates?.[props.sslMode]
|
||||||
const modeOptions: Array<{ value: SSLSettings['mode']; label: string }> = [
|
const modeOptions: Array<{ value: SSLSettings['mode']; label: string }> = [
|
||||||
|
|||||||
@@ -393,6 +393,17 @@ export const getSSLSettings = () =>
|
|||||||
export const updateSSLSettings = (data: UpdateSSLSettingsRequest) =>
|
export const updateSSLSettings = (data: UpdateSSLSettingsRequest) =>
|
||||||
api.put<APIResponse<SSLSettings>>('/ssl', data)
|
api.put<APIResponse<SSLSettings>>('/ssl', data)
|
||||||
|
|
||||||
|
export interface WebSSHOriginSettings {
|
||||||
|
origins: string[]
|
||||||
|
current_origin?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getWebSSHOriginSettings = () =>
|
||||||
|
api.get<APIResponse<WebSSHOriginSettings>>('/webssh-origins')
|
||||||
|
|
||||||
|
export const updateWebSSHOriginSettings = (origins: string[]) =>
|
||||||
|
api.put<APIResponse<WebSSHOriginSettings>>('/webssh-origins', { origins })
|
||||||
|
|
||||||
// Containers
|
// Containers
|
||||||
export const getContainers = () =>
|
export const getContainers = () =>
|
||||||
api.get<APIResponse<Container[]>>('/containers')
|
api.get<APIResponse<Container[]>>('/containers')
|
||||||
|
|||||||
@@ -397,6 +397,13 @@ const exact: Record<string, string> = {
|
|||||||
'保存后自动重启服务并立即生效': 'Restart service automatically after saving',
|
'保存后自动重启服务并立即生效': 'Restart service automatically after saving',
|
||||||
'保存中...': 'Saving...',
|
'保存中...': 'Saving...',
|
||||||
'保存 SSL 设置': 'Save SSL Settings',
|
'保存 SSL 设置': 'Save SSL Settings',
|
||||||
|
'WebSSH Origin 白名单': 'WebSSH Origin Allowlist',
|
||||||
|
'允许的 Origin': 'Allowed Origins',
|
||||||
|
'当前面板来源:': 'Current panel origin:',
|
||||||
|
'默认允许当前面板来源和本机回环来源;额外域名每行填写一个完整 Origin。': 'The current panel origin and local loopback origins are allowed by default. Add one full Origin per line.',
|
||||||
|
'保存 Origin 白名单': 'Save Origin Allowlist',
|
||||||
|
'Origin 白名单已保存': 'Origin allowlist saved',
|
||||||
|
'Origin 白名单保存失败': 'Failed to save Origin allowlist',
|
||||||
'登录日志': 'Login Logs',
|
'登录日志': 'Login Logs',
|
||||||
'首页': 'First',
|
'首页': 'First',
|
||||||
'上一页': 'Previous',
|
'上一页': 'Previous',
|
||||||
|
|||||||
Reference in New Issue
Block a user