From 28597721f2ad61f933f12315a2a21410f8a05798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=98=AD?= <81747598+lan-yonghui@users.noreply.github.com> Date: Thu, 21 Nov 2024 22:09:00 +0800 Subject: [PATCH] feat: Add API interface authentication function (#7146) --- backend/app/api/v1/setting.go | 49 +++++ backend/app/dto/setting.go | 10 + backend/app/service/setting.go | 27 +++ backend/configs/system.go | 51 ++--- backend/constant/errs.go | 28 +-- backend/i18n/lang/en.yaml | 4 + backend/i18n/lang/zh-Hant.yaml | 4 + backend/i18n/lang/zh.yaml | 4 + backend/init/hook/hook.go | 18 ++ backend/init/migration/migrate.go | 1 + backend/init/migration/migrations/v_1_10.go | 16 ++ backend/middleware/session.go | 61 ++++++ backend/router/ro_setting.go | 2 + cmd/server/docs/docs.go | 126 ++++++++++-- cmd/server/docs/swagger.json | 127 ++++++++++-- cmd/server/docs/swagger.yaml | 93 ++++++++- cmd/server/main.go | 20 +- frontend/src/api/interface/setting.ts | 9 + frontend/src/api/modules/setting.ts | 8 + frontend/src/lang/modules/en.ts | 25 ++- frontend/src/lang/modules/tw.ts | 22 ++- frontend/src/lang/modules/zh.ts | 14 ++ frontend/src/styles/element-dark.scss | 3 +- frontend/src/styles/element.scss | 1 + .../src/views/container/image/pull/index.vue | 5 + .../views/host/terminal/terminal/index.vue | 1 + frontend/src/views/log/operation/index.vue | 3 + .../setting/panel/api-interface/index.vue | 183 ++++++++++++++++++ frontend/src/views/setting/panel/index.vue | 57 +++++- 29 files changed, 884 insertions(+), 88 deletions(-) create mode 100644 frontend/src/views/setting/panel/api-interface/index.vue diff --git a/backend/app/api/v1/setting.go b/backend/app/api/v1/setting.go index bcf9fed5f..dc48e181c 100644 --- a/backend/app/api/v1/setting.go +++ b/backend/app/api/v1/setting.go @@ -342,3 +342,52 @@ func (b *BaseApi) MFABind(c *gin.Context) { helper.SuccessWithData(c, nil) } + +// @Tags System Setting +// @Summary generate api key +// @Description 生成 API 接口密钥 +// @Accept json +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/api/config/generate/key [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成 API 接口密钥","formatEN":"generate api key"} +func (b *BaseApi) GenerateApiKey(c *gin.Context) { + panelToken := c.GetHeader("1Panel-Token") + if panelToken != "" { + helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigDisable, nil) + return + } + apiKey, err := settingService.GenerateApiKey() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, apiKey) +} + +// @Tags System Setting +// @Summary Update api config +// @Description 更新 API 接口配置 +// @Accept json +// @Param request body dto.ApiInterfaceConfig true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /settings/api/config/update [post] +// @x-panel-log {"bodyKeys":["ipWhiteList"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 API 接口配置 => IP 白名单: [ipWhiteList]","formatEN":"update api config => IP White List: [ipWhiteList]"} +func (b *BaseApi) UpdateApiConfig(c *gin.Context) { + panelToken := c.GetHeader("1Panel-Token") + if panelToken != "" { + helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigDisable, nil) + return + } + var req dto.ApiInterfaceConfig + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := settingService.UpdateApiConfig(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} diff --git a/backend/app/dto/setting.go b/backend/app/dto/setting.go index 78e0ffef1..ac15e7c60 100644 --- a/backend/app/dto/setting.go +++ b/backend/app/dto/setting.go @@ -66,6 +66,10 @@ type SettingInfo struct { ProxyUser string `json:"proxyUser"` ProxyPasswd string `json:"proxyPasswd"` ProxyPasswdKeep string `json:"proxyPasswdKeep"` + + ApiInterfaceStatus string `json:"apiInterfaceStatus"` + ApiKey string `json:"apiKey"` + IpWhiteList string `json:"ipWhiteList"` } type SettingUpdate struct { @@ -231,3 +235,9 @@ type XpackHideMenu struct { Path string `json:"path,omitempty"` Children []XpackHideMenu `json:"children,omitempty"` } + +type ApiInterfaceConfig struct { + ApiInterfaceStatus string `json:"apiInterfaceStatus"` + ApiKey string `json:"apiKey"` + IpWhiteList string `json:"ipWhiteList"` +} diff --git a/backend/app/service/setting.go b/backend/app/service/setting.go index 160725571..1e05d458f 100644 --- a/backend/app/service/setting.go +++ b/backend/app/service/setting.go @@ -40,6 +40,8 @@ type ISettingService interface { UpdateSSL(c *gin.Context, req dto.SSLUpdate) error LoadFromCert() (*dto.SSLInfo, error) HandlePasswordExpired(c *gin.Context, old, new string) error + GenerateApiKey() (string, error) + UpdateApiConfig(req dto.ApiInterfaceConfig) error } func NewISettingService() ISettingService { @@ -485,3 +487,28 @@ func checkCertValid() error { return nil } + +func (u *SettingService) GenerateApiKey() (string, error) { + apiKey := common.RandStr(32) + if err := settingRepo.Update("ApiKey", apiKey); err != nil { + return global.CONF.System.ApiKey, err + } + global.CONF.System.ApiKey = apiKey + return apiKey, nil +} + +func (u *SettingService) UpdateApiConfig(req dto.ApiInterfaceConfig) error { + if err := settingRepo.Update("ApiInterfaceStatus", req.ApiInterfaceStatus); err != nil { + return err + } + global.CONF.System.ApiInterfaceStatus = req.ApiInterfaceStatus + if err := settingRepo.Update("ApiKey", req.ApiKey); err != nil { + return err + } + global.CONF.System.ApiKey = req.ApiKey + if err := settingRepo.Update("IpWhiteList", req.IpWhiteList); err != nil { + return err + } + global.CONF.System.IpWhiteList = req.IpWhiteList + return nil +} diff --git a/backend/configs/system.go b/backend/configs/system.go index 2e1f1c458..4ede9d642 100644 --- a/backend/configs/system.go +++ b/backend/configs/system.go @@ -1,28 +1,31 @@ package configs type System struct { - Port string `mapstructure:"port"` - Ipv6 string `mapstructure:"ipv6"` - BindAddress string `mapstructure:"bindAddress"` - SSL string `mapstructure:"ssl"` - DbFile string `mapstructure:"db_file"` - DbPath string `mapstructure:"db_path"` - LogPath string `mapstructure:"log_path"` - DataDir string `mapstructure:"data_dir"` - TmpDir string `mapstructure:"tmp_dir"` - Cache string `mapstructure:"cache"` - Backup string `mapstructure:"backup"` - EncryptKey string `mapstructure:"encrypt_key"` - BaseDir string `mapstructure:"base_dir"` - Mode string `mapstructure:"mode"` - RepoUrl string `mapstructure:"repo_url"` - Version string `mapstructure:"version"` - Username string `mapstructure:"username"` - Password string `mapstructure:"password"` - Entrance string `mapstructure:"entrance"` - IsDemo bool `mapstructure:"is_demo"` - AppRepo string `mapstructure:"app_repo"` - ChangeUserInfo string `mapstructure:"change_user_info"` - OneDriveID string `mapstructure:"one_drive_id"` - OneDriveSc string `mapstructure:"one_drive_sc"` + Port string `mapstructure:"port"` + Ipv6 string `mapstructure:"ipv6"` + BindAddress string `mapstructure:"bindAddress"` + SSL string `mapstructure:"ssl"` + DbFile string `mapstructure:"db_file"` + DbPath string `mapstructure:"db_path"` + LogPath string `mapstructure:"log_path"` + DataDir string `mapstructure:"data_dir"` + TmpDir string `mapstructure:"tmp_dir"` + Cache string `mapstructure:"cache"` + Backup string `mapstructure:"backup"` + EncryptKey string `mapstructure:"encrypt_key"` + BaseDir string `mapstructure:"base_dir"` + Mode string `mapstructure:"mode"` + RepoUrl string `mapstructure:"repo_url"` + Version string `mapstructure:"version"` + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` + Entrance string `mapstructure:"entrance"` + IsDemo bool `mapstructure:"is_demo"` + AppRepo string `mapstructure:"app_repo"` + ChangeUserInfo string `mapstructure:"change_user_info"` + OneDriveID string `mapstructure:"one_drive_id"` + OneDriveSc string `mapstructure:"one_drive_sc"` + ApiInterfaceStatus string `mapstructure:"api_interface_status"` + ApiKey string `mapstructure:"api_key"` + IpWhiteList string `mapstructure:"ip_white_list"` } diff --git a/backend/constant/errs.go b/backend/constant/errs.go index 69ff39d66..a76e8b3f3 100644 --- a/backend/constant/errs.go +++ b/backend/constant/errs.go @@ -37,18 +37,22 @@ var ( // api var ( - ErrTypeInternalServer = "ErrInternalServer" - ErrTypeInvalidParams = "ErrInvalidParams" - ErrTypeNotLogin = "ErrNotLogin" - ErrTypePasswordExpired = "ErrPasswordExpired" - ErrNameIsExist = "ErrNameIsExist" - ErrDemoEnvironment = "ErrDemoEnvironment" - ErrCmdIllegal = "ErrCmdIllegal" - ErrXpackNotFound = "ErrXpackNotFound" - ErrXpackNotActive = "ErrXpackNotActive" - ErrXpackLost = "ErrXpackLost" - ErrXpackTimeout = "ErrXpackTimeout" - ErrXpackOutOfDate = "ErrXpackOutOfDate" + ErrTypeInternalServer = "ErrInternalServer" + ErrTypeInvalidParams = "ErrInvalidParams" + ErrTypeNotLogin = "ErrNotLogin" + ErrTypePasswordExpired = "ErrPasswordExpired" + ErrNameIsExist = "ErrNameIsExist" + ErrDemoEnvironment = "ErrDemoEnvironment" + ErrCmdIllegal = "ErrCmdIllegal" + ErrXpackNotFound = "ErrXpackNotFound" + ErrXpackNotActive = "ErrXpackNotActive" + ErrXpackLost = "ErrXpackLost" + ErrXpackTimeout = "ErrXpackTimeout" + ErrXpackOutOfDate = "ErrXpackOutOfDate" + ErrApiConfigStatusInvalid = "ErrApiConfigStatusInvalid" + ErrApiConfigKeyInvalid = "ErrApiConfigKeyInvalid" + ErrApiConfigIPInvalid = "ErrApiConfigIPInvalid" + ErrApiConfigDisable = "ErrApiConfigDisable" ) // app diff --git a/backend/i18n/lang/en.yaml b/backend/i18n/lang/en.yaml index 0ea05c1e5..b62076f3b 100644 --- a/backend/i18n/lang/en.yaml +++ b/backend/i18n/lang/en.yaml @@ -8,6 +8,10 @@ ErrStructTransform: "Type conversion failure: {{ .detail }}" ErrNotLogin: "User is not Login: {{ .detail }}" ErrPasswordExpired: "The current password has expired: {{ .detail }}" ErrNotSupportType: "The system does not support the current type: {{ .detail }}" +ErrApiConfigStatusInvalid: "API Interface access prohibited: {{ .detail }}" +ErrApiConfigKeyInvalid: "API Interface key error: {{ .detail }}" +ErrApiConfigIPInvalid: "API Interface IP is not on the whitelist: {{ .detail }}" +ErrApiConfigDisable: "This interface prohibits the use of API Interface calls: {{ .detail }}" #common ErrNameIsExist: "Name is already exist" diff --git a/backend/i18n/lang/zh-Hant.yaml b/backend/i18n/lang/zh-Hant.yaml index 77fbed987..c5f10ead6 100644 --- a/backend/i18n/lang/zh-Hant.yaml +++ b/backend/i18n/lang/zh-Hant.yaml @@ -8,6 +8,10 @@ ErrStructTransform: "類型轉換失敗: {{ .detail }}" ErrNotLogin: "用戶未登入: {{ .detail }}" ErrPasswordExpired: "當前密碼已過期: {{ .detail }}" ErrNotSupportType: "系統暫不支持當前類型: {{ .detail }}" +ErrApiConfigStatusInvalid: "API 接口禁止訪問: {{ .detail }}" +ErrApiConfigKeyInvalid: "API 接口密钥錯誤: {{ .detail }}" +ErrApiConfigIPInvalid: "调用 API 接口 IP 不在白名单: {{ .detail }}" +ErrApiConfigDisable: "此接口禁止使用 API 接口調用: {{ .detail }}" #common ErrNameIsExist: "名稱已存在" diff --git a/backend/i18n/lang/zh.yaml b/backend/i18n/lang/zh.yaml index 94c983f1a..4880df5f6 100644 --- a/backend/i18n/lang/zh.yaml +++ b/backend/i18n/lang/zh.yaml @@ -8,6 +8,10 @@ ErrStructTransform: "类型转换失败: {{ .detail }}" ErrNotLogin: "用户未登录: {{ .detail }}" ErrPasswordExpired: "当前密码已过期: {{ .detail }}" ErrNotSupportType: "系统暂不支持当前类型: {{ .detail }}" +ErrApiConfigStatusInvalid: "API 接口禁止访问: {{ .detail }}" +ErrApiConfigKeyInvalid: "API 接口密钥错误: {{ .detail }}" +ErrApiConfigIPInvalid: "调用 API 接口 IP 不在白名单: {{ .detail }}" +ErrApiConfigDisable: "此接口禁止使用 API 接口调用: {{ .detail }}" #common ErrNameIsExist: "名称已存在" diff --git a/backend/init/hook/hook.go b/backend/init/hook/hook.go index 1ddbb8f10..b0334966f 100644 --- a/backend/init/hook/hook.go +++ b/backend/init/hook/hook.go @@ -61,6 +61,24 @@ func Init() { global.LOG.Fatalf("init service before start failed, err: %v", err) } + apiInterfaceStatusSetting, err := settingRepo.Get(settingRepo.WithByKey("ApiInterfaceStatus")) + if err != nil { + global.LOG.Errorf("load service api interface from setting failed, err: %v", err) + } + global.CONF.System.ApiInterfaceStatus = apiInterfaceStatusSetting.Value + if apiInterfaceStatusSetting.Value == "enable" { + apiKeySetting, err := settingRepo.Get(settingRepo.WithByKey("ApiKey")) + if err != nil { + global.LOG.Errorf("load service api key from setting failed, err: %v", err) + } + global.CONF.System.ApiKey = apiKeySetting.Value + ipWhiteListSetting, err := settingRepo.Get(settingRepo.WithByKey("IpWhiteList")) + if err != nil { + global.LOG.Errorf("load service ip white list from setting failed, err: %v", err) + } + global.CONF.System.IpWhiteList = ipWhiteListSetting.Value + } + handleUserInfo(global.CONF.System.ChangeUserInfo, settingRepo) handleCronjobStatus() diff --git a/backend/init/migration/migrate.go b/backend/init/migration/migrate.go index b2c472009..52c243fea 100644 --- a/backend/init/migration/migrate.go +++ b/backend/init/migration/migrate.go @@ -97,6 +97,7 @@ func Init() { migrations.AddComposeColumn, migrations.AddAutoRestart, + migrations.AddApiInterfaceConfig, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/backend/init/migration/migrations/v_1_10.go b/backend/init/migration/migrations/v_1_10.go index 4a10469ff..e0e97e7bf 100644 --- a/backend/init/migration/migrations/v_1_10.go +++ b/backend/init/migration/migrations/v_1_10.go @@ -334,3 +334,19 @@ var AddAutoRestart = &gormigrate.Migration{ return nil }, } + +var AddApiInterfaceConfig = &gormigrate.Migration{ + ID: "202411-add-api-interface-config", + Migrate: func(tx *gorm.DB) error { + if err := tx.Create(&model.Setting{Key: "ApiInterfaceStatus", Value: "disable"}).Error; err != nil { + return err + } + if err := tx.Create(&model.Setting{Key: "ApiKey", Value: ""}).Error; err != nil { + return err + } + if err := tx.Create(&model.Setting{Key: "IpWhiteList", Value: ""}).Error; err != nil { + return err + } + return nil + }, +} diff --git a/backend/middleware/session.go b/backend/middleware/session.go index 5bffb1cb9..8aa9b79bf 100644 --- a/backend/middleware/session.go +++ b/backend/middleware/session.go @@ -1,7 +1,11 @@ package middleware import ( + "crypto/md5" + "encoding/hex" + "net" "strconv" + "strings" "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" "github.com/1Panel-dev/1Panel/backend/app/repo" @@ -16,6 +20,28 @@ func SessionAuth() gin.HandlerFunc { c.Next() return } + panelToken := c.GetHeader("1Panel-Token") + panelTimestamp := c.GetHeader("1Panel-Timestamp") + if panelToken != "" || panelTimestamp != "" { + if global.CONF.System.ApiInterfaceStatus == "enable" { + clientIP := c.ClientIP() + if !isValid1PanelToken(panelToken, panelTimestamp) { + helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigKeyInvalid, nil) + return + } + + if !isIPInWhiteList(clientIP) { + helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigIPInvalid, nil) + return + } + c.Next() + return + } else { + helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrApiConfigStatusInvalid, nil) + return + } + } + sId, err := c.Cookie(constant.SessionName) if err != nil { helper.ErrorWithDetail(c, constant.CodeErrUnauthorized, constant.ErrTypeNotLogin, nil) @@ -36,3 +62,38 @@ func SessionAuth() gin.HandlerFunc { c.Next() } } + +func isValid1PanelToken(panelToken string, panelTimestamp string) bool { + system1PanelToken := global.CONF.System.ApiKey + if GenerateMD5("1panel"+panelToken+panelTimestamp) == GenerateMD5("1panel"+system1PanelToken+panelTimestamp) { + return true + } + return false +} + +func isIPInWhiteList(clientIP string) bool { + ipWhiteString := global.CONF.System.IpWhiteList + ipWhiteList := strings.Split(ipWhiteString, "\n") + for _, cidr := range ipWhiteList { + if cidr == "0.0.0.0" { + return true + } + _, ipNet, err := net.ParseCIDR(cidr) + if err != nil { + if cidr == clientIP { + return true + } + continue + } + if ipNet.Contains(net.ParseIP(clientIP)) { + return true + } + } + return false +} + +func GenerateMD5(input string) string { + hash := md5.New() + hash.Write([]byte(input)) + return hex.EncodeToString(hash.Sum(nil)) +} diff --git a/backend/router/ro_setting.go b/backend/router/ro_setting.go index 5d25f465d..dc19dc935 100644 --- a/backend/router/ro_setting.go +++ b/backend/router/ro_setting.go @@ -64,5 +64,7 @@ func (s *SettingRouter) InitRouter(Router *gin.RouterGroup) { settingRouter.POST("/upgrade/notes", baseApi.GetNotesByVersion) settingRouter.GET("/upgrade", baseApi.GetUpgradeInfo) settingRouter.GET("/basedir", baseApi.LoadBaseDir) + settingRouter.POST("/api/config/generate/key", baseApi.GenerateApiKey) + settingRouter.POST("/api/config/update", baseApi.UpdateApiConfig) } } diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index cf06e4b60..b2257bb4a 100644 --- a/cmd/server/docs/docs.go +++ b/cmd/server/docs/docs.go @@ -887,20 +887,6 @@ const docTemplate = `{ } } }, - "/auth/issafety": { - "get": { - "description": "获取系统安全登录状态", - "tags": [ - "Auth" - ], - "summary": "Load safety status", - "responses": { - "200": { - "description": "OK" - } - } - } - }, "/auth/language": { "get": { "description": "获取系统语言设置", @@ -9507,6 +9493,77 @@ const docTemplate = `{ } } }, + "/settings/api/config/generate/key": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "生成 API 接口密钥", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "generate api key", + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "generate api key", + "formatZH": "生成 API 接口密钥", + "paramKeys": [] + } + } + }, + "/settings/api/config/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 API 接口配置", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Update api config", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ApiInterfaceConfig" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "ipWhiteList" + ], + "formatEN": "update api config =\u003e IP White List: [ipWhiteList]", + "formatZH": "更新 API 接口配置 =\u003e IP 白名单: [ipWhiteList]", + "paramKeys": [] + } + } + }, "/settings/backup": { "post": { "security": [ @@ -15310,6 +15367,20 @@ const docTemplate = `{ } } }, + "dto.ApiInterfaceConfig": { + "type": "object", + "properties": { + "apiInterfaceStatus": { + "type": "string" + }, + "apiKey": { + "type": "string" + }, + "ipWhiteList": { + "type": "string" + } + } + }, "dto.AppInstallInfo": { "type": "object", "properties": { @@ -19629,6 +19700,12 @@ const docTemplate = `{ "allowIPs": { "type": "string" }, + "apiInterfaceStatus": { + "type": "string" + }, + "apiKey": { + "type": "string" + }, "appStoreLastModified": { "type": "string" }, @@ -19677,6 +19754,9 @@ const docTemplate = `{ "fileRecycleBin": { "type": "string" }, + "ipWhiteList": { + "type": "string" + }, "ipv6": { "type": "string" }, @@ -23635,15 +23715,29 @@ const docTemplate = `{ } } } + }, + "securityDefinitions": { + "CustomToken": { + "description": "自定义 Token 格式,格式:md5('1panel' + 1Panel-Token + 1Panel-Timestamp)。\n` + "`" + `` + "`" + `` + "`" + `\n示例请求头:\ncurl -X GET \"http://localhost:4004/api/v1/resource\" \\\n-H \"1Panel-Token: \u003c1panel_token\u003e\" \\\n-H \"1Panel-Timestamp: \u003ccurrent_unix_timestamp\u003e\"\n` + "`" + `` + "`" + `` + "`" + `\n- ` + "`" + `1Panel-Token` + "`" + ` 为面板 API 接口密钥。", + "type": "apiKey", + "name": "1Panel-Token", + "in": "Header" + }, + "Timestamp": { + "description": "- ` + "`" + `1Panel-Timestamp` + "`" + ` 为当前时间的 Unix 时间戳(单位:秒)。", + "type": "apiKey", + "name": "1Panel-Timestamp", + "in": "header" + } } }` // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ Version: "1.0", - Host: "localhost", + Host: "", BasePath: "/api/v1", - Schemes: []string{}, + Schemes: []string{"http", "https"}, Title: "1Panel", Description: "开源Linux面板", InfoInstanceName: "swagger", diff --git a/cmd/server/docs/swagger.json b/cmd/server/docs/swagger.json index 652a943b2..c738196d0 100644 --- a/cmd/server/docs/swagger.json +++ b/cmd/server/docs/swagger.json @@ -1,4 +1,8 @@ { + "schemes": [ + "http", + "https" + ], "swagger": "2.0", "info": { "description": "开源Linux面板", @@ -11,7 +15,6 @@ }, "version": "1.0" }, - "host": "localhost", "basePath": "/api/v1", "paths": { "/apps/:key": { @@ -881,20 +884,6 @@ } } }, - "/auth/issafety": { - "get": { - "description": "获取系统安全登录状态", - "tags": [ - "Auth" - ], - "summary": "Load safety status", - "responses": { - "200": { - "description": "OK" - } - } - } - }, "/auth/language": { "get": { "description": "获取系统语言设置", @@ -9501,6 +9490,77 @@ } } }, + "/settings/api/config/generate/key": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "生成 API 接口密钥", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "generate api key", + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [], + "formatEN": "generate api key", + "formatZH": "生成 API 接口密钥", + "paramKeys": [] + } + } + }, + "/settings/api/config/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新 API 接口配置", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Update api config", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ApiInterfaceConfig" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFunctions": [], + "bodyKeys": [ + "ipWhiteList" + ], + "formatEN": "update api config =\u003e IP White List: [ipWhiteList]", + "formatZH": "更新 API 接口配置 =\u003e IP 白名单: [ipWhiteList]", + "paramKeys": [] + } + } + }, "/settings/backup": { "post": { "security": [ @@ -15304,6 +15364,20 @@ } } }, + "dto.ApiInterfaceConfig": { + "type": "object", + "properties": { + "apiInterfaceStatus": { + "type": "string" + }, + "apiKey": { + "type": "string" + }, + "ipWhiteList": { + "type": "string" + } + } + }, "dto.AppInstallInfo": { "type": "object", "properties": { @@ -19623,6 +19697,12 @@ "allowIPs": { "type": "string" }, + "apiInterfaceStatus": { + "type": "string" + }, + "apiKey": { + "type": "string" + }, "appStoreLastModified": { "type": "string" }, @@ -19671,6 +19751,9 @@ "fileRecycleBin": { "type": "string" }, + "ipWhiteList": { + "type": "string" + }, "ipv6": { "type": "string" }, @@ -23629,5 +23712,19 @@ } } } + }, + "securityDefinitions": { + "CustomToken": { + "description": "自定义 Token 格式,格式:md5('1panel' + 1Panel-Token + 1Panel-Timestamp)。\n```\n示例请求头:\ncurl -X GET \"http://localhost:4004/api/v1/resource\" \\\n-H \"1Panel-Token: \u003c1panel_token\u003e\" \\\n-H \"1Panel-Timestamp: \u003ccurrent_unix_timestamp\u003e\"\n```\n- `1Panel-Token` 为面板 API 接口密钥。", + "type": "apiKey", + "name": "1Panel-Token", + "in": "Header" + }, + "Timestamp": { + "description": "- `1Panel-Timestamp` 为当前时间的 Unix 时间戳(单位:秒)。", + "type": "apiKey", + "name": "1Panel-Timestamp", + "in": "header" + } } } \ No newline at end of file diff --git a/cmd/server/docs/swagger.yaml b/cmd/server/docs/swagger.yaml index 48c049edc..8cb06a1e4 100644 --- a/cmd/server/docs/swagger.yaml +++ b/cmd/server/docs/swagger.yaml @@ -28,6 +28,15 @@ definitions: oldRule: $ref: '#/definitions/dto.AddrRuleOperate' type: object + dto.ApiInterfaceConfig: + properties: + apiInterfaceStatus: + type: string + apiKey: + type: string + ipWhiteList: + type: string + type: object dto.AppInstallInfo: properties: id: @@ -2951,6 +2960,10 @@ definitions: properties: allowIPs: type: string + apiInterfaceStatus: + type: string + apiKey: + type: string appStoreLastModified: type: string appStoreSyncStatus: @@ -2983,6 +2996,8 @@ definitions: type: string fileRecycleBin: type: string + ipWhiteList: + type: string ipv6: type: string language: @@ -5624,7 +5639,6 @@ definitions: version: type: string type: object -host: localhost info: contact: {} description: 开源Linux面板 @@ -6181,15 +6195,6 @@ paths: summary: Check System isDemo tags: - Auth - /auth/issafety: - get: - description: 获取系统安全登录状态 - responses: - "200": - description: OK - summary: Load safety status - tags: - - Auth /auth/language: get: description: 获取系统语言设置 @@ -11638,6 +11643,52 @@ paths: formatEN: Update runtime [name] formatZH: 更新运行环境 [name] paramKeys: [] + /settings/api/config/generate/key: + post: + consumes: + - application/json + description: 生成 API 接口密钥 + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: generate api key + tags: + - System Setting + x-panel-log: + BeforeFunctions: [] + bodyKeys: [] + formatEN: generate api key + formatZH: 生成 API 接口密钥 + paramKeys: [] + /settings/api/config/update: + post: + consumes: + - application/json + description: 更新 API 接口配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ApiInterfaceConfig' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update api config + tags: + - System Setting + x-panel-log: + BeforeFunctions: [] + bodyKeys: + - ipWhiteList + formatEN: 'update api config => IP White List: [ipWhiteList]' + formatZH: '更新 API 接口配置 => IP 白名单: [ipWhiteList]' + paramKeys: [] /settings/backup: post: consumes: @@ -15292,4 +15343,26 @@ paths: formatEN: Update website [primaryDomain] formatZH: 更新网站 [primaryDomain] paramKeys: [] +schemes: +- http +- https +securityDefinitions: + CustomToken: + description: |- + 自定义 Token 格式,格式:md5('1panel' + 1Panel-Token + 1Panel-Timestamp)。 + ``` + 示例请求头: + curl -X GET "http://localhost:4004/api/v1/resource" \ + -H "1Panel-Token: <1panel_token>" \ + -H "1Panel-Timestamp: " + ``` + - `1Panel-Token` 为面板 API 接口密钥。 + in: Header + name: 1Panel-Token + type: apiKey + Timestamp: + description: '- `1Panel-Timestamp` 为当前时间的 Unix 时间戳(单位:秒)。' + in: header + name: 1Panel-Timestamp + type: apiKey swagger: "2.0" diff --git a/cmd/server/main.go b/cmd/server/main.go index d0aefa1b5..fe326d7f5 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -15,8 +15,26 @@ import ( // @termsOfService http://swagger.io/terms/ // @license.name Apache 2.0 // @license.url http://www.apache.org/licenses/LICENSE-2.0.html -// @host localhost // @BasePath /api/v1 +// @schemes http https + +// @securityDefinitions.apikey CustomToken +// @description 自定义 Token 格式,格式:md5('1panel' + 1Panel-Token + 1Panel-Timestamp)。 +// @description ``` +// @description 示例请求头: +// @description curl -X GET "http://localhost:4004/api/v1/resource" \ +// @description -H "1Panel-Token: <1panel_token>" \ +// @description -H "1Panel-Timestamp: " +// @description ``` +// @description - `1Panel-Token` 为面板 API 接口密钥。 +// @type apiKey +// @in Header +// @name 1Panel-Token +// @securityDefinitions.apikey Timestamp +// @type apiKey +// @in header +// @name 1Panel-Timestamp +// @description - `1Panel-Timestamp` 为当前时间的 Unix 时间戳(单位:秒)。 //go:generate swag init -o ./docs -g main.go -d ../../backend -g ../cmd/server/main.go func main() { diff --git a/frontend/src/api/interface/setting.ts b/frontend/src/api/interface/setting.ts index 5d086f3a5..d908d586a 100644 --- a/frontend/src/api/interface/setting.ts +++ b/frontend/src/api/interface/setting.ts @@ -58,6 +58,10 @@ export namespace Setting { proxyUser: string; proxyPasswd: string; proxyPasswdKeep: string; + + apiInterfaceStatus: string; + apiKey: string; + ipWhiteList: string; } export interface SettingUpdate { key: string; @@ -186,4 +190,9 @@ export namespace Setting { trial: boolean; status: string; } + export interface ApiConfig { + apiInterfaceStatus: string; + apiKey: string; + ipWhiteList: string; + } } diff --git a/frontend/src/api/modules/setting.ts b/frontend/src/api/modules/setting.ts index 9e70c72dc..321f490fd 100644 --- a/frontend/src/api/modules/setting.ts +++ b/frontend/src/api/modules/setting.ts @@ -210,3 +210,11 @@ export const loadReleaseNotes = (version: string) => { export const upgrade = (version: string) => { return http.post(`/settings/upgrade`, { version: version }); }; + +// api config +export const generateApiKey = () => { + return http.post(`/settings/api/config/generate/key`); +}; +export const updateApiConfig = (param: Setting.ApiConfig) => { + return http.post(`/settings/api/config/update`, param); +}; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 393436a01..56064f84c 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -1401,11 +1401,28 @@ const message = { proxyPort: 'Proxy Port', proxyPasswdKeep: 'Remember Password', proxyDocker: 'Docker Proxy', - ProxyDockerHelper: + proxyDockerHelper: 'Synchronize proxy server configuration to Docker, support offline server image pulling and other operations', - ConfDockerProxy: 'Configure Docker Proxy', - RestartNowHelper: 'Configuring Docker proxy requires restarting the Docker service.', - RestartNow: 'Restart immediately', + apiInterface: 'API Interface', + apiInterfaceClose: 'Once closed, API interfaces cannot be accessed. Do you want to continue?', + apiInterfaceHelper: 'Provide panel support for API interface access', + apiInterfaceAlert1: + 'Please do not enable it in production environments as it may increase server security risks', + apiInterfaceAlert2: + 'Please do not use third-party applications to call the panel API to prevent potential security threats.', + apiInterfaceAlert3: 'API Interface Document:', + apiInterfaceAlert4: 'Usage Document:', + apiKey: 'Interface Key', + apiKeyHelper: 'Interface key is used for external applications to access API interfaces', + ipWhiteList: 'IP Whitelist', + ipWhiteListEgs: + 'When there are multiple IPs, line breaks are required for display, for example: \n172.161.10.111 \n172.161.10.0/24 ', + ipWhiteListHelper: 'IPs must be in the IP whitelist list to access the panel API interface', + apiKeyReset: 'Interface key reset', + apiKeyResetHelper: 'the associated key service will become invalid. Please add a new key to the service', + confDockerProxy: 'Configure Docker Proxy', + restartNowHelper: 'Configuring Docker proxy requires restarting the Docker service.', + restartNow: 'Restart immediately', systemIPWarning: 'The server address is not currently set. Please set it in the control panel first!', systemIPWarning1: 'The current server address is set to {0}, and quick redirection is not possible!', defaultNetwork: 'Network Card', diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index fddad2690..484e6b216 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -1323,9 +1323,23 @@ const message = { proxyPort: '代理端口', proxyPasswdKeep: '記住密碼', proxyDocker: 'Docker 代理', - proxyDockerHelper: '將代理伺服器配寘同步至Docker,支持離線服務器拉取鏡像等操作', - confDockerProxy: '配寘Docker代理', - restartNowHelper: '配寘Docker代理需要重啓Docker服務。', + proxyDockerHelper: '將代理伺服器配寘同步至 Docker,支持離線服務器拉取鏡像等操作', + apiInterface: 'API 接口', + apiInterfaceClose: '關閉後將不能使用 API 接口進行訪問,是否繼續?', + apiInterfaceHelper: '提供面板支持 API 接口訪問', + apiInterfaceAlert1: '請不要在生產環境開啟,這可能新增服務器安全風險', + apiInterfaceAlert2: '請不要使用協力廠商應用調用面板 API,以防止潜在的安全威脅。', + apiInterfaceAlert3: 'API 接口檔案:', + apiInterfaceAlert4: '使用檔案:', + apiKey: '接口密钥', + apiKeyHelper: '接口密钥用於外部應用訪問 API 接口', + ipWhiteList: 'IP白名單', + ipWhiteListEgs: '當存在多個 IP 時,需要換行顯示,例:\n172.16.10.111 \n172.16.10.0/24', + ipWhiteListHelper: '必需在 IP 白名單清單中的 IP 才能訪問面板 API 接口', + apiKeyReset: '接口密钥重置', + apiKeyResetHelper: '重置密钥後,已關聯密钥服務將失效,請重新添加新密鑰至服務。', + confDockerProxy: '配寘 Docker 代理', + restartNowHelper: '配寘 Docker 代理需要重啓 Docker 服務。', restartNow: '立即重啓', systemIPWarning: '當前未設置服務器地址,請先在面板設置中設置!', systemIPWarning1: '當前服務器地址設置為 {0},無法快速跳轉!', @@ -2095,7 +2109,7 @@ const message = { domainHelper: '一行一個網域名稱,支援*和IP位址', pushDir: '推送憑證到本機目錄', dir: '目錄', - pushDirHelper: '會在此目錄下產生兩個文件,憑證檔案:fullchain.pem 金鑰檔案:privkey.pem', + pushDirHelper: '會在此目錄下產生兩個文件,憑證檔案:fullchain.pem 密钥檔案:privkey.pem', organizationDetail: '機構詳情', fromWebsite: '從網站獲取', dnsMauanlHelper: '手動解析模式需要在建立完之後點選申請按鈕取得 DNS 解析值', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 510ba50ad..6e7b7c3cd 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -1326,6 +1326,20 @@ const message = { proxyPasswdKeep: '记住密码', proxyDocker: 'Docker 代理', proxyDockerHelper: '将代理服务器配置同步至 Docker,支持离线服务器拉取镜像等操作', + apiInterface: 'API 接口', + apiInterfaceClose: '关闭后将不能使用 API 接口进行访问,是否继续?', + apiInterfaceHelper: '提供面板支持 API 接口访问', + apiInterfaceAlert1: '请不要在生产环境开启,这可能增加服务器安全风险', + apiInterfaceAlert2: '请不要使用第三方应用调用面板 API,以防止潜在的安全威胁。', + apiInterfaceAlert3: 'API 接口文档:', + apiInterfaceAlert4: '使用文档:', + apiKey: '接口密钥', + apiKeyHelper: '接口密钥用于外部应用访问 API 接口', + ipWhiteList: 'IP 白名单', + ipWhiteListEgs: '当存在多个 IP 时,需要换行显示,例: \n172.16.10.111 \n172.16.10.0/24', + ipWhiteListHelper: '必需在 IP 白名单列表中的 IP 才能访问面板 API 接口', + apiKeyReset: '接口密钥重置', + apiKeyResetHelper: '重置密钥后,已关联密钥服务将失效,请重新添加新密钥至服务。', confDockerProxy: '配置 Docker 代理', restartNowHelper: '配置 Docker 代理需要重启 Docker 服务。', restartNow: '立即重启', diff --git a/frontend/src/styles/element-dark.scss b/frontend/src/styles/element-dark.scss index a70ae0345..14308b064 100644 --- a/frontend/src/styles/element-dark.scss +++ b/frontend/src/styles/element-dark.scss @@ -88,7 +88,7 @@ html.dark { --panel-border-color: var(--panel-main-bg-color-8); --panel-button-active: var(--panel-main-bg-color-10); --panel-button-text-color: var(--panel-main-bg-color-10); - --panel-button-bg-color: var(--panel-color-primary); + --panel-button-bg-color: var(--panel-color-primary); --panel-footer-bg: var(--panel-main-bg-color-9); --panel-footer-border: var(--panel-main-bg-color-7); --panel-text-color: var(--panel-main-bg-color-1); @@ -96,6 +96,7 @@ html.dark { --panel-terminal-tag-bg-color: var(--panel-main-bg-color-10); --panel-terminal-tag-active-bg-color: var(--panel-main-bg-color-10); --panel-terminal-bg-color: var(--panel-main-bg-color-10); + --panel-terminal-tag-active-text-color: var(--panel-color-primary); --panel-logs-bg-color: var(--panel-main-bg-color-9); --el-menu-item-bg-color: var(--panel-main-bg-color-10); diff --git a/frontend/src/styles/element.scss b/frontend/src/styles/element.scss index 391647068..0c2cc675e 100644 --- a/frontend/src/styles/element.scss +++ b/frontend/src/styles/element.scss @@ -45,6 +45,7 @@ html { --panel-footer-border: #e4e7ed; --panel-terminal-tag-bg-color: #efefef; --panel-terminal-tag-active-bg-color: #575758; + --panel-terminal-tag-active-text-color: #ebeef5; --panel-terminal-bg-color: #1e1e1e; --panel-logs-bg-color: #1e1e1e; diff --git a/frontend/src/views/container/image/pull/index.vue b/frontend/src/views/container/image/pull/index.vue index b0624eb93..ecd604359 100644 --- a/frontend/src/views/container/image/pull/index.vue +++ b/frontend/src/views/container/image/pull/index.vue @@ -160,3 +160,8 @@ defineExpose({ acceptParams, }); + diff --git a/frontend/src/views/host/terminal/terminal/index.vue b/frontend/src/views/host/terminal/terminal/index.vue index 5b05cd935..2b1fdf420 100644 --- a/frontend/src/views/host/terminal/terminal/index.vue +++ b/frontend/src/views/host/terminal/terminal/index.vue @@ -433,6 +433,7 @@ onMounted(() => { padding: 0; } :deep(.el-tabs__item.is-active) { + color: var(--panel-terminal-tag-active-text-color); background-color: var(--panel-terminal-tag-active-bg-color); } } diff --git a/frontend/src/views/log/operation/index.vue b/frontend/src/views/log/operation/index.vue index 408c34ee4..f2e531957 100644 --- a/frontend/src/views/log/operation/index.vue +++ b/frontend/src/views/log/operation/index.vue @@ -228,6 +228,9 @@ const loadDetail = (log: string) => { if (log.indexOf('[MonitorStoreDays]') !== -1) { return log.replace('[MonitorStoreDays]', '[' + i18n.global.t('setting.monitor') + ']'); } + if (log.indexOf('[ApiInterfaceStatus]') !== -1) { + return log.replace('[ApiInterfaceStatus]', '[' + i18n.global.t('setting.apiInterface') + ']'); + } return log; }; diff --git a/frontend/src/views/setting/panel/api-interface/index.vue b/frontend/src/views/setting/panel/api-interface/index.vue new file mode 100644 index 000000000..598ab6a7a --- /dev/null +++ b/frontend/src/views/setting/panel/api-interface/index.vue @@ -0,0 +1,183 @@ + + \ No newline at end of file diff --git a/frontend/src/views/setting/panel/index.vue b/frontend/src/views/setting/panel/index.vue index f1aee4fb2..e7aad0c05 100644 --- a/frontend/src/views/setting/panel/index.vue +++ b/frontend/src/views/setting/panel/index.vue @@ -133,6 +133,16 @@ + + + {{ $t('setting.apiInterfaceHelper') }} + + + @@ -177,7 +188,7 @@