1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-03-14 01:34:47 +08:00

feat: 时间同步增加时区、同步地址自定义设置 (#1102)

This commit is contained in:
ssongliu 2023-05-22 17:45:39 +08:00 committed by GitHub
parent ce19107c95
commit 53600900f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 281 additions and 74 deletions

View File

@ -2,14 +2,12 @@ package v1
import (
"errors"
"time"
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/mfa"
"github.com/1Panel-dev/1Panel/backend/utils/ntp"
"github.com/gin-gonic/gin"
)
@ -189,25 +187,40 @@ func (b *BaseApi) HandlePasswordExpired(c *gin.Context) {
}
// @Tags System Setting
// @Summary Sync system time
// @Description 系统时间同步
// @Success 200 {string} ntime
// @Summary Load time zone options
// @Description 加载系统可用时区
// @Success 200 {string}
// @Security ApiKeyAuth
// @Router /settings/time/sync [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFuntions":[],"formatZH":"系统时间同步","formatEN":"sync system time"}
func (b *BaseApi) SyncTime(c *gin.Context) {
ntime, err := ntp.GetRemoteTime()
// @Router /settings/time/option [get]
func (b *BaseApi) LoadTimeZone(c *gin.Context) {
zones, err := settingService.LoadTimeZone()
if err != nil {
helper.SuccessWithData(c, time.Now().Format("2006-01-02 15:04:05 MST -0700"))
return
}
ts := ntime.Format("2006-01-02 15:04:05")
if err := ntp.UpdateSystemDate(ts); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, zones)
}
// @Tags System Setting
// @Summary Sync system time
// @Description 系统时间同步
// @Accept json
// @Param request body dto.SyncTimeZone true "request"
// @Success 200 {string} ntime
// @Security ApiKeyAuth
// @Router /settings/time/sync [post]
// @x-panel-log {"bodyKeys":["ntpSite", "timeZone"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"系统时间同步[ntpSite]-[timeZone]","formatEN":"sync system time [ntpSite]-[timeZone]"}
func (b *BaseApi) SyncTime(c *gin.Context) {
var req dto.SyncTimeZone
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
ntime, err := settingService.SyncTime(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, ntime.Format("2006-01-02 15:04:05 MST -0700"))
}

View File

@ -9,6 +9,8 @@ type SettingInfo struct {
SessionTimeout string `json:"sessionTimeout"`
LocalTime string `json:"localTime"`
TimeZone string `json:"timeZone"`
NtpSite string `json:"ntpSite"`
Port string `json:"port"`
PanelName string `json:"panelName"`
@ -109,6 +111,11 @@ type UpgradeInfo struct {
ReleaseNote string `json:"releaseNote"`
}
type SyncTimeZone struct {
NtpSite string `json:"ntpSite"`
TimeZone string `json:"timeZone"`
}
type Upgrade struct {
Version string `json:"version"`
}

View File

@ -19,6 +19,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/encrypt"
"github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/1Panel-dev/1Panel/backend/utils/ntp"
"github.com/1Panel-dev/1Panel/backend/utils/ssl"
"github.com/gin-gonic/gin"
)
@ -27,12 +28,14 @@ type SettingService struct{}
type ISettingService interface {
GetSettingInfo() (*dto.SettingInfo, error)
LoadTimeZone() ([]string, error)
Update(key, value string) error
UpdatePassword(c *gin.Context, old, new string) error
UpdatePort(port uint) error
UpdateSSL(c *gin.Context, req dto.SSLUpdate) error
LoadFromCert() (*dto.SSLInfo, error)
HandlePasswordExpired(c *gin.Context, old, new string) error
SyncTime(req dto.SyncTimeZone) (time.Time, error)
}
func NewISettingService() ISettingService {
@ -60,6 +63,14 @@ func (u *SettingService) GetSettingInfo() (*dto.SettingInfo, error) {
return &info, err
}
func (u *SettingService) LoadTimeZone() ([]string, error) {
std, err := cmd.Exec("timedatectl list-timezones")
if err != nil {
return []string{}, nil
}
return strings.Split(std, "\n"), err
}
func (u *SettingService) Update(key, value string) error {
if key == "ExpirationDays" {
timeout, _ := strconv.Atoi(value)
@ -82,6 +93,27 @@ func (u *SettingService) Update(key, value string) error {
return nil
}
func (u *SettingService) SyncTime(req dto.SyncTimeZone) (time.Time, error) {
ntime, err := ntp.GetRemoteTime(req.NtpSite)
if err != nil {
return ntime, err
}
ts := ntime.Format("2006-01-02 15:04:05")
if err := ntp.UpdateSystemTime(ts, req.TimeZone); err != nil {
return ntime, err
}
if err := settingRepo.Update("TimeZone", req.TimeZone); err != nil {
return ntime, err
}
if err := settingRepo.Update("NtpSite", req.NtpSite); err != nil {
return ntime, err
}
return ntime, nil
}
func (u *SettingService) UpdatePort(port uint) error {
if common.ScanPort(int(port)) {
return buserr.WithDetail(constant.ErrPortInUsed, port, nil)

View File

@ -345,6 +345,12 @@ var AddBindAndAllowIPs = &gormigrate.Migration{
if err := tx.Create(&model.Setting{Key: "AllowIPs", Value: ""}).Error; err != nil {
return err
}
if err := tx.Create(&model.Setting{Key: "TimeZone", Value: common.LoadTimeZone()}).Error; err != nil {
return err
}
if err := tx.Create(&model.Setting{Key: "NtpSite", Value: "pool.ntp.org:123"}).Error; err != nil {
return err
}
return nil
},
}

View File

@ -26,6 +26,7 @@ func (s *SettingRouter) InitSettingRouter(Router *gin.RouterGroup) {
settingRouter.POST("/ssl/update", baseApi.UpdateSSL)
settingRouter.GET("/ssl/info", baseApi.LoadFromCert)
settingRouter.POST("/password/update", baseApi.UpdatePassword)
settingRouter.GET("/time/option", baseApi.LoadTimeZone)
settingRouter.POST("/time/sync", baseApi.SyncTime)
settingRouter.POST("/monitor/clean", baseApi.CleanMonitor)
settingRouter.GET("/mfa", baseApi.GetMFA)

View File

@ -121,3 +121,11 @@ func HasNoPasswordSudo() bool {
err2 := cmd2.Run()
return err2 == nil
}
func SudoHandleCmd() string {
cmd := exec.Command("sudo", "-n", "ls")
if err := cmd.Run(); err == nil {
return "sudo "
}
return ""
}

View File

@ -30,8 +30,8 @@ type packet struct {
TxTimeFrac uint32
}
func GetRemoteTime() (time.Time, error) {
conn, err := net.Dial("udp", "pool.ntp.org:123")
func GetRemoteTime(site string) (time.Time, error) {
conn, err := net.Dial("udp", site)
if err != nil {
return time.Time{}, fmt.Errorf("failed to connect: %v", err)
}
@ -59,11 +59,17 @@ func GetRemoteTime() (time.Time, error) {
return showtime, nil
}
func UpdateSystemDate(dateTime string) error {
func UpdateSystemTime(dateTime, timezone string) error {
system := runtime.GOOS
if system == "linux" {
if _, err := cmd.Execf(`date -s "` + dateTime + `"`); err != nil {
return fmt.Errorf("update system date failed, err: %v", err)
stdout, err := cmd.Execf(`%s timedatectl set-timezone "%s"`, cmd.SudoHandleCmd(), timezone)
if err != nil {
return fmt.Errorf("update system time zone failed, stdout: %s, err: %v", stdout, err)
}
stdout2, err := cmd.Execf(`%s timedatectl set-time "%s"`, cmd.SudoHandleCmd(), dateTime)
if err != nil {
return fmt.Errorf("update system time failed,stdout: %s, err: %v", stdout2, err)
}
return nil
}

View File

@ -9,6 +9,8 @@ export namespace Setting {
sessionTimeout: number;
localTime: string;
timeZone: string;
ntpSite: string;
panelName: string;
theme: string;

View File

@ -35,8 +35,11 @@ export const handleExpired = (param: Setting.PasswordUpdate) => {
return http.post(`/settings/expired/handle`, param);
};
export const syncTime = () => {
return http.post<string>(`/settings/time/sync`, {});
export const loadTimeZone = () => {
return http.get<Array<string>>(`/settings/time/option`);
};
export const syncTime = (timeZone: string, ntpSite: string) => {
return http.post<string>(`/settings/time/sync`, { timeZone: timeZone, ntpSite: ntpSite });
};
export const cleanMonitors = () => {

View File

@ -885,7 +885,12 @@ const message = {
sessionTimeoutHelper:
'If you do not operate the panel for more than {0} seconds, the panel automatically logs out',
syncTime: 'Server time',
second: ' S',
timeZone: 'Time Zone',
timeZoneHelper: 'Timezone modification depends on the system timedatectl service.',
timeZoneCN: 'Bei Jing',
timeZoneAM: 'Los Angeles',
timeZoneNY: 'New York',
syncSite: 'Ntp Server',
changePassword: 'Password change',
oldPassword: 'Original password',
newPassword: 'New password',
@ -949,6 +954,7 @@ const message = {
allowIPsWarnning:
'设After setting the authorized IP address, only the IP address in the setting can access the 1Panel service. Do you want to continue?',
allowIPsHelper1: 'If the authorized IP address is empty, the authorized IP address is canceled',
allowIPEgs: 'e.g. 172.16.10.111',
mfa: 'MFA',
mfaAlert:
'MFA password is generated based on the current time. Please ensure that the server time is synchronized.',

View File

@ -235,7 +235,7 @@ const message = {
terminal: '终端',
settings: '面板设置',
toolbox: '工具箱',
logs: '面板日志',
logs: '日志审计',
runtime: '运行环境',
},
home: {
@ -741,7 +741,7 @@ const message = {
commands: '快捷命令',
files: '文件管理',
backups: '备份账号',
logs: '面板日志',
logs: '日志审计',
settings: '面板设置',
cronjobs: '计划任务',
databases: '数据库',
@ -885,7 +885,12 @@ const message = {
sessionTimeoutError: '最小超时时间为 300 ',
sessionTimeoutHelper: '如果用户超过 {0} 秒未操作面板面板将自动退出登录',
syncTime: '服务器时间',
second: ' ',
timeZone: '时区',
timeZoneHelper: '时区修改依赖于系统 timedatectl 服务',
timeZoneCN: '北京',
timeZoneAM: '洛杉矶',
timeZoneNY: '纽约',
syncSite: '同步地址',
changePassword: '密码修改',
oldPassword: '原密码',
newPassword: '新密码',
@ -976,6 +981,7 @@ const message = {
allowIPsHelper: '设置授权 IP 仅有设置中的 IP 可以访问 1Panel 服务',
allowIPsWarnning: '设置授权 IP 仅有设置中的 IP 可以访问 1Panel 服务是否继续',
allowIPsHelper1: '授权 IP 为空时则取消授权 IP',
allowIPEgs: '172.16.10.111',
mfa: '两步验证',
mfaAlert: '两步验证密码是基于当前时间生成请确保服务器时间已同步',
mfaHelper: '开启后会验证手机应用验证码',

View File

@ -76,12 +76,9 @@
<el-form-item :label="$t('setting.syncTime')">
<el-input disabled v-model="form.localTime">
<template #append>
<el-button v-show="!show" @click="onSyncTime" icon="Refresh">
{{ $t('commons.button.sync') }}
<el-button v-show="!show" @click="onChangeNtp" icon="Setting">
{{ $t('commons.button.set') }}
</el-button>
<div style="width: 45px" v-show="show">
<span>{{ count }} {{ $t('setting.second') }}</span>
</div>
</template>
</el-input>
</el-form-item>
@ -95,6 +92,7 @@
<UserName ref="userNameRef" />
<PanelName ref="panelNameRef" @search="search()" />
<Timeout ref="timeoutRef" @search="search()" />
<Ntp ref="ntpRef" @search="search()" />
</div>
</template>
@ -102,7 +100,7 @@
import { ref, reactive, onMounted, computed } from 'vue';
import { ElForm } from 'element-plus';
import LayoutContent from '@/layout/layout-content.vue';
import { syncTime, getSettingInfo, updateSetting, getSystemAvailable } from '@/api/modules/setting';
import { getSettingInfo, updateSetting, getSystemAvailable } from '@/api/modules/setting';
import { GlobalStore } from '@/store';
import { useI18n } from 'vue-i18n';
import { useTheme } from '@/hooks/use-theme';
@ -111,6 +109,7 @@ import Password from '@/views/setting/panel/password/index.vue';
import UserName from '@/views/setting/panel/username/index.vue';
import Timeout from '@/views/setting/panel/timeout/index.vue';
import PanelName from '@/views/setting/panel/name/index.vue';
import Ntp from '@/views/setting/panel/ntp/index.vue';
const loading = ref(false);
const i18n = useI18n();
@ -124,21 +123,21 @@ const form = reactive({
email: '',
sessionTimeout: 0,
localTime: '',
timeZone: '',
ntpSite: '',
panelName: '',
theme: '',
language: '',
complexityVerification: '',
});
const timer = ref();
const TIME_COUNT = ref(10);
const count = ref();
const show = ref();
const userNameRef = ref();
const passwordRef = ref();
const panelNameRef = ref();
const timeoutRef = ref();
const ntpRef = ref();
const search = async () => {
const res = await getSettingInfo();
@ -146,6 +145,8 @@ const search = async () => {
form.password = '******';
form.sessionTimeout = Number(res.data.sessionTimeout);
form.localTime = res.data.localTime;
form.timeZone = res.data.timeZone;
form.ntpSite = res.data.ntpSite;
form.panelName = res.data.panelName;
form.theme = res.data.theme;
form.language = res.data.language;
@ -164,6 +165,9 @@ const onChangeTitle = () => {
const onChangeTimeout = () => {
timeoutRef.value.acceptParams({ sessionTimeout: form.sessionTimeout });
};
const onChangeNtp = () => {
ntpRef.value.acceptParams({ localTime: form.localTime, timeZone: form.timeZone, ntpSite: form.ntpSite });
};
const onSave = async (key: string, val: any) => {
loading.value = true;
@ -193,34 +197,6 @@ const onSave = async (key: string, val: any) => {
});
};
function countdown() {
count.value = TIME_COUNT.value;
show.value = true;
timer.value = setInterval(() => {
if (count.value > 0 && count.value <= TIME_COUNT.value) {
count.value--;
} else {
show.value = false;
clearInterval(timer.value);
timer.value = null;
}
}, 1000);
}
const onSyncTime = async () => {
loading.value = true;
await syncTime()
.then((res) => {
loading.value = false;
form.localTime = res.data;
countdown();
MsgSuccess(i18n.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
};
onMounted(() => {
search();
getSystemAvailable();

View File

@ -0,0 +1,148 @@
<template>
<div>
<el-drawer v-model="drawerVisiable" :destroy-on-close="true" :close-on-click-modal="false" size="30%">
<template #header>
<DrawerHeader :header="$t('setting.syncTime')" :back="handleClose" />
</template>
<el-alert v-if="canChangeZone()" style="margin-bottom: 20px" :closable="false" type="warning">
<template #default>
<span>
<span>{{ $t('setting.timeZoneHelper') }}</span>
</span>
</template>
</el-alert>
<el-form ref="formRef" label-position="top" :model="form" @submit.prevent v-loading="loading">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item :label="$t('setting.timeZone')" prop="timeZone" :rules="Rules.requiredInput">
<el-select filterable :disabled="canChangeZone()" v-model="form.timeZone">
<el-option v-for="item in zones" :key="item" :label="item" :value="item" />
</el-select>
<el-button
:disabled="canChangeZone()"
type="primary"
link
class="tagClass"
@click="form.timeZone = 'Asia/Shanghai'"
>
{{ $t('setting.timeZoneCN') }}
</el-button>
<el-button
:disabled="canChangeZone()"
type="primary"
link
class="tagClass"
@click="form.timeZone = 'America/Los_Angeles'"
>
{{ $t('setting.timeZoneAM') }}
</el-button>
<el-button
:disabled="canChangeZone()"
type="primary"
link
class="tagClass"
@click="form.timeZone = 'America/New_York'"
>
{{ $t('setting.timeZoneNY') }}
</el-button>
</el-form-item>
<el-form-item :label="$t('setting.syncTime')" prop="localTime">
<el-input v-model="form.localTime" disabled />
</el-form-item>
<el-form-item :label="$t('setting.syncSite')" prop="ntpSite" :rules="Rules.requiredInput">
<el-input v-model="form.ntpSite" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button :disabled="loading" type="primary" @click="onSyncTime(formRef)">
{{ $t('commons.button.sync') }}
</el-button>
</span>
</template>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { loadTimeZone, syncTime } from '@/api/modules/setting';
import { FormInstance } from 'element-plus';
import { Rules } from '@/global/form-rules';
const emit = defineEmits<{ (e: 'search'): void }>();
interface DialogProps {
timeZone: string;
localTime: string;
ntpSite: string;
}
const drawerVisiable = ref();
const loading = ref();
const zones = ref<Array<string>>([]);
const oldTimeZone = ref();
const form = reactive({
timeZone: '',
localTime: '',
ntpSite: '',
});
const formRef = ref<FormInstance>();
const acceptParams = (params: DialogProps): void => {
loadTimeZones();
oldTimeZone.value = params.timeZone;
form.timeZone = params.timeZone;
form.localTime = params.localTime;
form.ntpSite = params.ntpSite;
drawerVisiable.value = true;
};
const canChangeZone = () => {
return zones.value.length === 0;
};
const loadTimeZones = async () => {
const res = await loadTimeZone();
zones.value = res.data;
};
const onSyncTime = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
loading.value = true;
await syncTime(form.timeZone, form.ntpSite)
.then((res) => {
loading.value = false;
form.localTime = res.data;
emit('search');
handleClose();
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
});
};
const handleClose = () => {
drawerVisiable.value = false;
};
defineExpose({
acceptParams,
});
</script>
<style scoped lang="scss">
.tagClass {
margin-top: 5px;
}
</style>

View File

@ -11,14 +11,14 @@
<table style="width: 100%" class="tab-table">
<tr v-if="allowIPs.length !== 0">
<th scope="col" width="90%" align="left">
<label>IP</label>
<label>{{ $t('setting.allowIPs') }}</label>
</th>
<th align="left"></th>
</tr>
<tr v-for="(row, index) in allowIPs" :key="index">
<td width="90%">
<el-input
:placeholder="$t('container.serverExample')"
:placeholder="$t('setting.allowIPEgs')"
style="width: 100%"
v-model="row.value"
/>

View File

@ -10,21 +10,14 @@
<template #header>
<DrawerHeader :header="$t('setting.mfa')" :back="handleClose" />
</template>
<el-alert :closable="false" type="warning">
<el-alert style="margin-bottom: 20px" :closable="false" type="warning">
<template #default>
<span>
<span>{{ $t('setting.mfaAlert') }}</span>
</span>
</template>
</el-alert>
<el-form
:model="form"
style="margin-top: 20px"
ref="formRef"
@submit.prevent
v-loading="loading"
label-position="top"
>
<el-form :model="form" ref="formRef" @submit.prevent v-loading="loading" label-position="top">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item :label="$t('setting.mfaHelper1')">