feat: 两步验证支持设置标题,区分系统 (#2411)

Refs #2404
This commit is contained in:
ssongliu 2023-09-28 15:46:18 +08:00 committed by GitHub
parent e8564f38ab
commit c6111de050
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 73 additions and 38 deletions

View file

@ -2,10 +2,8 @@ package v1
import (
"errors"
"fmt"
"os"
"path"
"strconv"
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto"
@ -311,23 +309,19 @@ func (b *BaseApi) SystemClean(c *gin.Context) {
// @Tags System Setting
// @Summary Load mfa info
// @Description 获取 mfa 信息
// @Param interval path string true "request"
// @Accept json
// @Param request body dto.MfaCredential true "request"
// @Success 200 {object} mfa.Otp
// @Security ApiKeyAuth
// @Router /settings/mfa/:interval [get]
func (b *BaseApi) GetMFA(c *gin.Context) {
intervalStr, ok := c.Params.Get("interval")
if !ok {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error interval in path"))
return
}
interval, err := strconv.Atoi(intervalStr)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, fmt.Errorf("type conversion failed, err: %v", err))
// @Router /settings/mfa [post]
func (b *BaseApi) LoadMFA(c *gin.Context) {
var req dto.MfaRequest
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
otp, err := mfa.GetOtp("admin", interval)
otp, err := mfa.GetOtp("admin", req.Title, req.Interval)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return

View file

@ -11,6 +11,11 @@ type UserLoginInfo struct {
MfaStatus string `json:"mfaStatus"`
}
type MfaRequest struct {
Title string `json:"title"`
Interval int `json:"interval"`
}
type MfaCredential struct {
Secret string `json:"secret"`
Code string `json:"code"`

View file

@ -30,7 +30,7 @@ func (s *SettingRouter) InitSettingRouter(Router *gin.RouterGroup) {
settingRouter.GET("/time/option", baseApi.LoadTimeZone)
settingRouter.POST("/time/sync", baseApi.SyncTime)
settingRouter.POST("/monitor/clean", baseApi.CleanMonitor)
settingRouter.GET("/mfa/:interval", baseApi.GetMFA)
settingRouter.POST("/mfa", baseApi.LoadMFA)
settingRouter.POST("/mfa/bind", baseApi.MFABind)
settingRouter.POST("/scan", baseApi.ScanSystem)
settingRouter.POST("/clean", baseApi.SystemClean)

View file

@ -18,11 +18,11 @@ type Otp struct {
QrImage string `json:"qrImage"`
}
func GetOtp(username string, interval int) (otp Otp, err error) {
func GetOtp(username, title string, interval int) (otp Otp, err error) {
secret := gotp.RandomSecret(secretLength)
otp.Secret = secret
totp := gotp.NewTOTP(secret, 6, interval, nil)
uri := totp.ProvisioningUri(username, "1Panel")
uri := totp.ProvisioningUri(username, title)
subImg, err := qrcode.Encode(uri, qrcode.Medium, 256)
dist := make([]byte, 3000)
base64.StdEncoding.Encode(dist, subImg)

View file

@ -8648,25 +8648,30 @@ const docTemplate = `{
}
}
},
"/settings/mfa/:interval": {
"get": {
"/settings/mfa": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 mfa 信息",
"consumes": [
"application/json"
],
"tags": [
"System Setting"
],
"summary": "Load mfa info",
"parameters": [
{
"type": "string",
"description": "request",
"name": "interval",
"in": "path",
"required": true
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.MfaCredential"
}
}
],
"responses": {

View file

@ -8641,25 +8641,30 @@
}
}
},
"/settings/mfa/:interval": {
"get": {
"/settings/mfa": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 mfa 信息",
"consumes": [
"application/json"
],
"tags": [
"System Setting"
],
"summary": "Load mfa info",
"parameters": [
{
"type": "string",
"description": "request",
"name": "interval",
"in": "path",
"required": true
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.MfaCredential"
}
}
],
"responses": {

View file

@ -9611,15 +9611,18 @@ paths:
formatEN: reset an expired Password
formatZH: 重置过期密码
paramKeys: []
/settings/mfa/:interval:
get:
/settings/mfa:
post:
consumes:
- application/json
description: 获取 mfa 信息
parameters:
- description: request
in: path
name: interval
in: body
name: request
required: true
type: string
schema:
$ref: '#/definitions/dto.MfaCredential'
responses:
"200":
description: OK

View file

@ -70,6 +70,10 @@ export namespace Setting {
export interface PortUpdate {
serverPort: number;
}
export interface MFARequest {
title: string;
interval: number;
}
export interface MFAInfo {
secret: string;
qrImage: string;

View file

@ -57,8 +57,8 @@ export const cleanMonitors = () => {
return http.post(`/settings/monitor/clean`, {});
};
export const getMFA = (interval: number) => {
return http.get<Setting.MFAInfo>(`/settings/mfa/${interval}`, {});
export const loadMFA = (param: Setting.MFARequest) => {
return http.post<Setting.MFAInfo>(`/settings/mfa`, param);
};
export const loadDaemonJsonPath = () => {

View file

@ -1125,6 +1125,8 @@ const message = {
mfa: 'MFA',
secret: 'Secret',
mfaInterval: 'Refresh interval (s)',
mfaTitleHelper:
'Used to differentiate between different 1Panel hosts. After modification, please rescan or manually add the key information!',
mfaIntervalHelper: 'Please rescan or manually add key information after modifying the refresh time.',
mfaAlert:
'MFA password is generated based on the current time. Please ensure that the server time is synchronized.',

View file

@ -1117,6 +1117,7 @@ const message = {
mfaHelper3: '輸入手機應用上的 6 位數字',
mfaCode: '驗證碼',
mfaInterval: '刷新時間',
mfaTitleHelper: '用於區分不同 1Panel 主機修改後請重新掃描或手動添加密鑰信息',
mfaIntervalHelper: '修改刷新時間後請重新掃描或手動添加密鑰信息',
sslChangeHelper: 'https 設置修改需要重啟服務是否繼續',
sslDisable: '禁用',

View file

@ -1118,6 +1118,7 @@ const message = {
mfaHelper3: '输入手机应用上的 6 位数字',
mfaCode: '验证码',
mfaInterval: '刷新时间',
mfaTitleHelper: '用于区分不同 1Panel 主机修改后请重新扫描或手动添加密钥信息',
mfaIntervalHelper: '修改刷新时间后请重新扫描或手动添加密钥信息',
sslChangeHelper: 'https 设置修改需要重启服务是否继续',
sslDisable: '禁用',

View file

@ -52,6 +52,16 @@
</div>
</span>
</el-form-item>
<el-form-item :label="$t('commons.table.title')" prop="title">
<el-input v-model="form.title">
<template #append>
<el-button @click="loadMfaCodeBefore(formRef)">
{{ $t('commons.button.save') }}
</el-button>
</template>
</el-input>
<span class="input-help">{{ $t('setting.mfaTitleHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('setting.mfaInterval')" prop="interval">
<el-input v-model.number="form.interval">
<template #append>
@ -80,7 +90,7 @@
</div>
</template>
<script lang="ts" setup>
import { bindMFA, getMFA } from '@/api/modules/setting';
import { bindMFA, loadMFA } from '@/api/modules/setting';
import { reactive, ref } from 'vue';
import { Rules, checkNumberRange } from '@/global/form-rules';
import i18n from '@/lang';
@ -96,6 +106,7 @@ const drawerVisiable = ref();
const formRef = ref();
const form = reactive({
title: '1Panel',
code: '',
secret: '',
interval: 30,
@ -134,7 +145,11 @@ const loadMfaCodeBefore = async (formEl: FormInstance | undefined) => {
loadMfaCode();
};
const loadMfaCode = async () => {
const res = await getMFA(form.interval);
let param = {
title: form.title,
interval: form.interval,
};
const res = await loadMFA(param);
form.secret = res.data.secret;
qrImage.value = res.data.qrImage;
};