Browse Source

feat: Add API interface authentication function (#7146)

dev
12 hours ago committed by GitHub
parent
commit
28597721f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 49
      backend/app/api/v1/setting.go
  2. 10
      backend/app/dto/setting.go
  3. 27
      backend/app/service/setting.go
  4. 51
      backend/configs/system.go
  5. 28
      backend/constant/errs.go
  6. 4
      backend/i18n/lang/en.yaml
  7. 4
      backend/i18n/lang/zh-Hant.yaml
  8. 4
      backend/i18n/lang/zh.yaml
  9. 18
      backend/init/hook/hook.go
  10. 1
      backend/init/migration/migrate.go
  11. 16
      backend/init/migration/migrations/v_1_10.go
  12. 61
      backend/middleware/session.go
  13. 2
      backend/router/ro_setting.go
  14. 126
      cmd/server/docs/docs.go
  15. 127
      cmd/server/docs/swagger.json
  16. 93
      cmd/server/docs/swagger.yaml
  17. 20
      cmd/server/main.go
  18. 9
      frontend/src/api/interface/setting.ts
  19. 8
      frontend/src/api/modules/setting.ts
  20. 25
      frontend/src/lang/modules/en.ts
  21. 22
      frontend/src/lang/modules/tw.ts
  22. 14
      frontend/src/lang/modules/zh.ts
  23. 3
      frontend/src/styles/element-dark.scss
  24. 1
      frontend/src/styles/element.scss
  25. 5
      frontend/src/views/container/image/pull/index.vue
  26. 1
      frontend/src/views/host/terminal/terminal/index.vue
  27. 3
      frontend/src/views/log/operation/index.vue
  28. 183
      frontend/src/views/setting/panel/api-interface/index.vue
  29. 57
      frontend/src/views/setting/panel/index.vue

49
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)
}

10
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"`
}

27
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
}

51
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"`
}

28
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

4
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"

4
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: "名稱已存在"

4
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: "名称已存在"

18
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()

1
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)

16
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
},
}

61
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))
}

2
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)
}
}

126
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",

127
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"
}
}
}

93
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: <current_unix_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"

20
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: <current_unix_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() {

9
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;
}
}

8
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<string>(`/settings/api/config/generate/key`);
};
export const updateApiConfig = (param: Setting.ApiConfig) => {
return http.post(`/settings/api/config/update`, param);
};

25
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',

22
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 解析值',

14
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: '立即重启',

3
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);

1
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;

5
frontend/src/views/container/image/pull/index.vue

@ -160,3 +160,8 @@ defineExpose({
acceptParams,
});
</script>
<style scoped>
:deep(.log-container) {
background-color: var(--panel-main-bg-color-10);
}
</style>

1
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);
}
}

3
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;
};

183
frontend/src/views/setting/panel/api-interface/index.vue

