diff --git a/server/README.md b/server/README.md index 0dc544e..b1b9ecf 100644 --- a/server/README.md +++ b/server/README.md @@ -53,16 +53,16 @@ jwt: revokeTokensOnLogout: true # Address for custom STUN server +# Note: You can disable the STUN service by setting it to 'disable' (e.g., in a LAN environment) stun: stun.l.google.com:19302 # Address and authentication for custom TURN server turn: - turnAddr: turn.cloudflare.com:3478 + turnAddr: example_addr turnUser: example_user turnCred: example_cred ``` - ## Compile & Deploy Note: Use Linux operating system (x86-64). This build process is not compatible with ARM, Windows or macOS. diff --git a/server/README_ZH.md b/server/README_ZH.md index acfe6dd..07ba0c3 100644 --- a/server/README_ZH.md +++ b/server/README_ZH.md @@ -50,10 +50,12 @@ jwt: # 在帐号登出时是否使所有 jwt token 失效。默认为 true revokeTokensOnLogout: true +# 自定义 STUN 服务器的地址 +# 注意:可以设置为“disable”来禁用 STUN 服务(例如在局域网环境中使用时) stun: stun.l.google.com:19302 turn: - turnAddr: turn.cloudflare.com:3478 + turnAddr: example_addr turnUser: example_user turnCred: example_cred ``` diff --git a/server/router/vm.go b/server/router/vm.go index 8e47e26..fe34b0b 100644 --- a/server/router/vm.go +++ b/server/router/vm.go @@ -43,4 +43,6 @@ func vmRouter(r *gin.Engine) { api.GET("/vm/mdns", service.GetMdnsState) // get mDNS state api.POST("/vm/mdns/enable", service.EnableMdns) // enable mDNS api.POST("/vm/mdns/disable", service.DisableMdns) // disable mDNS + + api.POST("/vm/system/reboot", service.Reboot) // reboot system } diff --git a/server/service/vm/mdns.go b/server/service/vm/mdns.go index b588997..dcde875 100644 --- a/server/service/vm/mdns.go +++ b/server/service/vm/mdns.go @@ -71,6 +71,7 @@ func (s *Service) DisableMdns(c *gin.Context) { } _ = os.Remove(AvahiDaemonPid) + _ = os.Remove(AvahiDaemonScript) rsp.OkRsp(c) log.Debugf("avahi-daemon stopped") diff --git a/server/service/vm/system.go b/server/service/vm/system.go new file mode 100644 index 0000000..a5056a3 --- /dev/null +++ b/server/service/vm/system.go @@ -0,0 +1,25 @@ +package vm + +import ( + "NanoKVM-Server/proto" + "os/exec" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" +) + +func (s *Service) Reboot(c *gin.Context) { + var rsp proto.Response + + log.Println("reboot system...") + + err := exec.Command("reboot").Run() + if err != nil { + rsp.ErrRsp(c, -1, "operation failed") + log.Errorf("failed to reboot: %s", err) + return + } + + rsp.OkRsp(c) + log.Debug("system rebooted") +} diff --git a/web/src/api/vm.ts b/web/src/api/vm.ts index 46b6ee4..f3957ac 100644 --- a/web/src/api/vm.ts +++ b/web/src/api/vm.ts @@ -91,3 +91,8 @@ export function enableMdns() { export function disableMdns() { return http.post('/api/vm/mdns/disable'); } + +// reboot +export function reboot() { + return http.post('/api/vm/system/reboot'); +} diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index b6d893f..614d6ca 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -207,7 +207,11 @@ const en = { disk: 'Virtual Disk', diskDesc: 'Mount virtual U-disk on the remote host', network: 'Virtual Network', - networkDesc: 'Mount virtual network card on the remote host' + networkDesc: 'Mount virtual network card on the remote host', + reboot: 'Reboot', + rebootDesc: 'Are you sure you want to reboot NanoKVM?', + okBtn: 'Yes', + cancelBtn: 'No' }, tailscale: { title: 'Tailscale', @@ -241,7 +245,7 @@ const en = { deviceIP: 'Device IP', account: 'Account', logout: 'Logout', - logout2: 'Confirm logout', + logoutDesc: 'Are you sure you want to logout?', uninstall: 'Uninstall Tailscale', okBtn: 'Yes', cancelBtn: 'No' @@ -261,7 +265,10 @@ const en = { webAccount: 'Web Account Name', password: 'Password', updateBtn: 'Change', - logoutBtn: 'Logout' + logoutBtn: 'Logout', + logoutDesc: 'Are you sure you want to logout?', + okBtn: 'Yes', + cancelBtn: 'No' } }, error: { diff --git a/web/src/i18n/locales/zh.ts b/web/src/i18n/locales/zh.ts index d768fe4..0e62cbb 100644 --- a/web/src/i18n/locales/zh.ts +++ b/web/src/i18n/locales/zh.ts @@ -233,7 +233,7 @@ const zh = { deviceIP: '设备地址', account: '账号', logout: '退出', - logout2: '确认退出?', + logoutDesc: '确定要退出吗?', uninstall: '卸载 Tailscale', okBtn: '确认', cancelBtn: '取消' @@ -253,8 +253,22 @@ const zh = { webAccount: '网页帐号', password: '密码', updateBtn: '修改', - logoutBtn: '退出' + logoutBtn: '退出', + logoutDesc: '确定要退出吗?', + confirm: '确定', + cancel: '取消' } + }, + error: { + title: '我们遇到了问题', + refresh: '刷新' + }, + fullscreen: { + toggle: '切换全屏' + }, + menu: { + collapse: '收起', + expand: '展开' } } }; diff --git a/web/src/pages/desktop/menu/index.tsx b/web/src/pages/desktop/menu/index.tsx index 171068c..97cc63c 100644 --- a/web/src/pages/desktop/menu/index.tsx +++ b/web/src/pages/desktop/menu/index.tsx @@ -26,16 +26,45 @@ export const Menu = () => { const [menuDisabledItems, setMenuDisabledItems] = useAtom(menuDisabledItemsAtom); const [isMenuOpen, setIsMenuOpen] = useState(true); + const [bounds, setBounds] = useState({ left: 0, right: 0, top: 0, bottom: 0 }); const nodeRef = useRef(null); useEffect(() => { + // disabled menu items const items = getMenuDisabledItems(); setMenuDisabledItems(items); + + // react-draggable bounds + const handleResize = () => { + if (!nodeRef.current) return; + + const elementRect = nodeRef.current.getBoundingClientRect(); + const width = (window.innerWidth - elementRect.width) / 2; + + setBounds({ + left: -width, + top: -10, + right: width, + bottom: window.innerHeight - elementRect.height - 10 + }); + }; + + handleResize(); + + window.addEventListener('resize', handleResize); + return () => { + window.removeEventListener('resize', handleResize); + }; }, []); return ( - +
{ const { t } = useTranslation(); @@ -25,18 +25,6 @@ export const Account = () => { navigate('/auth/password'); } - async function logout() { - api.logout().then((rsp) => { - if (rsp.code !== 0) { - console.log(rsp.msg); - return; - } - - removeToken(); - navigate('/auth/login'); - }); - } - return ( <>
{t('settings.account.title')}
@@ -60,11 +48,7 @@ export const Account = () => {
-
- -
+ ); }; diff --git a/web/src/pages/desktop/menu/settings/account/logout.tsx b/web/src/pages/desktop/menu/settings/account/logout.tsx new file mode 100644 index 0000000..1425a35 --- /dev/null +++ b/web/src/pages/desktop/menu/settings/account/logout.tsx @@ -0,0 +1,40 @@ +import { LogoutOutlined } from '@ant-design/icons'; +import { Button, Popconfirm } from 'antd'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; + +import * as api from '@/api/auth.ts'; +import { removeToken } from '@/lib/cookie.ts'; + +export const Logout = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + + function logout() { + api.logout().then((rsp) => { + if (rsp.code !== 0) { + console.log(rsp.msg); + return; + } + + removeToken(); + navigate('/auth/login'); + }); + } + + return ( +
+ + + +
+ ); +}; diff --git a/web/src/pages/desktop/menu/settings/device/index.tsx b/web/src/pages/desktop/menu/settings/device/index.tsx index 95e0a5a..1f48838 100644 --- a/web/src/pages/desktop/menu/settings/device/index.tsx +++ b/web/src/pages/desktop/menu/settings/device/index.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'; import { Mdns } from './mdns.tsx'; import { Oled } from './oled.tsx'; +import { Reboot } from './reboot.tsx'; import { Ssh } from './ssh.tsx'; import { VirtualDevices } from './virtual-devices.tsx'; import { Wifi } from './wifi.tsx'; @@ -22,6 +23,9 @@ export const Device = () => {
+ + + ); }; diff --git a/web/src/pages/desktop/menu/settings/device/reboot.tsx b/web/src/pages/desktop/menu/settings/device/reboot.tsx new file mode 100644 index 0000000..e91607a --- /dev/null +++ b/web/src/pages/desktop/menu/settings/device/reboot.tsx @@ -0,0 +1,59 @@ +import { useState } from 'react'; +import { ReloadOutlined } from '@ant-design/icons'; +import { Button, Popconfirm } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import * as api from '@/api/vm.ts'; + +export const Reboot = () => { + const { t } = useTranslation(); + + const [isLoading, setIsLoading] = useState(false); + + function reboot() { + if (isLoading) return; + setIsLoading(true); + + const timeoutId = setTimeout(() => { + window.location.reload(); + }, 30000); + + api + .reboot() + .then((rsp) => { + if (rsp.code !== 0) { + console.log(rsp.msg); + setIsLoading(false); + clearTimeout(timeoutId); + } + }) + .catch((err) => { + console.log(err); + setIsLoading(false); + clearTimeout(timeoutId); + }); + } + + return ( +
+ + + +
+ ); +}; diff --git a/web/src/pages/desktop/menu/settings/tailscale/device.tsx b/web/src/pages/desktop/menu/settings/tailscale/device.tsx index 81dc5c6..a168570 100644 --- a/web/src/pages/desktop/menu/settings/tailscale/device.tsx +++ b/web/src/pages/desktop/menu/settings/tailscale/device.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { LogoutOutlined } from '@ant-design/icons'; -import { Button, Switch } from 'antd'; +import { Button, Divider, Popconfirm, Switch } from 'antd'; import { useTranslation } from 'react-i18next'; import * as api from '@/api/extensions/tailscale.ts'; @@ -17,7 +17,6 @@ export const Device = ({ status, onLogout }: DeviceProps) => { const [isRunning, setIsRunning] = useState(false); const [isUpdating, setIsUpdating] = useState(false); - const [isConfirmation, setIsConfirmation] = useState(false); const [isLogging, setIsLogging] = useState(false); const [errMsg, setErrMsg] = useState(''); @@ -82,30 +81,27 @@ export const Device = ({ status, onLogout }: DeviceProps) => { {t('settings.tailscale.account')} {status.account}
+ -
- {!isConfirmation ? ( - - ) : ( +
+ - )} +
{errMsg && {errMsg}} diff --git a/web/src/pages/desktop/menu/settings/tailscale/install.tsx b/web/src/pages/desktop/menu/settings/tailscale/install.tsx index ea40b32..f1d7c57 100644 --- a/web/src/pages/desktop/menu/settings/tailscale/install.tsx +++ b/web/src/pages/desktop/menu/settings/tailscale/install.tsx @@ -43,7 +43,13 @@ export const Install = ({ setIsLocked, onSuccess }: InstallProps) => { icon={} subTitle={t('settings.tailscale.notInstall')} extra={ - ]}