diff --git a/backend/app/api/v1/website.go b/backend/app/api/v1/website.go index 949a5ac34..2c6298303 100644 --- a/backend/app/api/v1/website.go +++ b/backend/app/api/v1/website.go @@ -715,3 +715,46 @@ func (b *BaseApi) UpdateProxyConfigFile(c *gin.Context) { } helper.SuccessWithOutData(c) } + +// @Tags Website +// @Summary Get AuthBasic conf +// @Description 获取密码访问配置 +// @Accept json +// @Param request body request.NginxAuthReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/auths [post] +func (b *BaseApi) GetAuthConfig(c *gin.Context) { + var req request.NginxAuthReq + if err := c.ShouldBindJSON(&req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + res, err := websiteService.GetAuthBasics(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags Website +// @Summary Get AuthBasic conf +// @Description 更新密码访问配置 +// @Accept json +// @Param request body request.NginxAuthUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/auths/update [post] +func (b *BaseApi) UpdateAuthConfig(c *gin.Context) { + var req request.NginxAuthUpdate + if err := c.ShouldBindJSON(&req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + if err := websiteService.UpdateAuthBasic(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} diff --git a/backend/app/dto/nginx.go b/backend/app/dto/nginx.go index 074b8add2..9e9a1f364 100644 --- a/backend/app/dto/nginx.go +++ b/backend/app/dto/nginx.go @@ -28,6 +28,11 @@ type NginxParam struct { Params []string } +type NginxAuth struct { + Username string `json:"username"` + Remark string `json:"remark"` +} + type NginxKey string const ( diff --git a/backend/app/dto/request/nginx.go b/backend/app/dto/request/nginx.go index f2f7e2fbf..0ba0156f8 100644 --- a/backend/app/dto/request/nginx.go +++ b/backend/app/dto/request/nginx.go @@ -36,3 +36,15 @@ type NginxProxyUpdate struct { Content string `json:"content" validate:"required"` Name string `json:"name" validate:"required"` } + +type NginxAuthUpdate struct { + WebsiteID uint `json:"websiteID" validate:"required"` + Operate string `json:"operate" validate:"required"` + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required"` + Remark string `json:"remark"` +} + +type NginxAuthReq struct { + WebsiteID uint `json:"websiteID" validate:"required"` +} diff --git a/backend/app/dto/response/nginx.go b/backend/app/dto/response/nginx.go index a92736e4e..7dcf14dd8 100644 --- a/backend/app/dto/response/nginx.go +++ b/backend/app/dto/response/nginx.go @@ -1,5 +1,7 @@ package response +import "github.com/1Panel-dev/1Panel/backend/app/dto" + type NginxStatus struct { Active string `json:"active"` Accepts string `json:"accepts"` @@ -14,3 +16,8 @@ type NginxParam struct { Name string `json:"name"` Params []string `json:"params"` } + +type NginxAuthRes struct { + Enable bool `json:"enable"` + Items []dto.NginxAuth `json:"items"` +} diff --git a/backend/app/service/website.go b/backend/app/service/website.go index 0dc0c18d2..69d9b88de 100644 --- a/backend/app/service/website.go +++ b/backend/app/service/website.go @@ -14,6 +14,7 @@ import ( "github.com/1Panel-dev/1Panel/backend/utils/nginx/components" "github.com/1Panel-dev/1Panel/backend/utils/nginx/parser" "github.com/1Panel-dev/1Panel/cmd/server/nginx_conf" + "golang.org/x/crypto/bcrypt" "gorm.io/gorm" "os" "path" @@ -70,6 +71,8 @@ type IWebsiteService interface { OperateProxy(req request.WebsiteProxyConfig) (err error) GetProxies(id uint) (res []request.WebsiteProxyConfig, err error) UpdateProxyFile(req request.NginxProxyUpdate) (err error) + GetAuthBasics(req request.NginxAuthReq) (res response.NginxAuthRes, err error) + UpdateAuthBasic(req request.NginxAuthUpdate) (err error) } func NewIWebsiteService() IWebsiteService { @@ -1327,3 +1330,170 @@ func (w WebsiteService) UpdateProxyFile(req request.NginxProxyUpdate) (err error }() return updateNginxConfig(constant.NginxScopeServer, nil, &website) } + +func (w WebsiteService) UpdateAuthBasic(req request.NginxAuthUpdate) (err error) { + var ( + website model.Website + nginxInstall model.AppInstall + params []dto.NginxParam + authContent []byte + authArray []string + ) + website, err = websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) + if err != nil { + return err + } + nginxInstall, err = getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return + } + authPath := fmt.Sprintf("/www/sites/%s/auth_basic/auth.pass", website.Alias) + absoluteAuthPath := path.Join(nginxInstall.GetPath(), authPath) + fileOp := files.NewFileOp() + if !fileOp.Stat(path.Dir(absoluteAuthPath)) { + _ = fileOp.CreateDir(path.Dir(absoluteAuthPath), 755) + } + if !fileOp.Stat(absoluteAuthPath) { + _ = fileOp.CreateFile(absoluteAuthPath) + } + defer func() { + if err != nil { + switch req.Operate { + case "create": + + } + } + }() + + params = append(params, dto.NginxParam{Name: "auth_basic", Params: []string{`"Authentication"`}}) + params = append(params, dto.NginxParam{Name: "auth_basic_user_file", Params: []string{authPath}}) + authContent, err = fileOp.GetContent(absoluteAuthPath) + if err != nil { + return + } + authArray = strings.Split(string(authContent), "\n") + switch req.Operate { + case "disable": + return deleteNginxConfig(constant.NginxScopeServer, params, &website) + case "enable": + return updateNginxConfig(constant.NginxScopeServer, params, &website) + case "create": + for _, line := range authArray { + authParams := strings.Split(line, ":") + username := authParams[0] + if username == req.Username { + err = buserr.New(constant.ErrUsernameIsExist) + return + } + } + var passwdHash []byte + passwdHash, err = bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + return + } + line := fmt.Sprintf("%s:%s\n", req.Username, passwdHash) + if req.Remark != "" { + line = fmt.Sprintf("%s:%s:%s\n", req.Username, passwdHash, req.Remark) + } + authArray = append(authArray, line) + case "edit": + userExist := false + for index, line := range authArray { + authParams := strings.Split(line, ":") + username := authParams[0] + if username == req.Username { + userExist = true + var passwdHash []byte + passwdHash, err = bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + return + } + userPasswd := fmt.Sprintf("%s:%s\n", req.Username, passwdHash) + if req.Remark != "" { + userPasswd = fmt.Sprintf("%s:%s:%s\n", req.Username, passwdHash, req.Remark) + } + authArray[index] = userPasswd + } + } + if !userExist { + err = buserr.New(constant.ErrUsernameIsNotExist) + return + } + case "delete": + deleteIndex := -1 + for index, line := range authArray { + authParams := strings.Split(line, ":") + username := authParams[0] + if username == req.Username { + deleteIndex = index + } + } + if deleteIndex < 0 { + return + } + authArray = append(authArray[:deleteIndex], authArray[deleteIndex+1:]...) + } + + var passFile *os.File + passFile, err = os.Create(absoluteAuthPath) + if err != nil { + return + } + defer passFile.Close() + writer := bufio.NewWriter(passFile) + for _, line := range authArray { + _, err = writer.WriteString(line + "\n") + if err != nil { + return + } + } + err = writer.Flush() + if err != nil { + return + } + return +} + +func (w WebsiteService) GetAuthBasics(req request.NginxAuthReq) (res response.NginxAuthRes, err error) { + var ( + website model.Website + nginxInstall model.AppInstall + authContent []byte + nginxParams []response.NginxParam + ) + website, err = websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) + if err != nil { + return + } + nginxInstall, err = getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return + } + authPath := fmt.Sprintf("/www/sites/%s/auth_basic/auth.pass", website.Alias) + absoluteAuthPath := path.Join(nginxInstall.GetPath(), authPath) + fileOp := files.NewFileOp() + if !fileOp.Stat(absoluteAuthPath) { + return + } + nginxParams, err = getNginxParamsByKeys(constant.NginxScopeServer, []string{"auth_basic"}, &website) + if err != nil { + return + } + res.Enable = len(nginxParams[0].Params) > 0 + authContent, err = fileOp.GetContent(absoluteAuthPath) + authArray := strings.Split(string(authContent), "\n") + for _, line := range authArray { + if line == "" { + continue + } + params := strings.Split(line, ":") + auth := dto.NginxAuth{ + Username: params[0], + } + if len(params) == 3 { + auth.Remark = params[2] + } + res.Items = append(res.Items, auth) + } + return +} diff --git a/backend/constant/errs.go b/backend/constant/errs.go index de3736d02..b78381e11 100644 --- a/backend/constant/errs.go +++ b/backend/constant/errs.go @@ -65,10 +65,12 @@ var ( // website var ( - ErrDomainIsExist = "ErrDomainIsExist" - ErrAliasIsExist = "ErrAliasIsExist" - ErrAppDelete = "ErrAppDelete" - ErrGroupIsUsed = "ErrGroupIsUsed" + ErrDomainIsExist = "ErrDomainIsExist" + ErrAliasIsExist = "ErrAliasIsExist" + ErrAppDelete = "ErrAppDelete" + ErrGroupIsUsed = "ErrGroupIsUsed" + ErrUsernameIsExist = "ErrUsernameIsExist" + ErrUsernameIsNotExist = "ErrUsernameIsNotExist" ) // ssl diff --git a/backend/i18n/lang/en.yaml b/backend/i18n/lang/en.yaml index 96da38a07..dd497fbf7 100644 --- a/backend/i18n/lang/en.yaml +++ b/backend/i18n/lang/en.yaml @@ -43,6 +43,8 @@ ErrDomainIsExist: "Domain is already exist" ErrAliasIsExist: "Alias is already exist" ErrAppDelete: 'Other Website use this App' ErrGroupIsUsed: 'The group is in use and cannot be deleted' +ErrUsernameIsExist: 'Username is already exist' +ErrUsernameIsNotExist: 'Username is not exist' #ssl ErrSSLCannotDelete: "The certificate is being used by the website and cannot be removed" diff --git a/backend/i18n/lang/zh.yaml b/backend/i18n/lang/zh.yaml index 1fce0f5dc..45036643a 100644 --- a/backend/i18n/lang/zh.yaml +++ b/backend/i18n/lang/zh.yaml @@ -43,6 +43,8 @@ ErrDomainIsExist: "域名已存在" ErrAliasIsExist: "代号已存在" ErrAppDelete: '其他网站使用此应用,无法删除' ErrGroupIsUsed: '分组正在使用中,无法删除' +ErrUsernameIsExist: '用户名已存在' +ErrUsernameIsNotExist: '用户不存在' #ssl ErrSSLCannotDelete: "证书正在被网站使用,无法删除" diff --git a/backend/router/ro_website.go b/backend/router/ro_website.go index 6e1bf6a39..fb39d2501 100644 --- a/backend/router/ro_website.go +++ b/backend/router/ro_website.go @@ -55,5 +55,8 @@ func (a *WebsiteRouter) InitWebsiteRouter(Router *gin.RouterGroup) { groupRouter.POST("/proxies", baseApi.GetProxyConfig) groupRouter.POST("/proxies/update", baseApi.UpdateProxyConfig) groupRouter.POST("/proxies/file", baseApi.UpdateProxyConfigFile) + + groupRouter.POST("/auths", baseApi.GetAuthConfig) + groupRouter.POST("/auths/update", baseApi.UpdateAuthConfig) } } diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index b5deae4ed..b3fa8c8af 100644 --- a/cmd/server/docs/docs.go +++ b/cmd/server/docs/docs.go @@ -7904,6 +7904,72 @@ const docTemplate = `{ } } }, + "/websites/auths": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取密码访问配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Get AuthBasic conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxAuthReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/websites/auths/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新密码访问配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Get AuthBasic conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxAuthUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/websites/check": { "post": { "security": [ @@ -12764,6 +12830,43 @@ const docTemplate = `{ } } }, + "request.NginxAuthReq": { + "type": "object", + "required": [ + "websiteID" + ], + "properties": { + "websiteID": { + "type": "integer" + } + } + }, + "request.NginxAuthUpdate": { + "type": "object", + "required": [ + "operate", + "password", + "username", + "websiteID" + ], + "properties": { + "operate": { + "type": "string" + }, + "password": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "username": { + "type": "string" + }, + "websiteID": { + "type": "integer" + } + } + }, "request.NginxConfigFileUpdate": { "type": "object", "required": [ diff --git a/cmd/server/docs/swagger.json b/cmd/server/docs/swagger.json index a2128a077..3114e9d78 100644 --- a/cmd/server/docs/swagger.json +++ b/cmd/server/docs/swagger.json @@ -7897,6 +7897,72 @@ } } }, + "/websites/auths": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取密码访问配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Get AuthBasic conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxAuthReq" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/websites/auths/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新密码访问配置", + "consumes": [ + "application/json" + ], + "tags": [ + "Website" + ], + "summary": "Get AuthBasic conf", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.NginxAuthUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/websites/check": { "post": { "security": [ @@ -12757,6 +12823,43 @@ } } }, + "request.NginxAuthReq": { + "type": "object", + "required": [ + "websiteID" + ], + "properties": { + "websiteID": { + "type": "integer" + } + } + }, + "request.NginxAuthUpdate": { + "type": "object", + "required": [ + "operate", + "password", + "username", + "websiteID" + ], + "properties": { + "operate": { + "type": "string" + }, + "password": { + "type": "string" + }, + "remark": { + "type": "string" + }, + "username": { + "type": "string" + }, + "websiteID": { + "type": "integer" + } + } + }, "request.NginxConfigFileUpdate": { "type": "object", "required": [ diff --git a/cmd/server/docs/swagger.yaml b/cmd/server/docs/swagger.yaml index 1105a11dc..b1a8cea8c 100644 --- a/cmd/server/docs/swagger.yaml +++ b/cmd/server/docs/swagger.yaml @@ -2124,6 +2124,31 @@ definitions: additionalProperties: true type: object type: object + request.NginxAuthReq: + properties: + websiteID: + type: integer + required: + - websiteID + type: object + request.NginxAuthUpdate: + properties: + operate: + type: string + password: + type: string + remark: + type: string + username: + type: string + websiteID: + type: integer + required: + - operate + - password + - username + - websiteID + type: object request.NginxConfigFileUpdate: properties: backup: @@ -8018,6 +8043,46 @@ paths: summary: Page website acme accounts tags: - Website Acme + /websites/auths: + post: + consumes: + - application/json + description: 获取密码访问配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.NginxAuthReq' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Get AuthBasic conf + tags: + - Website + /websites/auths/update: + post: + consumes: + - application/json + description: 更新密码访问配置 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.NginxAuthUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Get AuthBasic conf + tags: + - Website /websites/check: post: consumes: diff --git a/frontend/src/api/interface/website.ts b/frontend/src/api/interface/website.ts index 05c4aaad7..00b12473e 100644 --- a/frontend/src/api/interface/website.ts +++ b/frontend/src/api/interface/website.ts @@ -338,4 +338,26 @@ export namespace Website { name: string; content: string; } + + export interface AuthReq { + websiteID: number; + } + + export interface NginxAuth { + username: string; + remark: string; + } + + export interface AuthConfig { + enable: boolean; + items: NginxAuth[]; + } + + export interface NginxAuthConfig { + websiteID: number; + operate: string; + username: string; + password: string; + remark: string; + } } diff --git a/frontend/src/api/modules/website.ts b/frontend/src/api/modules/website.ts index 7c0d5e3c1..b3b629802 100644 --- a/frontend/src/api/modules/website.ts +++ b/frontend/src/api/modules/website.ts @@ -198,3 +198,11 @@ export const OperateProxyConfig = (req: Website.ProxyReq) => { export const UpdateProxyConfigFile = (req: Website.ProxyFileUpdate) => { return http.post(`/websites/proxies/file`, req); }; + +export const GetAuthConfig = (req: Website.AuthReq) => { + return http.post(`/websites/auths`, req); +}; + +export const OperateAuthConfig = (req: Website.NginxAuthConfig) => { + return http.post(`/websites/auths/update`, req); +}; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 0b9958449..15db1e213 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -1224,6 +1224,10 @@ const message = { replaceText: 'Replacement text, can be empty', replacedErr: 'The replaced text cannot be empty', replacedErr2: 'The replaced text cannot be repeated', + basicAuth: 'Password Access', + editBasicAuthHelper: + 'The password is asymmetrically encrypted and cannot be echoed. Editing needs to reset the password', + createPassword: 'Generate password', }, php: { short_open_tag: 'Short tag support', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index bc1c4a755..794fca826 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -1223,6 +1223,9 @@ const message = { replaceText: '替换的文本,可为空', replacedErr: '被替换的文本不能为空', replacedErr2: '被替换的文本不能重复', + basicAuth: '密码访问', + editBasicAuthHelper: '密码为非对称加密,无法回显,编辑需要重新设置密码', + createPassword: '生成密码', }, php: { short_open_tag: '短标签支持', diff --git a/frontend/src/views/website/website/config/basic/auth-basic/create/index.vue b/frontend/src/views/website/website/config/basic/auth-basic/create/index.vue new file mode 100644 index 000000000..c7728d917 --- /dev/null +++ b/frontend/src/views/website/website/config/basic/auth-basic/create/index.vue @@ -0,0 +1,118 @@ + + + diff --git a/frontend/src/views/website/website/config/basic/auth-basic/index.vue b/frontend/src/views/website/website/config/basic/auth-basic/index.vue new file mode 100644 index 000000000..33d1ebdd0 --- /dev/null +++ b/frontend/src/views/website/website/config/basic/auth-basic/index.vue @@ -0,0 +1,118 @@ + + + diff --git a/frontend/src/views/website/website/config/basic/index.vue b/frontend/src/views/website/website/config/basic/index.vue index bc8ff58c5..abf47b7ae 100644 --- a/frontend/src/views/website/website/config/basic/index.vue +++ b/frontend/src/views/website/website/config/basic/index.vue @@ -15,14 +15,17 @@ + + + - + - + - + @@ -38,6 +41,7 @@ import HTTPS from './https/index.vue'; import SitePath from './site-folder/index.vue'; import Rewrite from './rewrite/index.vue'; import Proxy from './proxy/index.vue'; +import AuthBasic from './auth-basic/index.vue'; const props = defineProps({ id: {