@ -0,0 +1,183 @@
<template>
<div>
<el-drawer
v-model="drawerVisible"
:destroy-on-close="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
@close="handleClose"
size="35%"
>
<template #header>
<DrawerHeader :header="$t('setting.apiInterface')" :back="handleClose" />
</template>
<el-alert class="common-prompt" :closable="false" type="warning">
<template #default>
<ul>
<li>
<el-text type="danger">{{ $t('setting.apiInterfaceAlert1') }}</el-text>
</li>
<li>
<el-text type="danger">{{ $t('setting.apiInterfaceAlert2') }}</el-text>
</li>
<li>
{{ $t('setting.apiInterfaceAlert3') }}
<el-link :href="apiURL" type="success" target="_blank" class="mb-0.5 ml-0.5">
{{ apiURL }}
</el-link>
</li>
<li>
{{ $t('setting.apiInterfaceAlert4') }}
<el-link :href="panelURL" type="success" target="_blank" class="mb-0.5 ml-0.5">
{{ panelURL }}
</el-link>
</li>
</ul>
</template>
</el-alert>
<el-form
:model="form"
ref="formRef"
@submit.prevent
v-loading="loading"
label-position="top"
:rules="rules"
>
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item :label="$t('setting.apiKey')" prop="apiKey">
<el-input v-model="form.apiKey" readonly>
<template #suffix>
<CopyButton type="icon" :content="form.apiKey" class="w-30" />
</template>
<template #append>
<el-button @click="resetApiKey()">
{{ $t('commons.button.reset') }}
</el-button>
</template>
</el-input>
<span class="input-help">{{ $t('setting.apiKeyHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('setting.ipWhiteList')" prop="ipWhiteList">
<el-input
type="textarea"
:placeholder="$t('setting.ipWhiteListEgs')"
:rows="4"
v-model="form.ipWhiteList"
/>
<span class="input-help">{{ $t('setting.ipWhiteListHelper') }}</span>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">{{ $t('commons.button.cancel') }}</el-button>
<el-button :disabled="loading" type="primary" @click="onBind(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { generateApiKey, updateApiConfig } from '@/api/modules/setting';
import { reactive, ref } from 'vue';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { ElMessageBox, FormInstance } from 'element-plus';
import DrawerHeader from '@/components/drawer-header/index.vue';
const loading = ref();
const drawerVisible = ref();
const formRef = ref();
const apiURL = `${window.location.protocol}//${window.location.hostname}${
window.location.port ? `:${window.location.port}` : ''
}/1panel/swagger/index.html`;
const panelURL = `https://1panel.cn/docs`;
const form = reactive({
apiKey: '',
ipWhiteList: '',
apiInterfaceStatus: '',
});
const rules = reactive({
ipWhiteList: [Rules.requiredInput],
apiKey: [Rules.requiredInput],
});
interface DialogProps {
apiInterfaceStatus: string;
apiKey: string;
ipWhiteList: string;
}
const emit = defineEmits<{ (e: 'search'): void }>();
const acceptParams = async (params: DialogProps): Promise<void> => {
form.apiInterfaceStatus = params.apiInterfaceStatus;
form.apiKey = params.apiKey;
if (params.apiKey == '') {
await generateApiKey().then((res) => {
form.apiKey = res.data;
});
}
form.ipWhiteList = params.ipWhiteList;
drawerVisible.value = true;
};
const resetApiKey = async () => {
loading.value = true;
ElMessageBox.confirm(i18n.global.t('setting.apiKeyResetHelper'), i18n.global.t('setting.apiKeyReset'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
})
.then(async () => {
await generateApiKey()
.then((res) => {
loading.value = false;
form.apiKey = res.data;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
})
.catch(() => {
loading.value = false;
});
};
const onBind = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
let param = {
apiKey: form.apiKey,
ipWhiteList: form.ipWhiteList,
apiInterfaceStatus: form.apiInterfaceStatus,
};
loading.value = true;
await updateApiConfig(param)
.then(() => {
loading.value = false;
drawerVisible.value = false;
emit('search');
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
});
};
const handleClose = () => {
emit('search');
drawerVisible.value = false;
};
defineExpose({
acceptParams,
});
</script>

57
frontend/src/views/setting/panel/index.vue

@ -133,6 +133,16 @@
</el-input>
</el-form-item>
<el-form-item :label="$t('setting.apiInterface')" prop="apiInterface">
<el-switch
@change="onChangeApiInterfaceStatus"
v-model="form.apiInterfaceStatus"
active-value="enable"
inactive-value="disable"
/>
<span class="input-help">{{ $t('setting.apiInterfaceHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('setting.developerMode')" prop="developerMode">
<el-radio-group
@change="onSave('DeveloperMode', form.developerMode)"
@ -168,6 +178,7 @@
<PanelName ref="panelNameRef" @search="search()" />
<SystemIP ref="systemIPRef" @search="search()" />
<Proxy ref="proxyRef" @search="search()" />
<ApiInterface ref="apiInterfaceRef" @search="search()" />
<Timeout ref="timeoutRef" @search="search()" />
<Network ref="networkRef" @search="search()" />
<HideMenu ref="hideMenuRef" @search="search()" />
@ -177,7 +188,7 @@
<script lang="ts" setup>
import { ref, reactive, onMounted, computed } from 'vue';
import { ElForm } from 'element-plus';
import { ElForm, ElMessageBox } from 'element-plus';
import { getSettingInfo, updateSetting, getSystemAvailable } from '@/api/modules/setting';
import { GlobalStore } from '@/store';
import { useI18n } from 'vue-i18n';
@ -192,6 +203,7 @@ import Proxy from '@/views/setting/panel/proxy/index.vue';
import Network from '@/views/setting/panel/default-network/index.vue';
import HideMenu from '@/views/setting/panel/hidemenu/index.vue';
import ThemeColor from '@/views/setting/panel/theme-color/index.vue';
import ApiInterface from '@/views/setting/panel/api-interface/index.vue';
import { storeToRefs } from 'pinia';
import { getXpackSetting, updateXpackSettingByKey } from '@/utils/xpack';
import { setPrimaryColor } from '@/utils/theme';
@ -241,6 +253,10 @@ const form = reactive({
proxyPasswdKeep: '',
proxyDocker: '',
apiInterfaceStatus: 'disable',
apiKey: '',
ipWhiteList: '',
proHideMenus: ref(i18n.t('setting.unSetting')),
hideMenuList: '',
});
@ -256,6 +272,7 @@ const timeoutRef = ref();
const networkRef = ref();
const hideMenuRef = ref();
const themeColorRef = ref();
const apiInterfaceRef = ref();
const unset = ref(i18n.t('setting.unSetting'));
interface Node {
@ -293,6 +310,9 @@ const search = async () => {
form.proxyUser = res.data.proxyUser;
form.proxyPasswd = res.data.proxyPasswd;
form.proxyPasswdKeep = res.data.proxyPasswdKeep;
form.apiInterfaceStatus = res.data.apiInterfaceStatus;
form.apiKey = res.data.apiKey;
form.ipWhiteList = res.data.ipWhiteList;
const json: Node = JSON.parse(res.data.xpackHideMenu);
const checkedTitles = getCheckedTitles(json);
@ -361,6 +381,41 @@ const onChangeProxy = () => {
proxyDocker: form.proxyDocker,
});
};
const onChangeApiInterfaceStatus = () => {
if (form.apiInterfaceStatus === 'enable') {
apiInterfaceRef.value.acceptParams({
apiInterfaceStatus: form.apiInterfaceStatus,
apiKey: form.apiKey,
ipWhiteList: form.ipWhiteList,
});
return;
}
ElMessageBox.confirm(i18n.t('setting.apiInterfaceClose'), i18n.t('setting.apiInterface'), {
confirmButtonText: i18n.t('commons.button.confirm'),
cancelButtonText: i18n.t('commons.button.cancel'),
})
.then(async () => {
loading.value = true;
await updateSetting({ key: 'ApiInterfaceStatus', value: 'disable' })
.then(() => {
loading.value = false;
search();
MsgSuccess(i18n.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
})
.catch(() => {
apiInterfaceRef.value.acceptParams({
apiInterfaceStatus: 'enable',
apiKey: form.apiKey,
ipWhiteList: form.ipWhiteList,
});
return;
});
};
const onChangeNetwork = () => {
networkRef.value.acceptParams({ defaultNetwork: form.defaultNetwork });
};

Loading…
Cancel
Save