diff --git a/backend/app/api/v1/setting.go b/backend/app/api/v1/setting.go index fa18b87c3..ece5e1921 100644 --- a/backend/app/api/v1/setting.go +++ b/backend/app/api/v1/setting.go @@ -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")) } diff --git a/backend/app/dto/setting.go b/backend/app/dto/setting.go index d2c8786ee..bc7433925 100644 --- a/backend/app/dto/setting.go +++ b/backend/app/dto/setting.go @@ -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"` } diff --git a/backend/app/service/setting.go b/backend/app/service/setting.go index 3954f638d..339083b6e 100644 --- a/backend/app/service/setting.go +++ b/backend/app/service/setting.go @@ -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) diff --git a/backend/init/migration/migrations/init.go b/backend/init/migration/migrations/init.go index 7bc85f642..094bff2d9 100644 --- a/backend/init/migration/migrations/init.go +++ b/backend/init/migration/migrations/init.go @@ -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 }, } diff --git a/backend/router/ro_setting.go b/backend/router/ro_setting.go index 95be7323b..61ee20302 100644 --- a/backend/router/ro_setting.go +++ b/backend/router/ro_setting.go @@ -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) diff --git a/backend/utils/cmd/cmd.go b/backend/utils/cmd/cmd.go index 4c8cbdf67..da4406fed 100644 --- a/backend/utils/cmd/cmd.go +++ b/backend/utils/cmd/cmd.go @@ -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 "" +} diff --git a/backend/utils/ntp/ntp.go b/backend/utils/ntp/ntp.go index 17d09b71f..377f46c44 100644 --- a/backend/utils/ntp/ntp.go +++ b/backend/utils/ntp/ntp.go @@ -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 } diff --git a/frontend/src/api/interface/setting.ts b/frontend/src/api/interface/setting.ts index 2d73692a9..c6112c6c8 100644 --- a/frontend/src/api/interface/setting.ts +++ b/frontend/src/api/interface/setting.ts @@ -9,6 +9,8 @@ export namespace Setting { sessionTimeout: number; localTime: string; + timeZone: string; + ntpSite: string; panelName: string; theme: string; diff --git a/frontend/src/api/modules/setting.ts b/frontend/src/api/modules/setting.ts index 51ffabd62..dfd7374e3 100644 --- a/frontend/src/api/modules/setting.ts +++ b/frontend/src/api/modules/setting.ts @@ -35,8 +35,11 @@ export const handleExpired = (param: Setting.PasswordUpdate) => { return http.post(`/settings/expired/handle`, param); }; -export const syncTime = () => { - return http.post(`/settings/time/sync`, {}); +export const loadTimeZone = () => { + return http.get>(`/settings/time/option`); +}; +export const syncTime = (timeZone: string, ntpSite: string) => { + return http.post(`/settings/time/sync`, { timeZone: timeZone, ntpSite: ntpSite }); }; export const cleanMonitors = () => { diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index bc6c9e889..a73c054c8 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -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.', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 9c4bcc748..29de2a55d 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -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: '开启后会验证手机应用验证码', diff --git a/frontend/src/views/setting/panel/index.vue b/frontend/src/views/setting/panel/index.vue index 378b89eac..8f16aa168 100644 --- a/frontend/src/views/setting/panel/index.vue +++ b/frontend/src/views/setting/panel/index.vue @@ -76,12 +76,9 @@ @@ -95,6 +92,7 @@ + @@ -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(); diff --git a/frontend/src/views/setting/panel/ntp/index.vue b/frontend/src/views/setting/panel/ntp/index.vue new file mode 100644 index 000000000..a7765d04d --- /dev/null +++ b/frontend/src/views/setting/panel/ntp/index.vue @@ -0,0 +1,148 @@ + + + + diff --git a/frontend/src/views/setting/safe/allowips/index.vue b/frontend/src/views/setting/safe/allowips/index.vue index 533bb7582..3609f84d7 100644 --- a/frontend/src/views/setting/safe/allowips/index.vue +++ b/frontend/src/views/setting/safe/allowips/index.vue @@ -11,14 +11,14 @@
- +
diff --git a/frontend/src/views/setting/safe/mfa/index.vue b/frontend/src/views/setting/safe/mfa/index.vue index a4cb7729c..b420d21f5 100644 --- a/frontend/src/views/setting/safe/mfa/index.vue +++ b/frontend/src/views/setting/safe/mfa/index.vue @@ -10,21 +10,14 @@ - + - +