diff --git a/front_vue/src/Core/api/mock.js b/front_vue/src/Core/api/mock.js index 7da63da..7d5aedd 100644 --- a/front_vue/src/Core/api/mock.js +++ b/front_vue/src/Core/api/mock.js @@ -6,11 +6,12 @@ import certsData from './mock-data/certs.json' import vaccessData from './mock-data/vaccess.json' const delay = (ms = 300) => new Promise(r => setTimeout(r, ms)) +const clone = (data) => JSON.parse(JSON.stringify(data)) export const mockApi = { async getAllServicesStatus() { await delay(200) - return JSON.parse(JSON.stringify(servicesData)) + return clone(servicesData) }, async checkServicesReady() { @@ -36,7 +37,7 @@ export const mockApi = { async getSitesList() { await delay(200) - return JSON.parse(JSON.stringify(sitesData)) + return clone(sitesData) }, async createNewSite(siteJSON) { @@ -65,12 +66,12 @@ export const mockApi = { async getProxyList() { await delay(200) - return JSON.parse(JSON.stringify(proxiesData)) + return clone(proxiesData) }, async getVAccessRules(host, isProxy) { await delay(200) - return JSON.parse(JSON.stringify(vaccessData)) + return clone(vaccessData) }, async saveVAccessRules(host, isProxy, configJSON) { @@ -80,7 +81,7 @@ export const mockApi = { async getConfig() { await delay(200) - return JSON.parse(JSON.stringify(configData)) + return clone(configData) }, async saveConfig(configJSON) { @@ -111,7 +112,7 @@ export const mockApi = { async getAllCertsInfo() { await delay(300) - return JSON.parse(JSON.stringify(certsData)) + return clone(certsData) }, async deleteCertificate(domain) { diff --git a/front_vue/src/Core/api/wails.js b/front_vue/src/Core/api/wails.js index 2dd35b2..2c3cb2d 100644 --- a/front_vue/src/Core/api/wails.js +++ b/front_vue/src/Core/api/wails.js @@ -1,99 +1,26 @@ const app = () => window.go.admin.App -export const wailsApi = { - async getAllServicesStatus() { - return await app().GetAllServicesStatus() - }, +const methods = [ + 'GetAllServicesStatus', 'CheckServicesReady', + 'StartServer', 'StopServer', + 'StartHTTPService', 'StopHTTPService', + 'StartHTTPSService', 'StopHTTPSService', + 'StartMySQLService', 'StopMySQLService', + 'StartPHPService', 'StopPHPService', + 'EnableProxyService', 'DisableProxyService', + 'EnableACMEService', 'DisableACMEService', + 'RestartAllServices', + 'GetSitesList', 'CreateNewSite', 'DeleteSite', 'UpdateSiteCache', 'OpenSiteFolder', + 'UploadCertificate', + 'GetProxyList', + 'GetVAccessRules', 'SaveVAccessRules', + 'GetConfig', 'SaveConfig', 'ReloadConfig', + 'ObtainSSLCertificate', 'ObtainAllSSLCertificates', + 'GetCertInfo', 'GetAllCertsInfo', 'DeleteCertificate', 'ReloadSSLCertificates', +] - async checkServicesReady() { - return await app().CheckServicesReady() - }, +const toCamelCase = (s) => s.charAt(0).toLowerCase() + s.slice(1) - async startServer() { return await app().StartServer() }, - async stopServer() { return await app().StopServer() }, - async startHTTPService() { return await app().StartHTTPService() }, - async stopHTTPService() { return await app().StopHTTPService() }, - async startHTTPSService() { return await app().StartHTTPSService() }, - async stopHTTPSService() { return await app().StopHTTPSService() }, - async startMySQLService() { return await app().StartMySQLService() }, - async stopMySQLService() { return await app().StopMySQLService() }, - async startPHPService() { return await app().StartPHPService() }, - async stopPHPService() { return await app().StopPHPService() }, - async enableProxyService() { return await app().EnableProxyService() }, - async disableProxyService() { return await app().DisableProxyService() }, - async enableACMEService() { return await app().EnableACMEService() }, - async disableACMEService() { return await app().DisableACMEService() }, - async restartAllServices() { return await app().RestartAllServices() }, - - async getSitesList() { - return await app().GetSitesList() - }, - - async createNewSite(siteJSON) { - return await app().CreateNewSite(siteJSON) - }, - - async deleteSite(host) { - return await app().DeleteSite(host) - }, - - async updateSiteCache() { - return await app().UpdateSiteCache() - }, - - async openSiteFolder(host) { - return await app().OpenSiteFolder(host) - }, - - async uploadCertificate(host, certType, certDataBase64) { - return await app().UploadCertificate(host, certType, certDataBase64) - }, - - async getProxyList() { - return await app().GetProxyList() - }, - - async getVAccessRules(host, isProxy) { - return await app().GetVAccessRules(host, isProxy) - }, - - async saveVAccessRules(host, isProxy, configJSON) { - return await app().SaveVAccessRules(host, isProxy, configJSON) - }, - - async getConfig() { - return await app().GetConfig() - }, - - async saveConfig(configJSON) { - return await app().SaveConfig(configJSON) - }, - - async reloadConfig() { - return await app().ReloadConfig() - }, - - async obtainSSLCertificate(domain) { - return await app().ObtainSSLCertificate(domain) - }, - - async obtainAllSSLCertificates() { - return await app().ObtainAllSSLCertificates() - }, - - async getCertInfo(domain) { - return await app().GetCertInfo(domain) - }, - - async getAllCertsInfo() { - return await app().GetAllCertsInfo() - }, - - async deleteCertificate(domain) { - return await app().DeleteCertificate(domain) - }, - - async reloadSSLCertificates() { - return await app().ReloadSSLCertificates() - }, -} +export const wailsApi = Object.fromEntries( + methods.map(m => [toCamelCase(m), (...args) => app()[m](...args)]) +) diff --git a/front_vue/src/Core/composables/useCertLookup.js b/front_vue/src/Core/composables/useCertLookup.js new file mode 100644 index 0000000..85b88cb --- /dev/null +++ b/front_vue/src/Core/composables/useCertLookup.js @@ -0,0 +1,35 @@ +export function useCertLookup() { + const certsStore = useCertsStore() + + const findCertForDomain = (domain, aliases = []) => { + const allDomains = [domain, ...aliases.filter(a => !a.includes('*'))] + + for (const d of allDomains) { + const direct = certsStore.list.find(c => c.domain === d && c.has_cert) + if (direct) return direct + + const parts = d.split('.') + if (parts.length >= 2) { + const wildcard = '*.' + parts.slice(1).join('.') + const wc = certsStore.list.find(c => c.domain === wildcard && c.has_cert) + if (wc) return wc + } + + for (const cert of certsStore.list) { + if (cert.has_cert && cert.dns_names) { + for (const dns of cert.dns_names) { + if (dns === d) return cert + if (dns.startsWith('*.')) { + const base = dns.slice(2) + const dParts = d.split('.') + if (dParts.length >= 2 && dParts.slice(1).join('.') === base) return cert + } + } + } + } + } + return null + } + + return { findCertForDomain } +} diff --git a/front_vue/src/Core/composables/useDraggable.js b/front_vue/src/Core/composables/useDraggable.js index ada11d7..f289c49 100644 --- a/front_vue/src/Core/composables/useDraggable.js +++ b/front_vue/src/Core/composables/useDraggable.js @@ -1,4 +1,4 @@ -export function useDraggable(list) { +export function useDraggable(list, { onReorder } = {}) { const dragIndex = ref(null) const dragOverIndex = ref(null) @@ -23,7 +23,7 @@ export function useDraggable(list) { dragOverIndex.value = null } - const onDrop = (index) => { + const onDrop = async (index) => { if (dragIndex.value === null || dragIndex.value === index) { dragIndex.value = null dragOverIndex.value = null @@ -37,6 +37,8 @@ export function useDraggable(list) { dragIndex.value = null dragOverIndex.value = null + + if (onReorder) await onReorder(items) } const onDragEnd = (event) => { diff --git a/front_vue/src/Core/stores/certs.js b/front_vue/src/Core/stores/certs.js index f3b916e..34e764f 100644 --- a/front_vue/src/Core/stores/certs.js +++ b/front_vue/src/Core/stores/certs.js @@ -1,4 +1,4 @@ -import { defineStore } from 'pinia' +import { defineStore } from 'pinia' export const useCertsStore = defineStore('certs', { state: () => ({ @@ -8,37 +8,62 @@ export const useCertsStore = defineStore('certs', { actions: { async loadAll() { - const data = await api.getAllCertsInfo() - if (data) { - this.list = data - this.loaded = true + try { + const data = await api.getAllCertsInfo() + if (data) { + this.list = data + this.loaded = true + } + } catch (e) { + console.error('Failed to load certs:', e) } }, async getInfo(domain) { - return await api.getCertInfo(domain) + try { + return await api.getCertInfo(domain) + } catch (e) { + console.error('Failed to get cert info:', e) + return null + } + }, + + async _obtainAndReload(domain) { + try { + const result = await api.obtainSSLCertificate(domain) + await this.loadAll() + return result + } catch (e) { + console.error('Failed to obtain certificate:', e) + return `Error: ${e.message}` + } }, async issue(domain) { - const result = await api.obtainSSLCertificate(domain) - await this.loadAll() - return result + return this._obtainAndReload(domain) }, async renew(domain) { - const result = await api.obtainSSLCertificate(domain) - await this.loadAll() - return result + return this._obtainAndReload(domain) }, async remove(domain) { - const result = await api.deleteCertificate(domain) - await this.loadAll() - return result + try { + const result = await api.deleteCertificate(domain) + await this.loadAll() + return result + } catch (e) { + console.error('Failed to delete certificate:', e) + return `Error: ${e.message}` + } }, async reload() { - return await api.reloadSSLCertificates() + try { + return await api.reloadSSLCertificates() + } catch (e) { + console.error('Failed to reload certificates:', e) + } }, }, }) diff --git a/front_vue/src/Core/stores/config.js b/front_vue/src/Core/stores/config.js index 83b0e47..6ed7ac2 100644 --- a/front_vue/src/Core/stores/config.js +++ b/front_vue/src/Core/stores/config.js @@ -1,4 +1,4 @@ -import { defineStore } from 'pinia' +import { defineStore } from 'pinia' export const useConfigStore = defineStore('config', { state: () => ({ @@ -12,39 +12,28 @@ export const useConfigStore = defineStore('config', { actions: { async load() { - const config = await api.getConfig() - if (config) { - this.data = config - this.loaded = true + try { + const config = await api.getConfig() + if (config) { + this.data = config + this.loaded = true + } + } catch (e) { + console.error('Failed to load config:', e) } }, async save(configData) { - const result = await api.saveConfig(JSON.stringify(configData, null, 4)) - if (result && !String(result).startsWith('Error')) { - this.data = configData + try { + const result = await api.saveConfig(JSON.stringify(configData, null, 4)) + if (isSuccess(result)) { + this.data = configData + } + return result + } catch (e) { + console.error('Failed to save config:', e) + return `Error: ${e.message}` } - return result - }, - - async enableProxy() { - return await api.enableProxyService() - }, - - async disableProxy() { - return await api.disableProxyService() - }, - - async enableACME() { - return await api.enableACMEService() - }, - - async disableACME() { - return await api.disableACMEService() - }, - - async restartAll() { - return await api.restartAllServices() }, }, }) diff --git a/front_vue/src/Core/stores/proxies.js b/front_vue/src/Core/stores/proxies.js index 78504df..97393cc 100644 --- a/front_vue/src/Core/stores/proxies.js +++ b/front_vue/src/Core/stores/proxies.js @@ -1,4 +1,4 @@ -import { defineStore } from 'pinia' +import { defineStore } from 'pinia' export const useProxiesStore = defineStore('proxies', { state: () => ({ @@ -8,44 +8,58 @@ export const useProxiesStore = defineStore('proxies', { actions: { async load() { - const data = await api.getProxyList() - if (data) { - this.list = data - this.loaded = true + try { + const data = await api.getProxyList() + if (data) { + this.list = data + this.loaded = true + } + } catch (e) { + console.error('Failed to load proxies:', e) } }, async create(proxyData) { - const config = await api.getConfig() - config.Proxy_Service.push({ - Name: proxyData.name || '', - Enable: proxyData.enabled, - ExternalDomain: proxyData.domain, - LocalAddress: proxyData.localAddr, - LocalPort: proxyData.localPort, - ServiceHTTPSuse: proxyData.serviceHttps, - AutoHTTPS: proxyData.autoHttps, - AutoCreateSSL: proxyData.autoSSL, - }) - const result = await api.saveConfig(JSON.stringify(config)) - if (result && !String(result).startsWith('Error')) { - await api.reloadConfig() - await this.load() + try { + const config = await api.getConfig() + config.Proxy_Service.push({ + Name: proxyData.name || '', + Enable: proxyData.enabled, + ExternalDomain: proxyData.domain, + LocalAddress: proxyData.localAddr, + LocalPort: proxyData.localPort, + ServiceHTTPSuse: proxyData.serviceHttps, + AutoHTTPS: proxyData.autoHttps, + AutoCreateSSL: proxyData.autoSSL, + }) + const result = await api.saveConfig(JSON.stringify(config)) + if (isSuccess(result)) { + await api.reloadConfig() + await this.load() + } + return result + } catch (e) { + console.error('Failed to create proxy:', e) + return `Error: ${e.message}` } - return result }, async remove(domain) { - const config = await api.getConfig() - config.Proxy_Service = config.Proxy_Service.filter( - p => p.ExternalDomain !== domain - ) - const result = await api.saveConfig(JSON.stringify(config)) - if (result && !String(result).startsWith('Error')) { - await api.reloadConfig() - await this.load() + try { + const config = await api.getConfig() + config.Proxy_Service = config.Proxy_Service.filter( + p => p.ExternalDomain !== domain + ) + const result = await api.saveConfig(JSON.stringify(config)) + if (isSuccess(result)) { + await api.reloadConfig() + await this.load() + } + return result + } catch (e) { + console.error('Failed to remove proxy:', e) + return `Error: ${e.message}` } - return result }, }, }) diff --git a/front_vue/src/Core/stores/services.js b/front_vue/src/Core/stores/services.js index 34c125c..14bbaba 100644 --- a/front_vue/src/Core/stores/services.js +++ b/front_vue/src/Core/stores/services.js @@ -1,4 +1,11 @@ -import { defineStore } from 'pinia' +import { defineStore } from 'pinia' + +const SERVICE_METHODS = { + HTTP: { start: () => api.startHTTPService(), stop: () => api.stopHTTPService() }, + HTTPS: { start: () => api.startHTTPSService(), stop: () => api.stopHTTPSService() }, + MySQL: { start: () => api.startMySQLService(), stop: () => api.stopMySQLService() }, + PHP: { start: () => api.startPHPService(), stop: () => api.stopPHPService() }, +} export const useServicesStore = defineStore('services', { state: () => ({ @@ -9,10 +16,14 @@ export const useServicesStore = defineStore('services', { actions: { async load() { - const data = await api.getAllServicesStatus() - if (data) { - this.list = Array.isArray(data) ? data : Object.values(data) - this.loaded = true + try { + const data = await api.getAllServicesStatus() + if (data) { + this.list = Array.isArray(data) ? data : Object.values(data) + this.loaded = true + } + } catch (e) { + console.error('Failed to load services:', e) } }, @@ -26,26 +37,62 @@ export const useServicesStore = defineStore('services', { this.isOperating = false }, - async startService(name) { - const methods = { - HTTP: () => api.startHTTPService(), - HTTPS: () => api.startHTTPSService(), - MySQL: () => api.startMySQLService(), - PHP: () => api.startPHPService(), + async toggleService(name, action) { + try { + const method = SERVICE_METHODS[name]?.[action] + if (method) await method() + await this.load() + } catch (e) { + console.error(`Failed to ${action} service ${name}:`, e) } - if (methods[name]) await methods[name]() - await this.load() + }, + + async startService(name) { + return this.toggleService(name, 'start') }, async stopService(name) { - const methods = { - HTTP: () => api.stopHTTPService(), - HTTPS: () => api.stopHTTPSService(), - MySQL: () => api.stopMySQLService(), - PHP: () => api.stopPHPService(), + return this.toggleService(name, 'stop') + }, + + async enableProxy() { + try { + return await api.enableProxyService() + } catch (e) { + console.error('Failed to enable proxy:', e) + } + }, + + async disableProxy() { + try { + return await api.disableProxyService() + } catch (e) { + console.error('Failed to disable proxy:', e) + } + }, + + async enableACME() { + try { + return await api.enableACMEService() + } catch (e) { + console.error('Failed to enable ACME:', e) + } + }, + + async disableACME() { + try { + return await api.disableACMEService() + } catch (e) { + console.error('Failed to disable ACME:', e) + } + }, + + async restartAll() { + try { + return await api.restartAllServices() + } catch (e) { + console.error('Failed to restart services:', e) } - if (methods[name]) await methods[name]() - await this.load() }, }, }) diff --git a/front_vue/src/Core/stores/sites.js b/front_vue/src/Core/stores/sites.js index 5649465..5906bb4 100644 --- a/front_vue/src/Core/stores/sites.js +++ b/front_vue/src/Core/stores/sites.js @@ -1,4 +1,4 @@ -import { defineStore } from 'pinia' +import { defineStore } from 'pinia' export const useSitesStore = defineStore('sites', { state: () => ({ @@ -8,35 +8,58 @@ export const useSitesStore = defineStore('sites', { actions: { async load() { - const data = await api.getSitesList() - if (data) { - this.list = data - this.loaded = true + try { + const data = await api.getSitesList() + if (data) { + this.list = data + this.loaded = true + } + } catch (e) { + console.error('Failed to load sites:', e) } }, async create(siteData) { - const result = await api.createNewSite(JSON.stringify(siteData)) - if (result && !String(result).startsWith('Error')) { - await this.load() + try { + const result = await api.createNewSite(JSON.stringify(siteData)) + if (isSuccess(result)) { + await this.load() + } + return result + } catch (e) { + console.error('Failed to create site:', e) + return `Error: ${e.message}` } - return result }, async remove(host) { - const result = await api.deleteSite(host) - if (result && !String(result).startsWith('Error')) { - await this.load() + try { + const result = await api.deleteSite(host) + if (isSuccess(result)) { + await this.load() + } + return result + } catch (e) { + console.error('Failed to delete site:', e) + return `Error: ${e.message}` } - return result }, async openFolder(host) { - await api.openSiteFolder(host) + try { + await api.openSiteFolder(host) + } catch (e) { + console.error('Failed to open folder:', e) + } }, async uploadCert(host, certType, certDataBase64) { - return await api.uploadCertificate(host, certType, certDataBase64) + try { + return await api.uploadCertificate(host, certType, certDataBase64) + } catch (e) { + console.error('Failed to upload cert:', e) + return `Error: ${e.message}` + } }, }, }) diff --git a/front_vue/src/Core/utils/apiResult.js b/front_vue/src/Core/utils/apiResult.js new file mode 100644 index 0000000..f0e2676 --- /dev/null +++ b/front_vue/src/Core/utils/apiResult.js @@ -0,0 +1 @@ +export const isSuccess = (result) => result && !String(result).startsWith('Error') diff --git a/front_vue/src/Core/utils/openUrl.js b/front_vue/src/Core/utils/openUrl.js new file mode 100644 index 0000000..5dcb573 --- /dev/null +++ b/front_vue/src/Core/utils/openUrl.js @@ -0,0 +1,7 @@ +export function openUrl(url) { + if (window.runtime?.BrowserOpenURL) { + window.runtime.BrowserOpenURL(url) + } else { + window.open(url, '_blank') + } +} diff --git a/front_vue/src/Core/utils/sleep.js b/front_vue/src/Core/utils/sleep.js new file mode 100644 index 0000000..4d1a748 --- /dev/null +++ b/front_vue/src/Core/utils/sleep.js @@ -0,0 +1 @@ +export const sleep = (ms) => new Promise(r => setTimeout(r, ms)) diff --git a/front_vue/src/Design/assets/css/forms.css b/front_vue/src/Design/assets/css/forms.css index 6efa8c8..c41d939 100644 --- a/front_vue/src/Design/assets/css/forms.css +++ b/front_vue/src/Design/assets/css/forms.css @@ -39,7 +39,7 @@ .form-subsection-title i { color: var(--accent-purple-light); - font-size: 16px; + font-size: var(--text-lg); } .form-group { diff --git a/front_vue/src/Design/assets/css/tables.css b/front_vue/src/Design/assets/css/tables.css index 1d1b89e..156a3e3 100644 --- a/front_vue/src/Design/assets/css/tables.css +++ b/front_vue/src/Design/assets/css/tables.css @@ -23,7 +23,7 @@ .data-table th { padding: 8px 20px; text-align: left; - font-size: 11px; + font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--text-muted); text-transform: uppercase; @@ -34,7 +34,7 @@ .th-icon { opacity: 0.4; - font-size: 10px; + font-size: var(--text-xs); margin-right: 2px; } @@ -152,12 +152,12 @@ cursor: wait; } -.status-toggle { +.data-table .status-toggle { cursor: pointer; transition: opacity var(--transition-fast); } -.status-toggle:hover { +.data-table .status-toggle:hover { opacity: 0.7; } @@ -166,7 +166,7 @@ color: var(--text-muted); opacity: 0.15; cursor: grab; - font-size: 11px; + font-size: var(--text-sm); margin-right: 8px; transition: all var(--transition-fast); } diff --git a/front_vue/src/Design/assets/css/transitions.css b/front_vue/src/Design/assets/css/transitions.css index 81a6086..fcc2114 100644 --- a/front_vue/src/Design/assets/css/transitions.css +++ b/front_vue/src/Design/assets/css/transitions.css @@ -57,6 +57,12 @@ transform: scale(0.95); } +/* Fade In (для страниц и компонентов) */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + /* Notification slide */ .notification-enter-active { transition: all 0.3s ease-out; diff --git a/front_vue/src/Design/components/certs/CertCard.vue b/front_vue/src/Design/components/certs/CertCard.vue deleted file mode 100644 index 88a9a1a..0000000 --- a/front_vue/src/Design/components/certs/CertCard.vue +++ /dev/null @@ -1 +0,0 @@ -`n diff --git a/front_vue/src/Design/components/certs/CertGrid.vue b/front_vue/src/Design/components/certs/CertGrid.vue deleted file mode 100644 index adc81fd..0000000 --- a/front_vue/src/Design/components/certs/CertGrid.vue +++ /dev/null @@ -1 +0,0 @@ -`n diff --git a/front_vue/src/Design/components/layout/AppFooter.vue b/front_vue/src/Design/components/layout/AppFooter.vue index e7765f7..af57159 100644 --- a/front_vue/src/Design/components/layout/AppFooter.vue +++ b/front_vue/src/Design/components/layout/AppFooter.vue @@ -2,19 +2,11 @@ const { t } = useI18n() const currentYear = new Date().getFullYear() - -const openSite = () => { - if (window.runtime?.BrowserOpenURL) { - window.runtime.BrowserOpenURL('https://vserf.ru') - } else { - window.open('https://vserf.ru', '_blank') - } -} diff --git a/front_vue/src/Design/components/layout/TitleBar.vue b/front_vue/src/Design/components/layout/TitleBar.vue index 69ec85c..caf3d9d 100644 --- a/front_vue/src/Design/components/layout/TitleBar.vue +++ b/front_vue/src/Design/components/layout/TitleBar.vue @@ -1,4 +1,4 @@ -