├── img └── oui.gif ├── docs ├── README.md ├── package.json └── src │ ├── zh │ ├── guide │ │ ├── README.md │ │ ├── lua-api.md │ │ ├── acl.md │ │ ├── getting-started.md │ │ ├── vue-api.md │ │ └── page.md │ └── README.md │ ├── .vuepress │ ├── components │ │ └── Sponsors.vue │ └── config.js │ ├── guide │ ├── README.md │ ├── lua-api.md │ ├── getting-started.md │ ├── acl.md │ ├── vue-api.md │ └── page.md │ └── README.md ├── oui-rpc-core ├── files │ ├── oui.default │ ├── admin.acl │ ├── lighttpd │ │ ├── oui.conf │ │ └── handler.lua │ ├── oui.init │ ├── rpc │ │ ├── ubus.lua │ │ ├── ui.lua │ │ ├── system.lua │ │ ├── user.lua │ │ ├── network.lua │ │ └── uci.lua │ ├── oui.config │ ├── rpc.lua │ └── oui.lua └── Makefile ├── oui-ui-core ├── htdoc │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── assets │ │ │ └── logo.png │ │ ├── App.vue │ │ ├── i18n │ │ │ └── index.js │ │ ├── components │ │ │ └── NotFound.vue │ │ ├── main.js │ │ ├── element-plus │ │ │ └── index.js │ │ ├── router │ │ │ ├── development.js │ │ │ └── index.js │ │ ├── timers │ │ │ └── index.js │ │ └── oui │ │ │ └── index.js │ ├── index.html │ ├── package.json │ ├── eslint.config.js │ └── vite.config.js ├── files │ └── menu.json └── Makefile ├── applications ├── oui-app-demo │ ├── files │ │ ├── rpc │ │ │ └── demo.lua │ │ └── menu.json │ ├── htdoc │ │ ├── index.vue │ │ ├── locale.json │ │ ├── package.json │ │ └── vite.config.mjs │ └── Makefile ├── oui-app-acl │ ├── files │ │ ├── menu.json │ │ └── rpc │ │ │ └── acl.lua │ ├── Makefile │ └── htdoc │ │ ├── package.json │ │ ├── vite.config.mjs │ │ ├── locale.json │ │ ├── dynamic-tags.vue │ │ └── index.vue ├── oui-app-user │ ├── files │ │ └── menu.json │ ├── Makefile │ └── htdoc │ │ ├── package.json │ │ ├── vite.config.mjs │ │ ├── locale.json │ │ └── index.vue ├── oui-app-system │ ├── files │ │ └── menu.json │ ├── Makefile │ └── htdoc │ │ ├── package.json │ │ ├── vite.config.mjs │ │ ├── locale.json │ │ └── index.vue ├── oui-app-upgrade │ ├── files │ │ └── menu.json │ ├── Makefile │ └── htdoc │ │ ├── package.json │ │ ├── vite.config.mjs │ │ ├── locale.json │ │ └── index.vue ├── oui-app-stations │ ├── files │ │ ├── menu.json │ │ └── rpc │ │ │ └── station.lua │ ├── htdoc │ │ ├── package.json │ │ ├── locale.json │ │ ├── vite.config.mjs │ │ └── index.vue │ └── Makefile ├── oui-app-backup │ ├── files │ │ └── menu.json │ ├── Makefile │ └── htdoc │ │ ├── package.json │ │ ├── vite.config.mjs │ │ ├── locale.json │ │ └── index.vue ├── oui-app-dhcp-lease │ ├── files │ │ └── menu.json │ ├── Makefile │ └── htdoc │ │ ├── package.json │ │ ├── vite.config.mjs │ │ ├── locale.json │ │ └── index.vue ├── oui-app-home │ ├── Makefile │ └── htdoc │ │ ├── package.json │ │ ├── dashboard.vue │ │ ├── vite.config.mjs │ │ ├── locale.json │ │ └── index.vue ├── oui-app-layout │ ├── Makefile │ └── htdoc │ │ ├── package.json │ │ ├── vite.config.mjs │ │ ├── Logo.vue │ │ ├── locale.json │ │ ├── openwrt-logo-black.svg │ │ ├── openwrt-logo-white.svg │ │ └── index.vue └── oui-app-login │ ├── Makefile │ └── htdoc │ ├── package.json │ ├── vite.config.mjs │ ├── locale.json │ └── index.vue ├── .github ├── ISSUE_TEMPLATE.md ├── workflows │ ├── scripts │ │ └── ci_helpers.sh │ ├── docs.yml │ └── formal.yml └── FUNDING.yml ├── .gitignore ├── version.mk ├── LICENSE ├── oui.mk ├── node.mk └── README.md /img/oui.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhaojh329/oui/HEAD/img/oui.gif -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # docs 2 | 3 | ## Start 4 | 5 | ```bash 6 | yarn 7 | yarn dev 8 | ``` 9 | -------------------------------------------------------------------------------- /oui-rpc-core/files/oui.default: -------------------------------------------------------------------------------- 1 | sed -i 's/index.html/oui.html/' /etc/lighttpd/lighttpd.conf 2 | -------------------------------------------------------------------------------- /oui-ui-core/htdoc/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhaojh329/oui/HEAD/oui-ui-core/htdoc/public/favicon.ico -------------------------------------------------------------------------------- /oui-ui-core/htdoc/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhaojh329/oui/HEAD/oui-ui-core/htdoc/src/assets/logo.png -------------------------------------------------------------------------------- /applications/oui-app-demo/files/rpc/demo.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | function M.echo(params) 4 | return params 5 | end 6 | 7 | return M 8 | -------------------------------------------------------------------------------- /applications/oui-app-demo/htdoc/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Environment: (put here arch, model, OpenWrt version) 2 | 3 | Description: 4 | 5 | ``` 6 | Formating code blocks by wrapping them with pairs of ``` 7 | ``` 8 | -------------------------------------------------------------------------------- /applications/oui-app-demo/htdoc/locale.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "hello": "hello world!" 4 | }, 5 | "zh-CN": { 6 | "hello": "你好世界!" 7 | }, 8 | "zh-TW": { 9 | "hello": "你好世界!" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /oui-ui-core/htdoc/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /oui-rpc-core/files/admin.acl: -------------------------------------------------------------------------------- 1 | { 2 | "rpc": { 3 | "matchs": [".+"] 4 | }, 5 | "menu": { 6 | "matchs": [".+"] 7 | }, 8 | "ubus": { 9 | "matchs": [".+"] 10 | }, 11 | "uci": { 12 | "matchs": [".+"] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /applications/oui-app-acl/files/menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "/system/acl": { 3 | "title": "acl", 4 | "view": "acl", 5 | "index": 4, 6 | "locales": { 7 | "en": "ACL", 8 | "zh-CN": "权限管理", 9 | "zh-TW": "權限管理" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /applications/oui-app-user/files/menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "/system/user": { 3 | "title": "user", 4 | "view": "user", 5 | "index": 2, 6 | "locales": { 7 | "en": "User", 8 | "zh-CN": "用户", 9 | "zh-TW": "用戶" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /applications/oui-app-system/files/menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "/system/system": { 3 | "title": "System", 4 | "view": "system", 5 | "index": 0, 6 | "locales": { 7 | "en": "System", 8 | "zh-CN": "系统", 9 | "zh-TW": "系統" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /oui-rpc-core/files/lighttpd/oui.conf: -------------------------------------------------------------------------------- 1 | magnet.attract-physical-path-to = ( conf_dir + "/oui-handler.lua" ) 2 | 3 | scgi.server = ( "/oui-" => 4 | ( 5 | "scgi-oui" => 6 | ( 7 | "socket" => "/var/run/oui.sock", 8 | "check-local" => "disable" 9 | ) 10 | ) 11 | ) 12 | -------------------------------------------------------------------------------- /applications/oui-app-upgrade/files/menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "/system/upgrade": { 3 | "title": "upgrade", 4 | "view": "upgrade", 5 | "index": 10, 6 | "locales": { 7 | "en": "Upgrade", 8 | "zh-CN": "升级", 9 | "zh-TW": "升級" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /applications/oui-app-stations/files/menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "/status/stations": { 3 | "title": "Stations", 4 | "view": "stations", 5 | "index": 0, 6 | "locales": { 7 | "en": "Stations", 8 | "zh-CN": "无线站点", 9 | "zh-TW": "無線站點" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /applications/oui-app-backup/files/menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "/system/backup": { 3 | "title": "backup", 4 | "view": "backup", 5 | "index": 11, 6 | "locales": { 7 | "en": "Backup / Restore", 8 | "zh-CN": "备份/恢复", 9 | "zh-TW": "備份/恢復" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /oui-rpc-core/files/oui.init: -------------------------------------------------------------------------------- 1 | #!/bin/sh /etc/rc.common 2 | 3 | START=50 4 | 5 | USE_PROCD=1 6 | 7 | start_service() { 8 | procd_open_instance 9 | procd_set_param command eco /usr/sbin/oui 10 | procd_set_param respawn 11 | procd_close_instance 12 | } 13 | 14 | service_triggers() { 15 | procd_add_reload_trigger "oui" 16 | } 17 | -------------------------------------------------------------------------------- /applications/oui-app-acl/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2022 Jianhui Zhao 3 | # 4 | # This is free software, licensed under the MIT. 5 | # 6 | 7 | include $(TOPDIR)/rules.mk 8 | 9 | APP_TITLE:=ACL manage 10 | APP_NAME:=acl 11 | 12 | include ../../oui.mk 13 | 14 | # call BuildPackage - OpenWrt buildroot signature 15 | -------------------------------------------------------------------------------- /applications/oui-app-demo/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2022 Jianhui Zhao 3 | # 4 | # This is free software, licensed under the MIT. 5 | # 6 | 7 | include $(TOPDIR)/rules.mk 8 | 9 | APP_TITLE:=Demo 10 | APP_NAME:=demo 11 | 12 | include ../../oui.mk 13 | 14 | # call BuildPackage - OpenWrt buildroot signature 15 | -------------------------------------------------------------------------------- /applications/oui-app-dhcp-lease/files/menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "/status/dhcp-lease": { 3 | "title": "DHCP lease", 4 | "view": "dhcp-lease", 5 | "index": 1, 6 | "locales": { 7 | "en": "DHCP lease", 8 | "zh-CN": "DHCP 租约", 9 | "zh-TW": "DHCP 租約" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /applications/oui-app-upgrade/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2022 Jianhui Zhao 3 | # 4 | # This is free software, licensed under the MIT. 5 | # 6 | 7 | include $(TOPDIR)/rules.mk 8 | 9 | APP_TITLE:=Upgrade 10 | APP_NAME:=upgrade 11 | 12 | include ../../oui.mk 13 | 14 | # call BuildPackage - OpenWrt buildroot signature 15 | -------------------------------------------------------------------------------- /applications/oui-app-user/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2022 Jianhui Zhao 3 | # 4 | # This is free software, licensed under the MIT. 5 | # 6 | 7 | include $(TOPDIR)/rules.mk 8 | 9 | APP_TITLE:=User manage 10 | APP_NAME:=user 11 | 12 | include ../../oui.mk 13 | 14 | # call BuildPackage - OpenWrt buildroot signature 15 | -------------------------------------------------------------------------------- /applications/oui-app-backup/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2022 Jianhui Zhao 3 | # 4 | # This is free software, licensed under the MIT. 5 | # 6 | 7 | include $(TOPDIR)/rules.mk 8 | 9 | APP_TITLE:=Backup / Restore 10 | APP_NAME:=backup 11 | 12 | include ../../oui.mk 13 | 14 | # call BuildPackage - OpenWrt buildroot signature 15 | -------------------------------------------------------------------------------- /applications/oui-app-dhcp-lease/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2022 Jianhui Zhao 3 | # 4 | # This is free software, licensed under the MIT. 5 | # 6 | 7 | include $(TOPDIR)/rules.mk 8 | 9 | APP_TITLE:=DHCP lease 10 | APP_NAME:=dhcp-lease 11 | 12 | include ../../oui.mk 13 | 14 | # call BuildPackage - OpenWrt buildroot signature 15 | -------------------------------------------------------------------------------- /applications/oui-app-home/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2022 Jianhui Zhao 3 | # 4 | # This is free software, licensed under the MIT. 5 | # 6 | 7 | include $(TOPDIR)/rules.mk 8 | 9 | APP_TITLE:=OUI built-in home page 10 | APP_NAME:=home 11 | 12 | include ../../oui.mk 13 | 14 | # call BuildPackage - OpenWrt buildroot signature 15 | -------------------------------------------------------------------------------- /applications/oui-app-system/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2022 Jianhui Zhao 3 | # 4 | # This is free software, licensed under the MIT. 5 | # 6 | 7 | include $(TOPDIR)/rules.mk 8 | 9 | APP_TITLE:=System Configure 10 | APP_NAME:=system 11 | 12 | include ../../oui.mk 13 | 14 | # call BuildPackage - OpenWrt buildroot signature 15 | -------------------------------------------------------------------------------- /applications/oui-app-layout/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2022 Jianhui Zhao 3 | # 4 | # This is free software, licensed under the MIT. 5 | # 6 | 7 | include $(TOPDIR)/rules.mk 8 | 9 | APP_TITLE:=OUI built-in layout page 10 | APP_NAME:=layout 11 | 12 | include ../../oui.mk 13 | 14 | # call BuildPackage - OpenWrt buildroot signature 15 | -------------------------------------------------------------------------------- /applications/oui-app-login/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2022 Jianhui Zhao 3 | # 4 | # This is free software, licensed under the MIT. 5 | # 6 | 7 | include $(TOPDIR)/rules.mk 8 | 9 | APP_TITLE:=OUI built-in login page 10 | APP_NAME:=login 11 | 12 | include ../../oui.mk 13 | 14 | # call BuildPackage - OpenWrt buildroot signature 15 | -------------------------------------------------------------------------------- /applications/oui-app-acl/htdoc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oui-app", 3 | "scripts": { 4 | "build": "vite build" 5 | }, 6 | "devDependencies": { 7 | "@intlify/unplugin-vue-i18n": "^4.0.0", 8 | "@vitejs/plugin-vue": "^5.1.2", 9 | "vite": "^5.4.1", 10 | "vite-plugin-compression": "^0.5.1", 11 | "vue-i18n": "^9.14.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /applications/oui-app-home/htdoc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oui-app", 3 | "scripts": { 4 | "build": "vite build" 5 | }, 6 | "devDependencies": { 7 | "@intlify/unplugin-vue-i18n": "^4.0.0", 8 | "@vitejs/plugin-vue": "^5.1.2", 9 | "vite": "^5.4.1", 10 | "vite-plugin-compression": "^0.5.1", 11 | "vue-i18n": "^9.14.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /applications/oui-app-login/htdoc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oui-app", 3 | "scripts": { 4 | "build": "vite build" 5 | }, 6 | "devDependencies": { 7 | "@intlify/unplugin-vue-i18n": "^4.0.0", 8 | "@vitejs/plugin-vue": "^5.1.2", 9 | "vite": "^5.4.1", 10 | "vite-plugin-compression": "^0.5.1", 11 | "vue-i18n": "^9.14.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /applications/oui-app-user/htdoc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oui-app", 3 | "scripts": { 4 | "build": "vite build" 5 | }, 6 | "devDependencies": { 7 | "@intlify/unplugin-vue-i18n": "^4.0.0", 8 | "@vitejs/plugin-vue": "^5.1.2", 9 | "vite": "^5.4.1", 10 | "vite-plugin-compression": "^0.5.1", 11 | "vue-i18n": "^9.14.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /applications/oui-app-dhcp-lease/htdoc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oui-app", 3 | "scripts": { 4 | "build": "vite build" 5 | }, 6 | "devDependencies": { 7 | "@intlify/unplugin-vue-i18n": "^4.0.0", 8 | "@vitejs/plugin-vue": "^5.1.2", 9 | "vite": "^5.4.1", 10 | "vite-plugin-compression": "^0.5.1", 11 | "vue-i18n": "^9.14.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /applications/oui-app-stations/htdoc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oui-app", 3 | "scripts": { 4 | "build": "vite build" 5 | }, 6 | "devDependencies": { 7 | "@intlify/unplugin-vue-i18n": "^4.0.0", 8 | "@vitejs/plugin-vue": "^5.1.2", 9 | "vite": "^5.4.1", 10 | "vite-plugin-compression": "^0.5.1", 11 | "vue-i18n": "^9.14.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /applications/oui-app-system/htdoc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oui-app", 3 | "scripts": { 4 | "build": "vite build" 5 | }, 6 | "devDependencies": { 7 | "@intlify/unplugin-vue-i18n": "^4.0.0", 8 | "@vitejs/plugin-vue": "^5.1.2", 9 | "vite": "^5.4.1", 10 | "vite-plugin-compression": "^0.5.1", 11 | "vue-i18n": "^9.14.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /applications/oui-app-upgrade/htdoc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oui-app", 3 | "scripts": { 4 | "build": "vite build" 5 | }, 6 | "devDependencies": { 7 | "@intlify/unplugin-vue-i18n": "^4.0.0", 8 | "@vitejs/plugin-vue": "^5.1.2", 9 | "vite": "^5.4.1", 10 | "vite-plugin-compression": "^0.5.1", 11 | "vue-i18n": "^9.14.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/scripts/ci_helpers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | color_out() { 4 | printf "\e[0;$1m%s\e[0;0m\n" "$2" 5 | } 6 | 7 | success() { 8 | color_out 32 "$1" 9 | } 10 | 11 | info() { 12 | color_out 36 "$1" 13 | } 14 | 15 | err() { 16 | color_out 31 "$1" 17 | } 18 | 19 | warn() { 20 | color_out 33 "$1" 21 | } 22 | 23 | err_die() { 24 | err "$1" 25 | exit 1 26 | } 27 | -------------------------------------------------------------------------------- /applications/oui-app-stations/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2022 Jianhui Zhao 3 | # 4 | # This is free software, licensed under the MIT. 5 | # 6 | 7 | include $(TOPDIR)/rules.mk 8 | 9 | APP_TITLE:=Stations 10 | APP_NAME:=stations 11 | APP_DEPENDS:=+lua-eco-nl80211 +lua-eco-ubus 12 | 13 | include ../../oui.mk 14 | 15 | # call BuildPackage - OpenWrt buildroot signature 16 | -------------------------------------------------------------------------------- /oui-ui-core/htdoc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | OUI 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "vuepress dev src", 8 | "build": "vuepress build src" 9 | }, 10 | "devDependencies": { 11 | "@vuepress/plugin-register-components": "^2.0.0-beta.51", 12 | "@vuepress/plugin-search": "^2.0.0-beta.51", 13 | "vuepress": "^2.0.0-beta.51" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # docs 27 | .temp 28 | .cache 29 | 30 | oui-ui-core/htdoc/src/applications 31 | 32 | .eslintcache 33 | -------------------------------------------------------------------------------- /oui-ui-core/htdoc/src/i18n/index.js: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | /* 3 | * Author: Jianhui Zhao 4 | */ 5 | 6 | import { createI18n } from 'vue-i18n' 7 | 8 | const i18n = createI18n({ 9 | locale: 'en', 10 | fallbackLocale: 'en', 11 | silentTranslationWarn: true, 12 | silentFallbackWarn: true, 13 | messages: { 14 | 'en': {}, 15 | 'zh-CN': {}, 16 | 'zh-TW': {}, 17 | 'ja-JP': {} 18 | } 19 | }) 20 | 21 | export default i18n 22 | -------------------------------------------------------------------------------- /oui-ui-core/htdoc/src/components/NotFound.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 26 | -------------------------------------------------------------------------------- /docs/src/zh/guide/README.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | 3 | [1]: https://github.com/zhaojh329/lua-eco 4 | [2]: https://github.com/vuejs/core 5 | [3]: https://github.com/vitejs/vite 6 | 7 | Oui 是一个用来开发 `OpenWrt` Web 接口的`框架`。 8 | 9 | Oui 使用 [Lua-eco][1] 开发其静态文件服务器。 10 | 11 | Oui 前端使用 [Vue3][2] 编写,使用 [Vite][3] 构建前端代码。 12 | 13 | 不同于传统的前端项目,所有的页面作为一个整体进行打包。Oui 实现了和 Luci 一样的模块化,每个页面独立打包,互不影响。其处理方式为将每个页面以库的形式进行打包。 14 | 15 | ::: tip 16 | Oui 默认使用 [Element Plus](https://github.com/element-plus/element-plus) 组件库。你可以根据自己的需求,选择适合自己的组件库或者自己开发组件。 17 | ::: 18 | -------------------------------------------------------------------------------- /oui-rpc-core/files/rpc/ubus.lua: -------------------------------------------------------------------------------- 1 | local ubus = require 'eco.ubus' 2 | local rpc = require 'oui.rpc' 3 | local log = require 'eco.log' 4 | 5 | local M = {} 6 | 7 | function M.call(params, session) 8 | local object = params.object 9 | local method = params.method 10 | 11 | if not rpc.acl_match(session, params.object .. '.' .. params.method, 'ubus') then 12 | return nil, rpc.ERROR_CODE_PERMISSION_DENIED 13 | end 14 | 15 | return ubus.call(object, method, params.params or {}) 16 | end 17 | 18 | return M 19 | -------------------------------------------------------------------------------- /applications/oui-app-home/htdoc/dashboard.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | -------------------------------------------------------------------------------- /applications/oui-app-stations/htdoc/locale.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "Type": "Type", 4 | "Network": "Network", 5 | "MAC address": "MAC address", 6 | "Signal / Noise": "Signal / Noise", 7 | "RX Rate / TX Rate": "RX Rate / TX Rate" 8 | }, 9 | "zh-CN": { 10 | "Type": "类型", 11 | "Network": "网络", 12 | "MAC address": "MAC 地址", 13 | "Signal / Noise": "信号/噪声", 14 | "RX Rate / TX Rate": "接收速率/发送速率" 15 | }, 16 | "zh-TW": { 17 | "Type": "類型", 18 | "Network": "網絡", 19 | "MAC address": "MAC 地址", 20 | "Signal / Noise": "信號 /雜訊比", 21 | "RX Rate / TX Rate": "接收速率 / 發送速率" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docs/src/.vuepress/components/Sponsors.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | 18 | 32 | -------------------------------------------------------------------------------- /oui-ui-core/htdoc/src/main.js: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | /* 3 | * Author: Jianhui Zhao 4 | */ 5 | 6 | import { createApp } from 'vue' 7 | import VueAxios from 'vue-axios' 8 | import axios from 'axios' 9 | import App from './App.vue' 10 | import router from './router' 11 | import timers from './timers' 12 | import i18n from './i18n' 13 | import oui from './oui' 14 | import ElementPlus from './element-plus' 15 | 16 | const app = createApp(App) 17 | 18 | app.use(VueAxios, axios) 19 | app.use(router) 20 | app.use(i18n) 21 | app.use(oui) 22 | app.use(timers) 23 | app.use(ElementPlus) 24 | 25 | app.mount('#app') 26 | -------------------------------------------------------------------------------- /applications/oui-app-demo/htdoc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oui-app", 3 | "scripts": { 4 | "build": "vite build" 5 | }, 6 | "devDependencies": { 7 | "@intlify/unplugin-vue-i18n": "^4.0.0", 8 | "@vicons/antd": "^0.12.0", 9 | "@vicons/carbon": "^0.12.0", 10 | "@vicons/fa": "^0.12.0", 11 | "@vicons/fluent": "^0.12.0", 12 | "@vicons/ionicons4": "^0.12.0", 13 | "@vicons/ionicons5": "^0.12.0", 14 | "@vicons/material": "^0.12.0", 15 | "@vicons/tabler": "^0.12.0", 16 | "@vitejs/plugin-vue": "^5.1.2", 17 | "vite": "^5.4.1", 18 | "vite-plugin-compression": "^0.5.1", 19 | "vue-i18n": "^9.14.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /applications/oui-app-backup/htdoc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oui-app", 3 | "scripts": { 4 | "build": "vite build" 5 | }, 6 | "devDependencies": { 7 | "@intlify/unplugin-vue-i18n": "^4.0.0", 8 | "@vicons/antd": "^0.12.0", 9 | "@vicons/carbon": "^0.12.0", 10 | "@vicons/fa": "^0.12.0", 11 | "@vicons/fluent": "^0.12.0", 12 | "@vicons/ionicons4": "^0.12.0", 13 | "@vicons/ionicons5": "^0.12.0", 14 | "@vicons/material": "^0.12.0", 15 | "@vicons/tabler": "^0.12.0", 16 | "@vitejs/plugin-vue": "^5.1.2", 17 | "vite": "^5.4.1", 18 | "vite-plugin-compression": "^0.5.1", 19 | "vue-i18n": "^9.14.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /applications/oui-app-layout/htdoc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oui-app", 3 | "scripts": { 4 | "build": "vite build" 5 | }, 6 | "devDependencies": { 7 | "@intlify/unplugin-vue-i18n": "^4.0.0", 8 | "@vicons/antd": "^0.12.0", 9 | "@vicons/carbon": "^0.12.0", 10 | "@vicons/fa": "^0.12.0", 11 | "@vicons/fluent": "^0.12.0", 12 | "@vicons/ionicons4": "^0.12.0", 13 | "@vicons/ionicons5": "^0.12.0", 14 | "@vicons/material": "^0.12.0", 15 | "@vicons/tabler": "^0.12.0", 16 | "@vitejs/plugin-vue": "^5.1.2", 17 | "vite": "^5.4.1", 18 | "vite-plugin-compression": "^0.5.1", 19 | "vue-i18n": "^9.14.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: 11 | otechie: # Replace with a single Otechie username 12 | buy_me_a_coffee: zhaojh329 13 | custom: [ 14 | "https://zhaojh329.github.io/zhaojh329/" 15 | ] 16 | -------------------------------------------------------------------------------- /applications/oui-app-demo/files/menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "/demo": { 3 | "title": "Oui Demo", 4 | "view": "demo", 5 | "index": 60, 6 | "locales": { 7 | "en": "Oui Demo", 8 | "zh-CN": "Oui 示范", 9 | "zh-TW": "Oui 示範" 10 | }, 11 | "svg":{"-xmlns":"http://www.w3.org/2000/svg","-xmlns:xlink":"http://www.w3.org/1999/xlink","-viewBox":"0 0 24 24","path":{"-d":"M22 11h-4.17l3.24-3.24l-1.41-1.42L15 11h-2V9l4.66-4.66l-1.42-1.41L13 6.17V2h-2v4.17L7.76 2.93L6.34 4.34L11 9v2H9L4.34 6.34L2.93 7.76L6.17 11H2v2h4.17l-3.24 3.24l1.41 1.42L9 13h2v2l-4.66 4.66l1.42 1.41L11 17.83V22h2v-4.17l3.24 3.24l1.42-1.41L13 15v-2h2l4.66 4.66l1.41-1.42L17.83 13H22v-2z","-fill":"currentColor"}} 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /oui-rpc-core/files/oui.config: -------------------------------------------------------------------------------- 1 | config oui global 2 | option locale 'auto' 3 | option theme 'dark' 4 | option lua_code_cache 1 5 | 6 | # ERR, INFO, DEBUG 7 | option log_level 'INFO' 8 | 9 | # The default logging destination is the system log (syslog). 10 | # You can choose to log to a specific file by uncommenting the 11 | # line 'option log_path' and specifying the path to the desired 12 | # log file (e.g., '/var/log/oui.log') 13 | # option log_path '/var/log/oui.log' 14 | 15 | config no-auth 16 | option module 'ui' 17 | list func 'get_locale' 18 | list func 'get_theme' 19 | 20 | config user 21 | option username 'admin' 22 | 23 | # echo -n admin:123456 | md5sum 24 | option password '2d08086927f4d87a31154aaf0ba2e067' 25 | option acl admin 26 | -------------------------------------------------------------------------------- /docs/src/zh/guide/lua-api.md: -------------------------------------------------------------------------------- 1 | # Lua 接口 2 | 3 | 在 Oui 中,Lua 接口以 `模块-方法` 的形式进行组织。 4 | 5 | ```sh 6 | root@OpenWrt:~# ls /usr/share/oui/rpc/ 7 | acl.lua network.lua ubus.lua ui.lua wireless.lua 8 | demo.lua system.lua uci.lua user.lua 9 | ``` 10 | 11 | 这里的每个 Lua 文件代表着一个模块。模块名为 Lua 文件名(不带后缀)。 12 | 13 | 每个 Lua 接口文件需要返回一个 `Lua Table`,该 `Lua Table` 由多个 `Lua function` 组成。 14 | 15 | ```lua 16 | -- /usr/share/oui/rpc/test.lua 17 | 18 | local M = {} 19 | 20 | --[[ 21 | params: 前端调用传递的参数 22 | section: 登录的会话信息,为一个 Table, 23 | 包含当前登录的用户名(username)和其所属的权限组(acl) 24 | --]] 25 | function M.func1(params, section) 26 | local res = {} 27 | ... 28 | return res 29 | end 30 | 31 | return M 32 | ``` 33 | 34 | ```js 35 | this.$oui.call('test', 'func1', {a: 1}).then(res => { 36 | ... 37 | }) 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/src/guide/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | [1]: https://github.com/zhaojh329/lua-eco 4 | [2]: https://github.com/vuejs/core 5 | [3]: https://github.com/vitejs/vite 6 | 7 | Oui is a `framework` for developing `OpenWrt` Web interfaces. 8 | 9 | Oui uses [Lua-eco][1] as to build its static file server. 10 | 11 | The Oui front-end is written in [Vue3][2], and the front-end code is build with [Vite][3]. 12 | 13 | Unlike traditional front-end projects, all pages are packaged as a whole. Oui implements the same modularity as Luci, with each page packaged independently of the other. This is done by packaging each page as a library. 14 | 15 | ::: tip 16 | Oui uses the [Element Plus](https://github.com/element-plus/element-plus) component library by default. You can choose your own library or develop your own components according to your needs. 17 | ::: 18 | -------------------------------------------------------------------------------- /applications/oui-app-acl/htdoc/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import viteCompression from 'vite-plugin-compression' 4 | import vueI18n from '@intlify/unplugin-vue-i18n/vite' 5 | 6 | const env = loadEnv('', process.cwd()) 7 | 8 | export default defineConfig({ 9 | plugins: [ 10 | vue(), 11 | viteCompression({ 12 | deleteOriginFile: true 13 | }), 14 | vueI18n({ 15 | compositionOnly: false 16 | }) 17 | ], 18 | build: { 19 | cssCodeSplit: true, 20 | lib: { 21 | formats: ['umd'], 22 | entry: 'index.vue', 23 | name: 'oui-com-' + env.VITE_APP_NAME, 24 | fileName: env.VITE_APP_NAME 25 | }, 26 | rollupOptions: { 27 | external: ['vue'], 28 | output: { 29 | globals: { 30 | vue: 'Vue' 31 | } 32 | } 33 | } 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /applications/oui-app-backup/htdoc/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import viteCompression from 'vite-plugin-compression' 4 | import vueI18n from '@intlify/unplugin-vue-i18n/vite' 5 | 6 | const env = loadEnv('', process.cwd()) 7 | 8 | export default defineConfig({ 9 | plugins: [ 10 | vue(), 11 | viteCompression({ 12 | deleteOriginFile: true 13 | }), 14 | vueI18n({ 15 | compositionOnly: false 16 | }) 17 | ], 18 | build: { 19 | cssCodeSplit: true, 20 | lib: { 21 | formats: ['umd'], 22 | entry: 'index.vue', 23 | name: 'oui-com-' + env.VITE_APP_NAME, 24 | fileName: env.VITE_APP_NAME 25 | }, 26 | rollupOptions: { 27 | external: ['vue'], 28 | output: { 29 | globals: { 30 | vue: 'Vue' 31 | } 32 | } 33 | } 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /applications/oui-app-demo/htdoc/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import viteCompression from 'vite-plugin-compression' 4 | import vueI18n from '@intlify/unplugin-vue-i18n/vite' 5 | 6 | const env = loadEnv('', process.cwd()) 7 | 8 | export default defineConfig({ 9 | plugins: [ 10 | vue(), 11 | viteCompression({ 12 | deleteOriginFile: true 13 | }), 14 | vueI18n({ 15 | compositionOnly: false 16 | }) 17 | ], 18 | build: { 19 | cssCodeSplit: true, 20 | lib: { 21 | formats: ['umd'], 22 | entry: 'index.vue', 23 | name: 'oui-com-' + env.VITE_APP_NAME, 24 | fileName: env.VITE_APP_NAME 25 | }, 26 | rollupOptions: { 27 | external: ['vue'], 28 | output: { 29 | globals: { 30 | vue: 'Vue' 31 | } 32 | } 33 | } 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /applications/oui-app-home/htdoc/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import viteCompression from 'vite-plugin-compression' 4 | import vueI18n from '@intlify/unplugin-vue-i18n/vite' 5 | 6 | const env = loadEnv('', process.cwd()) 7 | 8 | export default defineConfig({ 9 | plugins: [ 10 | vue(), 11 | viteCompression({ 12 | deleteOriginFile: true 13 | }), 14 | vueI18n({ 15 | compositionOnly: false 16 | }) 17 | ], 18 | build: { 19 | cssCodeSplit: true, 20 | lib: { 21 | formats: ['umd'], 22 | entry: 'index.vue', 23 | name: 'oui-com-' + env.VITE_APP_NAME, 24 | fileName: env.VITE_APP_NAME 25 | }, 26 | rollupOptions: { 27 | external: ['vue'], 28 | output: { 29 | globals: { 30 | vue: 'Vue' 31 | } 32 | } 33 | } 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /applications/oui-app-layout/htdoc/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import viteCompression from 'vite-plugin-compression' 4 | import vueI18n from '@intlify/unplugin-vue-i18n/vite' 5 | 6 | const env = loadEnv('', process.cwd()) 7 | 8 | export default defineConfig({ 9 | plugins: [ 10 | vue(), 11 | viteCompression({ 12 | deleteOriginFile: true 13 | }), 14 | vueI18n({ 15 | compositionOnly: false 16 | }) 17 | ], 18 | build: { 19 | cssCodeSplit: true, 20 | lib: { 21 | formats: ['umd'], 22 | entry: 'index.vue', 23 | name: 'oui-com-' + env.VITE_APP_NAME, 24 | fileName: env.VITE_APP_NAME 25 | }, 26 | rollupOptions: { 27 | external: ['vue'], 28 | output: { 29 | globals: { 30 | vue: 'Vue' 31 | } 32 | } 33 | } 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /applications/oui-app-login/htdoc/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import viteCompression from 'vite-plugin-compression' 4 | import vueI18n from '@intlify/unplugin-vue-i18n/vite' 5 | 6 | const env = loadEnv('', process.cwd()) 7 | 8 | export default defineConfig({ 9 | plugins: [ 10 | vue(), 11 | viteCompression({ 12 | deleteOriginFile: true 13 | }), 14 | vueI18n({ 15 | compositionOnly: false 16 | }) 17 | ], 18 | build: { 19 | cssCodeSplit: true, 20 | lib: { 21 | formats: ['umd'], 22 | entry: 'index.vue', 23 | name: 'oui-com-' + env.VITE_APP_NAME, 24 | fileName: env.VITE_APP_NAME 25 | }, 26 | rollupOptions: { 27 | external: ['vue'], 28 | output: { 29 | globals: { 30 | vue: 'Vue' 31 | } 32 | } 33 | } 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /applications/oui-app-system/htdoc/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import viteCompression from 'vite-plugin-compression' 4 | import vueI18n from '@intlify/unplugin-vue-i18n/vite' 5 | 6 | const env = loadEnv('', process.cwd()) 7 | 8 | export default defineConfig({ 9 | plugins: [ 10 | vue(), 11 | viteCompression({ 12 | deleteOriginFile: true 13 | }), 14 | vueI18n({ 15 | compositionOnly: false 16 | }) 17 | ], 18 | build: { 19 | cssCodeSplit: true, 20 | lib: { 21 | formats: ['umd'], 22 | entry: 'index.vue', 23 | name: 'oui-com-' + env.VITE_APP_NAME, 24 | fileName: env.VITE_APP_NAME 25 | }, 26 | rollupOptions: { 27 | external: ['vue'], 28 | output: { 29 | globals: { 30 | vue: 'Vue' 31 | } 32 | } 33 | } 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /applications/oui-app-user/htdoc/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import viteCompression from 'vite-plugin-compression' 4 | import vueI18n from '@intlify/unplugin-vue-i18n/vite' 5 | 6 | const env = loadEnv('', process.cwd()) 7 | 8 | export default defineConfig({ 9 | plugins: [ 10 | vue(), 11 | viteCompression({ 12 | deleteOriginFile: true 13 | }), 14 | vueI18n({ 15 | compositionOnly: false 16 | }) 17 | ], 18 | build: { 19 | cssCodeSplit: true, 20 | lib: { 21 | formats: ['umd'], 22 | entry: 'index.vue', 23 | name: 'oui-com-' + env.VITE_APP_NAME, 24 | fileName: env.VITE_APP_NAME 25 | }, 26 | rollupOptions: { 27 | external: ['vue'], 28 | output: { 29 | globals: { 30 | vue: 'Vue' 31 | } 32 | } 33 | } 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /applications/oui-app-dhcp-lease/htdoc/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import viteCompression from 'vite-plugin-compression' 4 | import vueI18n from '@intlify/unplugin-vue-i18n/vite' 5 | 6 | const env = loadEnv('', process.cwd()) 7 | 8 | export default defineConfig({ 9 | plugins: [ 10 | vue(), 11 | viteCompression({ 12 | deleteOriginFile: true 13 | }), 14 | vueI18n({ 15 | compositionOnly: false 16 | }) 17 | ], 18 | build: { 19 | cssCodeSplit: true, 20 | lib: { 21 | formats: ['umd'], 22 | entry: 'index.vue', 23 | name: 'oui-com-' + env.VITE_APP_NAME, 24 | fileName: env.VITE_APP_NAME 25 | }, 26 | rollupOptions: { 27 | external: ['vue'], 28 | output: { 29 | globals: { 30 | vue: 'Vue' 31 | } 32 | } 33 | } 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /applications/oui-app-stations/htdoc/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import viteCompression from 'vite-plugin-compression' 4 | import vueI18n from '@intlify/unplugin-vue-i18n/vite' 5 | 6 | const env = loadEnv('', process.cwd()) 7 | 8 | export default defineConfig({ 9 | plugins: [ 10 | vue(), 11 | viteCompression({ 12 | deleteOriginFile: true 13 | }), 14 | vueI18n({ 15 | compositionOnly: false 16 | }) 17 | ], 18 | build: { 19 | cssCodeSplit: true, 20 | lib: { 21 | formats: ['umd'], 22 | entry: 'index.vue', 23 | name: 'oui-com-' + env.VITE_APP_NAME, 24 | fileName: env.VITE_APP_NAME 25 | }, 26 | rollupOptions: { 27 | external: ['vue'], 28 | output: { 29 | globals: { 30 | vue: 'Vue' 31 | } 32 | } 33 | } 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /applications/oui-app-upgrade/htdoc/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import viteCompression from 'vite-plugin-compression' 4 | import vueI18n from '@intlify/unplugin-vue-i18n/vite' 5 | 6 | const env = loadEnv('', process.cwd()) 7 | 8 | export default defineConfig({ 9 | plugins: [ 10 | vue(), 11 | viteCompression({ 12 | deleteOriginFile: true 13 | }), 14 | vueI18n({ 15 | compositionOnly: false 16 | }) 17 | ], 18 | build: { 19 | cssCodeSplit: true, 20 | lib: { 21 | formats: ['umd'], 22 | entry: 'index.vue', 23 | name: 'oui-com-' + env.VITE_APP_NAME, 24 | fileName: env.VITE_APP_NAME 25 | }, 26 | rollupOptions: { 27 | external: ['vue'], 28 | output: { 29 | globals: { 30 | vue: 'Vue' 31 | } 32 | } 33 | } 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /applications/oui-app-system/htdoc/locale.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "This field is required": "This field is required", 4 | "Invalid hostname": "Invalid hostname", 5 | "Save & Apply": "Save & Apply", 6 | "Reset": "Reset", 7 | "Hostname": "Hostname", 8 | "Timezone": "Timezone", 9 | "Configuration has been applied": "Configuration has been applied" 10 | }, 11 | "zh-CN": { 12 | "This field is required": "必填", 13 | "Invalid hostname": "无效的主机名", 14 | "Save & Apply": "保存和应用", 15 | "Reset": "重置", 16 | "Hostname": "主机名", 17 | "Timezone": "时区", 18 | "Configuration has been applied": "配置已应用" 19 | }, 20 | "zh-TW": { 21 | "This field is required": "必填", 22 | "Invalid hostname": "無效的主機名", 23 | "Save & Apply": "保存和應用", 24 | "Reset": "重置", 25 | "Hostname": "主機名", 26 | "Timezone": "時區", 27 | "Configuration has been applied": "配置已應用" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /applications/oui-app-dhcp-lease/htdoc/locale.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "Active DHCP Leases": "Active DHCP Leases", 4 | "Active DHCPv6 Leases": "Active DHCPv6 Leases", 5 | "Hostname": "Hostname", 6 | "IPv4 address": "IPv4 address", 7 | "IPv6 address": "IPv6 address", 8 | "MAC address": "MAC address", 9 | "Lease": "Lease time remaining" 10 | }, 11 | "zh-CN": { 12 | "Active DHCP Leases": "已分配的 DHCP 租约", 13 | "Active DHCPv6 Leases": "已分配的 DHCPv6 租约", 14 | "Hostname": "主机名", 15 | "IPv4 address": "IPv4 地址", 16 | "IPv6 address": "IPv6 地址", 17 | "MAC address": "MAC 地址", 18 | "Lease": "剩余租期" 19 | }, 20 | "zh-TW": { 21 | "Active DHCP Leases": "已分配的 DHCP 租約", 22 | "Active DHCPv6 Leases": "已分配的 DHCPv6 租約", 23 | "Hostname": "主機名", 24 | "IPv4 address": "IPv4 地址", 25 | "IPv6 address": "IPv6 地址", 26 | "MAC address": "MAC 地址", 27 | "Lease": "剩余租期" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /version.mk: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2022 Jianhui Zhao 3 | # 4 | # This is free software, licensed under the MIT. 5 | # 6 | 7 | define findrev 8 | $(shell \ 9 | if git log -1 >/dev/null 2>/dev/null; then \ 10 | set -- $$(git log -1 --format="%ct %h" --abbrev=7 -- .); \ 11 | if [ -n "$$1" ]; then 12 | secs="$$(($$1 % 86400))"; \ 13 | yday="$$(date --utc --date="@$$1" "+%y.%j")"; \ 14 | printf '%s.%05d~%s' "$$yday" "$$secs" "$$2"; \ 15 | else \ 16 | echo "0"; \ 17 | fi; \ 18 | else \ 19 | ts=$$(find . -type f -printf '%T@\n' 2>/dev/null | sort -rn | head -n1 | cut -d. -f1); \ 20 | if [ -n "$$ts" ]; then \ 21 | secs="$$(($$ts % 86400))"; \ 22 | date="$$(date --utc --date="@$$ts" "+%y%m%d")"; \ 23 | printf '0.%s.%05d' "$$date" "$$secs"; \ 24 | else \ 25 | echo "0"; \ 26 | fi; \ 27 | fi \ 28 | ) 29 | endef 30 | -------------------------------------------------------------------------------- /applications/oui-app-acl/htdoc/locale.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "OK": "OK", 4 | "Class": "Class", 5 | "Matchs": "Matchs", 6 | "Reverse": "Reverse", 7 | "Change Matchs": "Change Matchs", 8 | "Add group": "Add group", 9 | "Delete group": "Delete group", 10 | "delete-group-confirm": "Are you sure you want to delete the group '{ group }'?", 11 | "Save & Apply": "Save & Apply" 12 | }, 13 | "zh-CN": { 14 | "OK": "确定", 15 | "Class": "类型", 16 | "Matchs": "匹配项", 17 | "Reverse": "反向匹配", 18 | "Add group": "添加组", 19 | "Delete group": "删除组", 20 | "delete-group-confirm": "你确定要删除组 \"{ group }\" 吗?", 21 | "Save & Apply": "保存和应用" 22 | }, 23 | "zh-TW": { 24 | "OK": "確定", 25 | "Class": "類型", 26 | "Matchs": "匹配項", 27 | "Reverse": "反向匹配", 28 | "Username": "用戶名", 29 | "Add group": "添加組", 30 | "Delete group": "刪除組", 31 | "delete-group-confirm": "你確定要刪除組 \"{ group }\" 嗎?", 32 | "Save & Apply": "保存和應用" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/src/guide/lua-api.md: -------------------------------------------------------------------------------- 1 | # Lua API 2 | 3 | In Oui, Lua API are organized as `module-methods`. 4 | 5 | ```sh 6 | root@OpenWrt:~# ls /usr/share/oui/rpc/ 7 | acl.lua network.lua ubus.lua ui.lua wireless.lua 8 | demo.lua system.lua uci.lua user.lua 9 | ``` 10 | 11 | Each Lua file here represents a module. Module name is Lua file name(without suffix). 12 | 13 | Each Lua API file needs to return a `Lua Table`, which consists of multiple `Lua functions`. 14 | 15 | ```lua 16 | -- /usr/share/oui/rpc/test.lua 17 | 18 | local M = {} 19 | 20 | --[[ 21 | params: Parameters passed by the front-end call 22 | section: The login session information is a Table. 23 | Contains the currently logged in username (username) and the permission group (acl) to which it belongs. 24 | --]] 25 | function M.func1(params, section) 26 | local res = {} 27 | ... 28 | return res 29 | end 30 | 31 | return M 32 | ``` 33 | 34 | ```js 35 | this.$oui.call('test', 'func1', {a: 1}).then(res => { 36 | ... 37 | }) 38 | ``` 39 | -------------------------------------------------------------------------------- /oui-rpc-core/files/lighttpd/handler.lua: -------------------------------------------------------------------------------- 1 | local req_attr = lighty.r.req_attr 2 | local resp_header = lighty.r.resp_header 3 | 4 | local function handle_gzip() 5 | local accept_encoding = lighty.r.req_header['Accept-Encoding'] 6 | 7 | if not accept_encoding or not accept_encoding:find('gzip') then 8 | return 9 | end 10 | 11 | local orig_path = req_attr['physical.path'] 12 | local gzip_path = orig_path .. '.gz' 13 | 14 | if not lighty.c.stat(gzip_path) then 15 | return 16 | end 17 | 18 | req_attr['physical.path'] = gzip_path 19 | resp_header['Content-Encoding'] = 'gzip' 20 | 21 | local content_type 22 | 23 | if orig_path:match('%.css$') then 24 | content_type = 'text/css' 25 | elseif orig_path:match('%.js$') then 26 | content_type = 'application/javascript' 27 | end 28 | 29 | if content_type then 30 | resp_header['Content-Type'] = content_type 31 | end 32 | 33 | return true 34 | end 35 | 36 | if handle_gzip() then 37 | return 38 | end 39 | -------------------------------------------------------------------------------- /oui-ui-core/htdoc/src/element-plus/index.js: -------------------------------------------------------------------------------- 1 | import ElementPlus from 'element-plus' 2 | import 'element-plus/dist/index.css' 3 | import 'element-plus/theme-chalk/dark/css-vars.css' 4 | import * as ElementPlusIconsVue from '@element-plus/icons-vue' 5 | import zhCN from 'element-plus/es/locale/lang/zh-cn' 6 | import zhTW from 'element-plus/es/locale/lang/zh-tw' 7 | import jaJP from 'element-plus/es/locale/lang/ja' 8 | import en from 'element-plus/es/locale/lang/en' 9 | 10 | import { computed } from 'vue' 11 | import oui from '../oui' 12 | 13 | const locales = { 14 | 'en': en, 15 | 'zh-CN': zhCN, 16 | 'zh-TW': zhTW, 17 | 'ja-JP': jaJP 18 | } 19 | 20 | function globalLocale() { 21 | if (oui.state.locale === 'auto') 22 | return navigator.language 23 | else 24 | return oui.state.locale 25 | } 26 | 27 | export const locale = computed(() => locales[globalLocale()]) 28 | 29 | export default { 30 | install(app) { 31 | app.use(ElementPlus) 32 | 33 | for (const [key, component] of Object.entries(ElementPlusIconsVue)) { 34 | app.component(key, component) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /oui-rpc-core/files/rpc/ui.lua: -------------------------------------------------------------------------------- 1 | local file = require 'eco.file' 2 | local rpc = require 'oui.rpc' 3 | local cjson = require 'cjson' 4 | local uci = require 'eco.uci' 5 | 6 | local M = {} 7 | 8 | function M.get_locale() 9 | local c = uci.cursor() 10 | local locale = c:get('oui', 'global', 'locale') 11 | 12 | return { locale = locale } 13 | end 14 | 15 | function M.get_theme() 16 | local c = uci.cursor() 17 | local theme = c:get('oui', 'global', 'theme') 18 | 19 | return { theme = theme } 20 | end 21 | 22 | function M.get_menus(params, session) 23 | local menus = {} 24 | 25 | for name in file.dir('/usr/share/oui/menu.d') do 26 | if name:match('^%w.*%.json$') then 27 | local data = file.readfile('/usr/share/oui/menu.d/' .. name) 28 | local menu = cjson.decode(data) 29 | for m, info in pairs(menu) do 30 | if rpc.acl_match(session, m, 'menu') then 31 | menus[m] = info 32 | end 33 | end 34 | end 35 | end 36 | 37 | return { menus = menus } 38 | end 39 | 40 | return M 41 | -------------------------------------------------------------------------------- /docs/src/zh/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | actions: 4 | - text: 快速上手 5 | link: /zh/guide/getting-started.md 6 | type: primary 7 | footer: MIT Licensed | Copyright © 2022 Jianhui Zhao 8 | --- 9 | 10 | ## 赞助商 11 | 12 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/src/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | actions: 4 | - text: Get Started 5 | link: /guide/getting-started.md 6 | type: primary 7 | footer: MIT Licensed | Copyright © 2022 Jianhui Zhao 8 | --- 9 | 10 | ## Sponsor 11 | 12 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jianhui Zhao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /applications/oui-app-layout/htdoc/Logo.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 23 | 24 | 53 | -------------------------------------------------------------------------------- /applications/oui-app-stations/files/rpc/station.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local nl80211 = require 'eco.nl80211' 4 | local ubus = require 'eco.ubus' 5 | 6 | function M.stations() 7 | local status = ubus.call('network.wireless', 'status', {}) or {} 8 | 9 | local stations = {} 10 | 11 | for _, dev in pairs(status) do 12 | if dev.up then 13 | local band = dev.config.band 14 | 15 | for _, ifs in ipairs(dev.interfaces) do 16 | local ifname = ifs.ifname 17 | local res = nl80211.get_stations(ifname) 18 | 19 | for _, sta in pairs(res) do 20 | stations[#stations + 1] = { 21 | macaddr = sta.mac, 22 | ifname = ifname, 23 | band = band, 24 | signal = sta.signal, 25 | noise = sta.noise, 26 | rx_rate = sta.rx_rate, 27 | tx_rate = sta.tx_rate 28 | } 29 | end 30 | end 31 | end 32 | end 33 | 34 | return { stations = stations } 35 | end 36 | 37 | return M 38 | -------------------------------------------------------------------------------- /applications/oui-app-login/htdoc/locale.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "Login": "Login", 4 | "Username": "Username", 5 | "Password": "Password", 6 | "Please enter username": "Please enter username", 7 | "Please enter password": "Please enter password", 8 | "wrong username or password": "wrong username or password" 9 | }, 10 | "zh-CN": { 11 | "Login": "登录", 12 | "Username": "用户名", 13 | "Password": "密码", 14 | "Please enter username": "请输入用户名", 15 | "Please enter password": "请输入密码", 16 | "wrong username or password": "用户名或密码错误" 17 | }, 18 | "zh-TW": { 19 | "Login": "登入", 20 | "Username": "用戶名", 21 | "Password": "密碼", 22 | "Please enter username": "請輸入用戶名", 23 | "Please enter password": "請輸入密碼", 24 | "wrong username or password": "用戶名或密碼錯誤" 25 | }, 26 | "ja-JP": { 27 | "Login": "ログイン", 28 | "Username":"ユーザー名", 29 | "Password": "パスワード", 30 | "Please enter username": "ユーザー名を入力してください", 31 | "Please enter password": "パスワードを入力してください", 32 | "wrong username or password": "ユーザー名またはパスワードが間違っています" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /oui-ui-core/htdoc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oui-ui-core", 3 | "type": "module", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build", 7 | "preview": "vite preview" 8 | }, 9 | "dependencies": { 10 | "@element-plus/icons-vue": "^2.3.1", 11 | "@vueuse/core": "^11.0.0", 12 | "axios": "^1.7.4", 13 | "element-plus": "^2.8.0", 14 | "js-md5": "^0.8.3", 15 | "vue": "^3.4.38", 16 | "vue-axios": "^3.5.2", 17 | "vue-i18n": "^9.14.0", 18 | "vue-router": "^4.4.3" 19 | }, 20 | "devDependencies": { 21 | "@eslint/js": "^9.9.0", 22 | "@intlify/unplugin-vue-i18n": "^4.0.0", 23 | "@nabla/vite-plugin-eslint": "^2.0.4", 24 | "@vicons/antd": "^0.12.0", 25 | "@vicons/carbon": "^0.12.0", 26 | "@vicons/fa": "^0.12.0", 27 | "@vicons/fluent": "^0.12.0", 28 | "@vicons/ionicons4": "^0.12.0", 29 | "@vicons/ionicons5": "^0.12.0", 30 | "@vicons/material": "^0.12.0", 31 | "@vicons/tabler": "^0.12.0", 32 | "@vitejs/plugin-vue": "^5.1.2", 33 | "eslint": "^9.9.0", 34 | "eslint-plugin-vue": "^9.27.0", 35 | "globals": "^15.9.0", 36 | "vite": "^5.4.1", 37 | "vite-plugin-compression": "^0.5.1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /applications/oui-app-layout/htdoc/locale.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "Auto": "Auto", 4 | "Light theme": "Light theme", 5 | "Dark theme": "Dark theme", 6 | "Logout": "Logout", 7 | "Reboot": "Reboot", 8 | "RebootConfirm": "Are you sure you want to restart the device?", 9 | "OK": "OK", 10 | "Rebooting": "Rebooting" 11 | }, 12 | "zh-CN": { 13 | "Auto": "自动", 14 | "Light theme": "浅色主题", 15 | "Dark theme": "深色主题", 16 | "Logout": "退出", 17 | "Reboot": "重启", 18 | "RebootConfirm": "你确定要重启设备吗?", 19 | "OK": "确定", 20 | "Rebooting": "正在重启" 21 | }, 22 | "zh-TW": { 23 | "Auto": "自動", 24 | "Light theme": "淺色主題", 25 | "Dark theme": "深色主題", 26 | "Logout": "退出", 27 | "Reboot": "重啟", 28 | "RebootConfirm": "你確定要重啓設備嗎?", 29 | "OK": "確定", 30 | "Rebooting": "正在重啓" 31 | }, 32 | "ja-JP": { 33 | "Auto": "自動", 34 | "Light theme": "明るい色のテーマ", 35 | "Dark theme": "暗いテーマ", 36 | "Logout": "ログアウト", 37 | "Reboot": "リブート", 38 | "RebootConfirm": "あなたは、デバイスを再起動したいですか?", 39 | "OK": "を選択して、", 40 | "Rebooting": "再起動中" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | paths: 8 | - 'docs/**' 9 | 10 | jobs: 11 | docs: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: '14' 23 | 24 | - name: Cache dependencies 25 | uses: actions/cache@v4 26 | id: yarn-cache 27 | with: 28 | path: | 29 | **/node_modules 30 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 31 | restore-keys: | 32 | ${{ runner.os }}-yarn- 33 | 34 | - name: Install dependencies 35 | if: steps.yarn-cache.outputs.cache-hit != 'true' 36 | run: cd docs && yarn --frozen-lockfile 37 | 38 | - name: Build Oui site 39 | run: cd docs && yarn build 40 | 41 | - name: Deploy to GitHub Pages 42 | uses: crazy-max/ghaction-github-pages@v2 43 | with: 44 | target_branch: gh-pages 45 | build_dir: docs/src/.vuepress/dist 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | -------------------------------------------------------------------------------- /applications/oui-app-acl/files/rpc/acl.lua: -------------------------------------------------------------------------------- 1 | local file = require 'eco.file' 2 | local cjson = require 'cjson' 3 | local rpc = require 'oui.rpc' 4 | 5 | local M = {} 6 | 7 | function M.load() 8 | local res = {} 9 | 10 | for group, acls in pairs(rpc.get_acls()) do 11 | local acl = {} 12 | 13 | for cls, info in pairs(acls) do 14 | acl[#acl + 1] = { 15 | cls = cls, 16 | matchs = info.matchs, 17 | reverse = not not info.reverse 18 | } 19 | end 20 | 21 | res[group] = acl 22 | end 23 | 24 | return res 25 | end 26 | 27 | function M.set(params) 28 | for name in file.dir('/usr/share/oui/acl') do 29 | if name ~= '.' and name ~= '..' then 30 | os.remove('/usr/share/oui/acl/' .. name) 31 | end 32 | end 33 | 34 | for group, acls in pairs((params.acls)) do 35 | local acl = {} 36 | for _, info in ipairs(acls) do 37 | acl[info.cls] = { 38 | matchs = info.matchs, 39 | reverse = info.reverse 40 | } 41 | end 42 | file.writefile('/usr/share/oui/acl/' .. group .. '.json', cjson.encode(acl)) 43 | end 44 | 45 | rpc.load_acl() 46 | end 47 | 48 | return M 49 | -------------------------------------------------------------------------------- /applications/oui-app-user/htdoc/locale.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "Username": "Username", 4 | "Password": "Password", 5 | "ACL group": "ACL group", 6 | "delete-user-confirm": "Are you sure you want to delete the user '{ username }'?", 7 | "OK": "OK", 8 | "Cancel": "Cancel", 9 | "Add user": "Add user", 10 | "This field is required": "This field is required", 11 | "username-exist": "The user '{ username }' already exist", 12 | "Change": "Change", 13 | "Delete": "Delete" 14 | }, 15 | "zh-CN": { 16 | "Username": "用户名", 17 | "Password": "密码", 18 | "ACL group": "权限组", 19 | "delete-user-confirm": "你确定要删除用户 \"{ username }\" 吗?", 20 | "OK": "确定", 21 | "Cancel": "取消", 22 | "Add user": "添加用户", 23 | "This field is required": "必填", 24 | "username-exist": "用户 \"{ username }\" 已经存在", 25 | "Change": "修改", 26 | "Delete": "删除" 27 | 28 | }, 29 | "zh-TW": { 30 | "Username": "用戶名", 31 | "Password": "密碼", 32 | "ACL group": "許可權組", 33 | "delete-user-confirm": "你確定要删除用戶 \"{ username }\" 嗎?", 34 | "OK": "確定", 35 | "Cancel": "取消", 36 | "Add user": "添加用戶", 37 | "This field is required": "必填", 38 | "username-exist": "用戶 \"{ username }\" 已經存在", 39 | "Change": "修改", 40 | "Delete": "删除" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /applications/oui-app-acl/htdoc/dynamic-tags.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 42 | -------------------------------------------------------------------------------- /oui-ui-core/htdoc/src/router/development.js: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | /* 3 | * Author: Jianhui Zhao 4 | */ 5 | 6 | function addRoutes(r, menu) { 7 | if (menu.view && menu.path !== '/') { 8 | r.push({ 9 | path: menu.path, 10 | component: () => import(`../applications/oui-app-${menu.view}/htdoc/index.vue`), 11 | meta: { menu: menu } 12 | }) 13 | } else if (menu.children) { 14 | menu.children.forEach(m => addRoutes(r, m)) 15 | } 16 | } 17 | 18 | export default function(routes, menus, loginView, layoutView, homeView) { 19 | routes.push({ 20 | path: '/login', 21 | name: 'login', 22 | component: () => import(`../applications/oui-app-${loginView}/htdoc/index.vue`) 23 | }) 24 | 25 | routes.push({ 26 | path: '/', 27 | name: '/', 28 | component: () => import(`../applications/oui-app-${layoutView}/htdoc/index.vue`), 29 | props: () => ({menus: menus}), 30 | children: [ 31 | { 32 | path: '/home', 33 | name: 'home', 34 | component: () => import(`../applications/oui-app-${homeView}/htdoc/index.vue`) 35 | }, 36 | { 37 | path: '/:pathMatch(.*)*', 38 | name: 'NotFound', 39 | component: () => import('../components/NotFound.vue') 40 | } 41 | ] 42 | }) 43 | 44 | menus.forEach(menu => addRoutes(routes[1].children, menu)) 45 | } 46 | -------------------------------------------------------------------------------- /oui-ui-core/htdoc/eslint.config.js: -------------------------------------------------------------------------------- 1 | import globals from 'globals' 2 | import pluginJs from '@eslint/js' 3 | import pluginVue from 'eslint-plugin-vue' 4 | 5 | export default [ 6 | {files: ['**/*.{js,mjs,cjs,vue}']}, 7 | {languageOptions: { globals: globals.browser }}, 8 | pluginJs.configs.recommended, 9 | ...pluginVue.configs['flat/essential'], 10 | { 11 | rules: { 12 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 13 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 14 | 'vue/no-multiple-template-root': 'off', 15 | 'vue/multi-word-component-names': 'off', 16 | 'linebreak-style': ['error', 'unix'], 17 | 'quotes': ['error', 'single'], 18 | 'brace-style': 'error', 19 | 'comma-dangle': 'error', 20 | 'comma-spacing': 'error', 21 | 'keyword-spacing': 'error', 22 | 'no-trailing-spaces': 'error', 23 | 'no-unneeded-ternary': 'error', 24 | 'space-before-function-paren': ['error', 'never'], 25 | 'space-infix-ops': ['error', {'int32Hint': false}], 26 | 'arrow-spacing': 'error', 27 | 'no-var': 'error', 28 | 'no-duplicate-imports': 'error', 29 | 'space-before-blocks': 'error', 30 | 'space-in-parens': ['error', 'never'], 31 | 'no-multi-spaces': 'error', 32 | 'eqeqeq': 'error', 33 | 'indent': ['error', 2], 34 | 'semi': ['error', 'never'] 35 | } 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /oui.mk: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2022 Jianhui Zhao 3 | # 4 | # This is free software, licensed under the MIT. 5 | # 6 | 7 | include ${CURDIR}/../../version.mk 8 | include ${CURDIR}/../../node.mk 9 | 10 | PKG_NAME:=$(notdir ${CURDIR}) 11 | PKG_VERSION:=$(strip $(call findrev)) 12 | PKG_RELEASE?=1 13 | 14 | include $(INCLUDE_DIR)/package.mk 15 | 16 | define Package/$(PKG_NAME) 17 | SECTION:=oui 18 | CATEGORY:=Oui 19 | SUBMENU:=Applications 20 | TITLE:=$(APP_TITLE) 21 | DEPENDS:=+oui-ui-core $(APP_DEPENDS) 22 | PKGARCH:=all 23 | endef 24 | 25 | define Build/Prepare 26 | if [ -d ./htdoc ]; then \ 27 | $(CP) ./htdoc $(PKG_BUILD_DIR); \ 28 | echo "VITE_APP_NAME=$(APP_NAME)" > $(PKG_BUILD_DIR)/htdoc/.env.local; \ 29 | fi 30 | endef 31 | 32 | define Build/Compile 33 | if [ -d $(PKG_BUILD_DIR)/htdoc ]; then \ 34 | $(NPM) --prefix $(PKG_BUILD_DIR)/htdoc install && \ 35 | $(NPM) --prefix $(PKG_BUILD_DIR)/htdoc run build && \ 36 | $(RM) -rf $(PKG_BUILD_DIR)/htdoc/node_modules; \ 37 | fi 38 | endef 39 | 40 | define Package/$(PKG_NAME)/install 41 | if [ -d $(PKG_BUILD_DIR)/htdoc/dist ]; then \ 42 | $(INSTALL_DIR) $(1)/www/views; \ 43 | $(CP) $(PKG_BUILD_DIR)/htdoc/dist/* $(1)/www/views; \ 44 | fi 45 | if [ -f ./files/menu.json ]; then \ 46 | $(INSTALL_DIR) $(1)/usr/share/oui/menu.d; \ 47 | $(INSTALL_CONF) ./files/menu.json $(1)/usr/share/oui/menu.d/$(APP_NAME).json; \ 48 | fi 49 | if [ -d ./files/rpc ]; then \ 50 | $(CP) ./files/rpc $(1)/usr/share/oui/rpc; \ 51 | fi 52 | endef 53 | 54 | $(eval $(call BuildPackage,$(PKG_NAME))) 55 | -------------------------------------------------------------------------------- /node.mk: -------------------------------------------------------------------------------- 1 | define ColorInfo 2 | tput setaf 6;echo $1;tput sgr0 3 | endef 4 | 5 | define ColorError 6 | tput setaf 1;echo $1;tput sgr0 7 | endef 8 | 9 | ifneq ($(CONFIG_OUI_USE_HOST_NODE),) 10 | NODE_PATH := PATH=$(PATH) 11 | else 12 | NODE_PATH := PATH=$(STAGING_DIR_HOSTPKG)/bin 13 | endif 14 | 15 | NODE := $(NODE_PATH) node 16 | NPM := $(NODE_PATH) npm 17 | 18 | NODE_BIN := $(shell $(NODE_PATH) $(STAGING_DIR_HOST)/bin/which node) 19 | 20 | ifneq ($(NODE_BIN),) 21 | NODE_VER := $(shell $(NODE_BIN) -v | sed 's/v//') 22 | NODE_VER_MAJOR := $(shell echo $(NODE_VER) | cut -d. -f1) 23 | NODE_VER_MINOR := $(shell echo $(NODE_VER) | cut -d. -f2) 24 | endif 25 | 26 | define CheckNode 27 | $(call ColorInfo, "Checking Node.js for building oui..."); \ 28 | if [ -n "$(CONFIG_OUI_USE_HOST_NODE)" ]; \ 29 | then \ 30 | $(call ColorInfo, "Using Node.js from Host"); \ 31 | else \ 32 | $(call ColorInfo, "Using Node.js from OpenWrt"); \ 33 | fi; \ 34 | if [ -z "$(NODE_VER)" ]; \ 35 | then \ 36 | $(call ColorError, "Node.js $(1)+ is required");false; \ 37 | else \ 38 | $(call ColorInfo, "Node.js path: $(NODE_BIN)"); \ 39 | $(call ColorInfo, "Node.js version: $(NODE_VER)"); \ 40 | if [ $(NODE_VER_MAJOR) -lt $(shell echo $(1) | cut -d. -f1) ]; \ 41 | then \ 42 | $(call ColorError, "Node.js $(1)+ is required");false; \ 43 | elif [ $(NODE_VER_MAJOR) -eq $(shell echo $(1) | cut -d. -f1) \ 44 | -a $(NODE_VER_MINOR) -lt $(shell echo $(1) | cut -d. -f2) ]; \ 45 | then \ 46 | $(call ColorError, "Node.js $(1)+ is required");false; \ 47 | fi \ 48 | fi 49 | endef 50 | -------------------------------------------------------------------------------- /oui-rpc-core/files/rpc/system.lua: -------------------------------------------------------------------------------- 1 | local time = require 'eco.time' 2 | local sys = require 'eco.sys' 3 | 4 | local M = {} 5 | 6 | function M.get_cpu_time() 7 | local result = {} 8 | 9 | for line in io.lines('/proc/stat') do 10 | local cpu = line:match('^(cpu%d?)') 11 | if cpu then 12 | local times = {} 13 | for field in line:gmatch('%S+') do 14 | if not field:match('cpu') then 15 | times[#times + 1] = tonumber(field) 16 | end 17 | end 18 | result[cpu] = times 19 | end 20 | end 21 | 22 | return { times = result } 23 | end 24 | 25 | function M.sysupgrade(params) 26 | time.at(0.5, function() 27 | local arg = params.keep and '' or '-n' 28 | os.execute('sysupgrade ' .. arg .. ' /tmp/firmware.bin') 29 | end) 30 | end 31 | 32 | function M.create_backup(params) 33 | local path = params.path 34 | sys.sh({ 'sysupgrade', '-b', path }) 35 | end 36 | 37 | function M.list_backup(params) 38 | local path = params.path 39 | 40 | local f = io.popen('tar -tzf ' .. path) 41 | if not f then 42 | return 43 | end 44 | 45 | local files = f:read('*a') 46 | 47 | f:close() 48 | 49 | return { files = files } 50 | end 51 | 52 | function M.restore_backup(params) 53 | os.execute('sysupgrade -r ' .. params.path) 54 | 55 | time.at(0.5, function() 56 | os.execute('reboot') 57 | end) 58 | end 59 | 60 | function M.reset() 61 | time.at(0.5, function() 62 | os.execute('firstboot -y && reboot') 63 | end) 64 | end 65 | 66 | return M 67 | -------------------------------------------------------------------------------- /oui-rpc-core/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2022 Jianhui Zhao 3 | # 4 | # This is free software, licensed under the MIT. 5 | # 6 | 7 | include $(TOPDIR)/rules.mk 8 | include ../version.mk 9 | 10 | PKG_NAME:=oui-rpc-core 11 | PKG_VERSION:=$(strip $(call findrev)) 12 | PKG_RELEASE:=1 13 | 14 | PKG_MAINTAINER:=Jianhui Zhao 15 | PKG_LICENSE:=MIT 16 | 17 | include $(INCLUDE_DIR)/package.mk 18 | 19 | define Package/oui-rpc-core 20 | SECTION:=oui 21 | CATEGORY:=Oui 22 | TITLE:=Oui rpc core 23 | URL:=https://github.com/zhaojh329/oui 24 | DEPENDS:=+lua-cjson-lua5.4 +lua-eco-uci +lua-eco-md5 +lua-eco-ubus +lua-eco-log \ 25 | +lua-eco-socket +lighttpd +lighttpd-mod-scgi +lighttpd-mod-magnet 26 | endef 27 | 28 | Build/Compile= 29 | 30 | define Package/oui-rpc-core/install 31 | $(INSTALL_DIR) $(1)/usr/sbin $(1)/etc/init.d $(1)/etc/config 32 | $(INSTALL_BIN) ./files/oui.lua $(1)/usr/sbin/oui 33 | $(INSTALL_BIN) ./files/oui.init $(1)/etc/init.d/oui 34 | $(INSTALL_CONF) ./files/oui.config $(1)/etc/config/oui 35 | 36 | $(INSTALL_DIR) $(1)/usr/local/lib/lua/5.4/oui 37 | $(CP) ./files/rpc.lua $(1)/usr/local/lib/lua/5.4/oui 38 | 39 | $(INSTALL_DIR) $(1)/usr/share/oui/acl 40 | $(CP) ./files/rpc $(1)/usr/share/oui 41 | $(INSTALL_CONF) ./files/admin.acl $(1)/usr/share/oui/acl/admin.json 42 | 43 | $(INSTALL_DIR) $(1)/etc/uci-defaults 44 | $(INSTALL_DATA) ./files/oui.default $(1)/etc/uci-defaults/50-oui 45 | 46 | $(INSTALL_DIR) $(1)/etc/lighttpd/conf.d 47 | $(INSTALL_DATA) ./files/lighttpd/oui.conf $(1)/etc/lighttpd/conf.d/50-oui.conf 48 | $(INSTALL_DATA) ./files/lighttpd/handler.lua $(1)/etc/lighttpd/oui-handler.lua 49 | endef 50 | 51 | $(eval $(call BuildPackage,oui-rpc-core)) 52 | -------------------------------------------------------------------------------- /applications/oui-app-upgrade/htdoc/locale.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "Upgrade": "Upgrade", 4 | "Click or drag files to this area to upload": "Click or drag files to this area to upload", 5 | "flash-confirm": "The flash image was uploaded. Below is the checksum and file size listed, compare them with the original file to ensure data integrity. Click '{ btn }' below to start the flash procedure.", 6 | "Cancel": "Cancel", 7 | "OK": "OK", 8 | "Size": "Size", 9 | "Firmware verification failed. Please upload the firmware again": "Firmware verification failed. Please upload the firmware again", 10 | "Keep settings and retain the current configuration": "Keep settings and retain the current configuration", 11 | "Upgrading": "Upgrading..." 12 | }, 13 | "zh-CN": { 14 | "Upgrade": "升级", 15 | "Click or drag files to this area to upload": "点击或者拖动文件到该区域来上传", 16 | "flash-confirm": "固件已上传。下面是列出的校验和及文件大小,将它们与原始文件进行比较以确保数据完整性。 单击下面的 \"{ btn }\" 开始刷写操作", 17 | "Cancel": "算了", 18 | "OK": "确认", 19 | "Size": "大小", 20 | "Firmware verification failed. Please upload the firmware again": "固件校验失败,请重新上传", 21 | "Keep settings and retain the current configuration": "保持设置并保留当前配置", 22 | "Upgrading": "正在升级..." 23 | }, 24 | "zh-TW": { 25 | "Upgrade": "升級", 26 | "Click or drag files to this area to upload": "點擊或者拖動文件到該區域來上傳", 27 | "flash-confirm": "固件已上傳。 下麵是列出的校驗和及文件大小,將它們與原始檔案進行比較以確保數據完整性。 按一下下麵的 \"{ btn }\" 開始刷寫操作", 28 | "Cancel": "算了", 29 | "OK": "確認", 30 | "Size": "大小", 31 | "Firmware verification failed. Please upload the firmware again": "固件校驗失敗,請重新上傳", 32 | "Keep settings and retain the current configuration": "保留目前設定", 33 | "Upgrading": "正在陞級..." 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OUI 2 | 3 | [1]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=plastic 4 | [2]: /LICENSE 5 | [3]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=plastic 6 | [4]: https://github.com/zhaojh329/oui/pulls 7 | [5]: https://img.shields.io/badge/Issues-welcome-brightgreen.svg?style=plastic 8 | [6]: https://github.com/zhaojh329/oui/issues/new 9 | 10 | [![license][1]][2] 11 | [![PRs Welcome][3]][4] 12 | [![Issue Welcome][5]][6] 13 | ![visitors](https://visitor-badge.laobi.icu/badge?page_id=zhaojh329.oui) 14 | 15 | [Lua-eco]: https://github.com/zhaojh329/lua-eco 16 | [Vue3]: https://github.com/vuejs/core 17 | [Element Plus]: https://github.com/element-plus/element-plus 18 | [Vite]: https://github.com/vitejs/vite 19 | 20 | ![](/img/oui.gif) 21 | 22 | A `framework` used to develop Web interface for OpenWrt. 23 | 24 | ## Features 25 | 26 | * Separation of front-end and backend 27 | * Developing back-end APIs using [Lua-eco]. 28 | * Developing front-end pages using [Vue3] + [Element Plus] + [Vite]. 29 | * Support multi-user and ACL management, provides fine-grained permission management. 30 | * Modularization as with Luci, each page is individually packaged as an IPK. 31 | 32 | ## Documentation 33 | 34 | [English](https://zhaojh329.github.io/oui/) 35 | 36 | [中文](https://zhaojh329.github.io/oui/zh/) 37 | 38 | ## Star History 39 | [![Star History Chart](https://api.star-history.com/svg?repos=zhaojh329/oui&type=Date)](https://www.star-history.com/#zhaojh329/oui&Date) 40 | 41 | ## [OpenWrt Web interface](https://openwrt.org/docs/guide-user/luci/webinterface.overview) 42 | 43 | ## Support 44 | 45 | If this project is helpful to you, please don't hesitate to give it a star. Thank you! 46 | 47 | ## ❤️ [Donation](https://zhaojh329.github.io/zhaojh329/) 48 | -------------------------------------------------------------------------------- /oui-ui-core/htdoc/src/timers/index.js: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | /* 3 | * Author: Jianhui Zhao 4 | */ 5 | 6 | class timer { 7 | constructor() { 8 | this.timers = {} 9 | } 10 | 11 | create(name, callback, options) { 12 | if (this.timers[name]) 13 | throw new Error(`[timer.create] name '${name}' is conflicting`) 14 | 15 | this.timers[name] = { 16 | callback: callback, 17 | ...options 18 | } 19 | 20 | const timer = this.timers[name] 21 | 22 | if (typeof timer.time !== 'number') 23 | timer.time = 1000 24 | 25 | if (timer.autostart === undefined || timer.autostart) 26 | this.start(name) 27 | 28 | if (timer.immediate) 29 | timer.callback() 30 | } 31 | 32 | start(name) { 33 | if (!this.timers[name]) 34 | throw new Error(`[timer.start] '${name}' not found`) 35 | 36 | const timer = this.timers[name] 37 | 38 | if (timer.instance) 39 | return 40 | 41 | if (timer.repeat) 42 | timer.instance = setInterval(timer.callback, timer.time) 43 | else 44 | timer.instance = setTimeout(timer.callback, timer.time) 45 | } 46 | 47 | stop(name) { 48 | if (!this.timers[name]) 49 | return 50 | 51 | const timer = this.timers[name] 52 | if (!timer.instance) 53 | return 54 | 55 | if (timer.repeat) 56 | clearInterval(timer.instance) 57 | else 58 | clearTimeout(timer.instance) 59 | 60 | timer.instance = undefined 61 | } 62 | } 63 | 64 | export default { 65 | install: app => { 66 | app.mixin({ 67 | created() { 68 | this.$timer = new timer() 69 | }, 70 | beforeUnmount() { 71 | Object.keys(this.$timer.timers).forEach(name => this.$timer.stop(name)) 72 | this.$timer.timers = {} 73 | } 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /oui-rpc-core/files/rpc/user.lua: -------------------------------------------------------------------------------- 1 | local hex = require 'eco.encoding.hex' 2 | local md5 = require 'eco.hash.md5' 3 | local uci = require 'eco.uci' 4 | 5 | local M = {} 6 | 7 | function M.get_users() 8 | local c = uci.cursor() 9 | local users = {} 10 | 11 | c:foreach('oui', 'user', function(s) 12 | users[#users + 1] = { 13 | username = s.username, 14 | acl = s.acl, 15 | id = s['.name'] 16 | } 17 | end) 18 | 19 | return { users = users } 20 | end 21 | 22 | function M.del_user(params) 23 | local c = uci.cursor() 24 | local id = params.id 25 | 26 | c:delete('oui', id) 27 | c:commit('oui') 28 | end 29 | 30 | function M.change(params) 31 | local c = uci.cursor() 32 | local password = params.password 33 | local acl = params.acl 34 | local id = params.id 35 | 36 | local username = c:get('oui', id, 'username') 37 | if username then 38 | c:set('oui', id, 'password', hex.encode(md5.sum(username .. ':' .. password))) 39 | c:set('oui', id, 'acl', acl or '') 40 | c:commit('oui') 41 | end 42 | end 43 | 44 | function M.add_user(params) 45 | local c = uci.cursor() 46 | local username = params.username 47 | local password = params.password 48 | local acl = params.acl 49 | local exist = false 50 | 51 | c:foreach('oui', 'user', function(s) 52 | if s.username == username then 53 | exist = true 54 | return false 55 | end 56 | end) 57 | 58 | if exist then 59 | return { code = 1, errors = 'already exist' } 60 | end 61 | 62 | local sid = c:add('oui', 'user') 63 | c:set('oui', sid, 'username', username) 64 | c:set('oui', sid, 'password', hex.encode(md5.sum(username .. ':' .. password))) 65 | c:set('oui', sid, 'acl', acl or '') 66 | c:commit('oui') 67 | 68 | return { code = 0 } 69 | end 70 | 71 | return M 72 | -------------------------------------------------------------------------------- /oui-rpc-core/files/rpc/network.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local ubus = require 'eco.ubus' 4 | local file = require 'eco.file' 5 | local uci = require 'eco.uci' 6 | 7 | function M.dhcp_leases() 8 | local c = uci.cursor() 9 | local leases = {} 10 | local leasefile = c:get('dhcp', '@dnsmasq[0]', 'leasefile') or '/tmp/dhcp.leases' 11 | 12 | if not file.access(leasefile) then 13 | return { leases = leases } 14 | end 15 | 16 | local now = os.time() 17 | 18 | for line in io.lines(leasefile) do 19 | local ts, mac, addr, name = line:match("(%S+) +(%S+) +(%S+) +(%S+)") 20 | local expire 21 | 22 | ts = tonumber(ts) 23 | 24 | if ts > now then 25 | expire = ts - now 26 | elseif ts > 0 then 27 | expire = 0 28 | else 29 | expire = -1 30 | end 31 | 32 | leases[#leases + 1] = { 33 | ipaddr = addr, 34 | macaddr = mac, 35 | hostname = name, 36 | expire = expire 37 | } 38 | end 39 | 40 | return { leases = leases } 41 | end 42 | 43 | local function get_networks() 44 | local status = ubus.call('network.interface', 'dump', {}) 45 | return status.interface 46 | end 47 | 48 | local function get_networks_by_route(target, mask) 49 | local networks = get_networks() 50 | local r = {} 51 | 52 | for _, network in ipairs(networks) do 53 | for _, route in ipairs(network.route or {}) do 54 | if route.target == target and route.mask == mask then 55 | r[#r + 1] = network 56 | break 57 | end 58 | end 59 | end 60 | 61 | return r 62 | end 63 | 64 | function M.get_networks() 65 | return { networks = get_networks() } 66 | end 67 | 68 | function M.get_wan_networks() 69 | return { networks = get_networks_by_route('0.0.0.0', 0) } 70 | end 71 | 72 | function M.get_wan6_networks() 73 | return { networks = get_networks_by_route('::', 0) } 74 | end 75 | 76 | return M 77 | -------------------------------------------------------------------------------- /applications/oui-app-backup/htdoc/locale.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "Backup": "Backup", 4 | "Generate backup file": "Generate backup file", 5 | "Restore from backup": "Restore from backup", 6 | "Click or drag files to this area to upload": "Click or drag files to this area to upload", 7 | "restore-confirm": "The uploaded backup archive appears to be valid and contains the files listed below. Press \"{0}\" to restore the backup and reboot, or \"{1}\" to abort the operation.", 8 | "Apply backup": "Apply backup", 9 | "Cancel": "Cancel", 10 | "Continue": "Continue", 11 | "The uploaded backup archive is not readable": "The uploaded backup archive is not readable", 12 | "Reset to defaults": "Reset to defaults", 13 | "Perform reset": "Perform reset", 14 | "Rebooting": "Rebooting", 15 | "ResettConfirm": "Do you really want to erase all settings" 16 | }, 17 | "zh-CN": { 18 | "Backup": "备份", 19 | "Generate backup file": "生成备份文件", 20 | "Restore from backup": "从备份恢复", 21 | "Click or drag files to this area to upload": "点击或者拖动文件到该区域来上传", 22 | "restore-confirm": "上传的备份归档有效,并且包含以下列出的文件。点击“{0}”恢复备份并重新启动,或点击“{1}”中止操作。", 23 | "Apply backup": "应用备份", 24 | "Cancel": "算了", 25 | "Continue": "继续", 26 | "The uploaded backup archive is not readable": "无法读取上传的备份归档", 27 | "Reset to defaults": "恢复到出厂设置", 28 | "Perform reset": "执行重置", 29 | "Rebooting": "正在重启", 30 | "ResettConfirm": "您确定要清除所有设置吗" 31 | }, 32 | "zh-TW": { 33 | "Backup": "備份", 34 | "Generate backup file": "生成備份檔案", 35 | "Restore from backup": "從備份恢復", 36 | "Click or drag files to this area to upload": "點擊或者拖動文件到該區域來上傳", 37 | "restore-confirm": "上傳的備份檔無效且包含下列檔案。按「{0}」來還原備份並重啟,或「{1}」以終止動作。", 38 | "Apply backup": "是否套用備份", 39 | "Cancel": "算了", 40 | "Continue": "繼續", 41 | "The uploaded backup archive is not readable": "上傳的復原檔案無法讀取", 42 | "Reset to defaults": "回復預設值", 43 | "Perform reset": "執行重置", 44 | "Rebooting": "正在重啓", 45 | "ResettConfirm": "您確定要清除所有設定" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /oui-ui-core/files/menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "/status": { 3 | "title": "Status", 4 | "icon": "md-stats", 5 | "index": 10, 6 | "locales": { 7 | "en": "Status", 8 | "zh-CN": "状态", 9 | "zh-TW": "狀態" 10 | }, 11 | "svg":{"-xmlns":"http://www.w3.org/2000/svg","-xmlns:xlink":"http://www.w3.org/1999/xlink","-viewBox":"0 0 24 24","path":{"-d":"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z","-fill":"currentColor"}} 12 | }, 13 | "/system": { 14 | "title": "System", 15 | "icon": "md-settings", 16 | "index": 20, 17 | "locales": { 18 | "en": "System", 19 | "zh-CN": "系统", 20 | "zh-TW": "系統" 21 | }, 22 | "svg":{"-xmlns":"http://www.w3.org/2000/svg","-xmlns:xlink":"http://www.w3.org/1999/xlink","-viewBox":"0 0 24 24","g":{"-fill":"none","path":{"-d":"M4.946 5h14.108C20.678 5 22 6.304 22 7.92v8.16c0 1.616-1.322 2.92-2.946 2.92H4.946C3.322 19 2 17.696 2 16.08V7.92C2 6.304 3.322 5 4.946 5zm0 2A.933.933 0 0 0 4 7.92v8.16c0 .505.42.92.946.92h14.108a.933.933 0 0 0 .946-.92V7.92c0-.505-.42-.92-.946-.92H4.946z","-fill":"currentColor"}}} 23 | }, 24 | "/network": { 25 | "title": "Network", 26 | "icon": "md-git-network", 27 | "index": 30, 28 | "locales": { 29 | "en": "Network", 30 | "zh-CN": "网络", 31 | "zh-TW": "網絡" 32 | }, 33 | "svg":{"-xmlns":"http://www.w3.org/2000/svg","-xmlns:xlink":"http://www.w3.org/1999/xlink","-viewBox":"0 0 640 512","path":{"-d":"M640 264v-16c0-8.84-7.16-16-16-16H344v-40h72c17.67 0 32-14.33 32-32V32c0-17.67-14.33-32-32-32H224c-17.67 0-32 14.33-32 32v128c0 17.67 14.33 32 32 32h72v40H16c-8.84 0-16 7.16-16 16v16c0 8.84 7.16 16 16 16h104v40H64c-17.67 0-32 14.33-32 32v128c0 17.67 14.33 32 32 32h160c17.67 0 32-14.33 32-32V352c0-17.67-14.33-32-32-32h-56v-40h304v40h-56c-17.67 0-32 14.33-32 32v128c0 17.67 14.33 32 32 32h160c17.67 0 32-14.33 32-32V352c0-17.67-14.33-32-32-32h-56v-40h104c8.84 0 16-7.16 16-16zM256 128V64h128v64H256zm-64 320H96v-64h96v64zm352 0h-96v-64h96v64z","-fill":"currentColor"}} 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/formal.yml: -------------------------------------------------------------------------------- 1 | name: Test Formalities 2 | 3 | on: 4 | pull_request: 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | build: 11 | name: Test Formalities 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | with: 19 | ref: ${{ github.event.pull_request.head.sha }} 20 | fetch-depth: 0 21 | 22 | - name: Determine branch name 23 | run: | 24 | BRANCH="${GITHUB_BASE_REF#refs/heads/}" 25 | echo "Building for $BRANCH" 26 | echo "BRANCH=$BRANCH" >> $GITHUB_ENV 27 | 28 | - name: Test formalities 29 | run: | 30 | source .github/workflows/scripts/ci_helpers.sh 31 | 32 | RET=0 33 | for commit in $(git rev-list HEAD ^origin/$BRANCH); do 34 | info "=== Checking commit '$commit'" 35 | if git show --format='%P' -s $commit | grep -qF ' '; then 36 | err "Pull request should not include merge commits" 37 | RET=1 38 | fi 39 | 40 | author="$(git show -s --format=%aN $commit)" 41 | if echo $author | grep -q '\S\+\s\+\S\+'; then 42 | success "Author name ($author) seems ok" 43 | else 44 | err "Author name ($author) need to be your real name 'firstname lastname'" 45 | RET=1 46 | fi 47 | 48 | subject="$(git show -s --format=%s $commit)" 49 | if echo "$subject" | grep -q -e '^[0-9A-Za-z,+/_\.-]\+: ' -e '^Revert '; then 50 | success "Commit subject line seems ok ($subject)" 51 | else 52 | err "Commit subject line MUST start with ': ' ($subject)" 53 | RET=1 54 | fi 55 | 56 | body="$(git show -s --format=%b $commit)" 57 | sob="$(git show -s --format='Signed-off-by: %aN <%aE>' $commit)" 58 | if echo "$body" | grep -qF "$sob"; then 59 | success "Signed-off-by match author" 60 | else 61 | err "Signed-off-by is missing or doesn't match author (should be '$sob')" 62 | RET=1 63 | fi 64 | done 65 | 66 | exit $RET 67 | -------------------------------------------------------------------------------- /applications/oui-app-stations/htdoc/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /oui-ui-core/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2022 Jianhui Zhao 3 | # 4 | # This is free software, licensed under the MIT. 5 | # 6 | 7 | include $(TOPDIR)/rules.mk 8 | include ../version.mk 9 | include ../node.mk 10 | 11 | PKG_NAME:=oui-ui-core 12 | PKG_VERSION:=$(strip $(call findrev)) 13 | PKG_RELEASE:=1 14 | 15 | PKG_MAINTAINER:=Jianhui Zhao 16 | PKG_LICENSE:=MIT 17 | 18 | PKG_BUILD_DEPENDS:=!OUI_USE_HOST_NODE:node/host 19 | 20 | PKG_CONFIG_DEPENDS:= \ 21 | CONFIG_OUI_USE_HOST_NODE \ 22 | CONFIG_OUI_LOGIN_VIEW \ 23 | CONFIG_OUI_LAYOUT_VIEW \ 24 | CONFIG_OUI_HOME_VIEW 25 | 26 | include $(INCLUDE_DIR)/package.mk 27 | 28 | define Package/oui-ui-core 29 | SECTION:=oui 30 | CATEGORY:=Oui 31 | TITLE:=Oui ui core 32 | URL:=https://github.com/zhaojh329/oui 33 | DEPENDS:=+oui-rpc-core 34 | PKGARCH:=all 35 | endef 36 | 37 | define Package/oui-ui-core/config 38 | config OUI_LOGIN_VIEW 39 | string "Customize the login view" 40 | default "login" 41 | 42 | config OUI_LAYOUT_VIEW 43 | string "Customize the layout view" 44 | default "layout" 45 | 46 | config OUI_HOME_VIEW 47 | string "Customize the home view" 48 | default "home" 49 | 50 | config OUI_USE_HOST_NODE 51 | bool "Use existing nodejs installation on the host system" 52 | default n 53 | help 54 | This disables the build dependency on the node package from 55 | the OpenWrt packages feed, allowing for faster development 56 | builds. 57 | endef 58 | 59 | define Package/oui-ui-core/conffiles 60 | /etc/config/oui 61 | endef 62 | 63 | define Build/Prepare 64 | $(call CheckNode,20.9) 65 | $(CP) ./htdoc $(PKG_BUILD_DIR) 66 | echo "VITE_OUI_LOGIN_VIEW=$(CONFIG_OUI_LOGIN_VIEW)" > $(PKG_BUILD_DIR)/htdoc/.env.local 67 | echo "VITE_OUI_LAYOUT_VIEW=$(CONFIG_OUI_LAYOUT_VIEW)" >> $(PKG_BUILD_DIR)/htdoc/.env.local 68 | echo "VITE_OUI_HOME_VIEW=$(CONFIG_OUI_HOME_VIEW)" >> $(PKG_BUILD_DIR)/htdoc/.env.local 69 | $(NPM) --prefix $(PKG_BUILD_DIR)/htdoc install 70 | endef 71 | 72 | define Build/Compile 73 | $(NPM) --prefix $(PKG_BUILD_DIR)/htdoc run build 74 | endef 75 | 76 | define Package/oui-ui-core/install 77 | $(INSTALL_DIR) $(1)/usr/share/oui/menu.d $(1)/www 78 | $(INSTALL_CONF) ./files/menu.json $(1)/usr/share/oui/menu.d/base.json 79 | $(CP) $(PKG_BUILD_DIR)//htdoc/dist/* $(1)/www 80 | mv $(1)/www/index.html $(1)/www/oui.html 81 | endef 82 | 83 | $(eval $(call BuildPackage,oui-ui-core)) 84 | -------------------------------------------------------------------------------- /docs/src/zh/guide/acl.md: -------------------------------------------------------------------------------- 1 | # 权限管理 2 | 3 | ## 介绍 4 | 5 | Oui 将权限分为权限组,每个权限组里面又分为权限类,每个权限类由多个匹配项构成。每个用户需要为其分配一个权限组。 6 | 7 | Oui 默认具有一个名为 `admin` 的权限组,其配置文件为:/usr/share/oui/acl/admin.json 8 | 9 | ```json 10 | { 11 | "rpc": { 12 | "matchs": [".+"] 13 | }, 14 | "menu": { 15 | "matchs": [".+"] 16 | }, 17 | "ubus": { 18 | "matchs": [".+"] 19 | }, 20 | "uci": { 21 | "matchs": [".+"] 22 | } 23 | } 24 | ``` 25 | 26 | 目前共有 4 个权限类: 27 | 28 | * rpc - `rpc` 接口调用权限 29 | * menu - 菜单隐藏或显示 30 | * ubus - `ubus` 调用权限 31 | * uci - `uci` 操作权限 32 | 33 | 匹配项为一个数组,`admin` 权限组中全部的匹配项均为 `.+`,表示匹配任意,即每一类都拥有所有权限。 34 | 35 | :::tip 36 | 这里的匹配项事实上为一个正则表达式。可以是任意的 `Lua` 正则表达式。 37 | ::: 38 | 39 | ## 反向匹配 40 | 41 | ```json 42 | { 43 | "rpc": { 44 | "matchs": ["^uci.get$"], 45 | "reverse": true 46 | } 47 | } 48 | ``` 49 | 给权限类的 `reverse` 属性设置为 `true` 即可反向匹配。 50 | 51 | ## 匹配项示例 52 | 53 | ### rpc 54 | 55 | ```json 56 | { 57 | "rpc": { 58 | "matchs": [".+"] 59 | } 60 | } 61 | ``` 62 | 匹配所有 `rpc` 接口 63 | 64 | ```json 65 | { 66 | "rpc": { 67 | "matchs": ["^uci%..+"] 68 | } 69 | } 70 | ``` 71 | 匹配 `uci` 模块里面所有的方法 72 | 73 | ```json 74 | { 75 | "rpc": { 76 | "matchs": ["^uci%..+", "^system%..+"] 77 | } 78 | } 79 | ``` 80 | 匹配 `uci` 和 `system` 模块里面所有的方法 81 | 82 | ```json 83 | { 84 | "rpc": { 85 | "matchs": ["^uci%.get$"] 86 | } 87 | } 88 | ``` 89 | 匹配 `uci` 模块里面的 `get` 方法 90 | 91 | ```json 92 | { 93 | "rpc": { 94 | "matchs": ["^uci%.get$"], 95 | "reverse": true 96 | } 97 | } 98 | ``` 99 | 不匹配 `uci` 模块的 `get` 方法,即除了 `uci` 模块的 `get` 方法不能调用,其余所有接口均能调用。 100 | 101 | ### menu 102 | 103 | ```json 104 | { 105 | "menu": { 106 | "matchs": ["^/system/"] 107 | } 108 | } 109 | ``` 110 | 匹配 `/system/` 开头的菜单 111 | 112 | ```json 113 | { 114 | "menu": { 115 | "matchs": ["^/system/upgrade$"] 116 | } 117 | } 118 | ``` 119 | 匹配 `/system/upgrade` 菜单 120 | 121 | ```json 122 | { 123 | "menu": { 124 | "matchs": ["^/system/upgrade$"], 125 | "reverse": true 126 | } 127 | } 128 | ``` 129 | 隐藏 `/system/upgrade` 菜单 130 | 131 | ### uci 132 | 133 | ```json 134 | { 135 | "uci": { 136 | "matchs": ["^system$"] 137 | } 138 | } 139 | ``` 140 | 只允许操作 `/etc/config/system` 141 | -------------------------------------------------------------------------------- /oui-rpc-core/files/rpc/uci.lua: -------------------------------------------------------------------------------- 1 | local rpc = require 'oui.rpc' 2 | local uci = require 'eco.uci' 3 | 4 | local M = {} 5 | 6 | function M.load(params, session) 7 | local config = params.config 8 | local c = uci.cursor() 9 | 10 | if not rpc.acl_match(session, config, 'uci') then 11 | return nil, rpc.ERROR_CODE_PERMISSION_DENIED 12 | end 13 | 14 | return c:get_all(params.config) 15 | end 16 | 17 | function M.get(params, session) 18 | local c = uci.cursor() 19 | local config = params.config 20 | local section = params.section 21 | local option = params.option 22 | 23 | if not rpc.acl_match(session, config, 'uci') then 24 | return nil, rpc.ERROR_CODE_PERMISSION_DENIED 25 | end 26 | 27 | return c:get(config, section, option) 28 | end 29 | 30 | function M.set(params, session) 31 | local c = uci.cursor() 32 | local config = params.config 33 | local section = params.section 34 | 35 | if not rpc.acl_match(session, config, 'uci') then 36 | return nil, rpc.ERROR_CODE_PERMISSION_DENIED 37 | end 38 | 39 | for option, value in pairs(params.values) do 40 | c:set(config, section, option, value) 41 | end 42 | 43 | c:commit(config) 44 | end 45 | 46 | function M.delete(params, session) 47 | local c = uci.cursor() 48 | local config = params.config 49 | local section = params.section 50 | local options = params.options 51 | 52 | if not rpc.acl_match(session, config, 'uci') then 53 | return nil, rpc.ERROR_CODE_PERMISSION_DENIED 54 | end 55 | 56 | if options then 57 | for _, option in ipairs(options) do 58 | c:delete(config, section, option) 59 | end 60 | else 61 | c:delete(config, section) 62 | end 63 | 64 | c:commit(config) 65 | end 66 | 67 | function M.add(params, session) 68 | local c = uci.cursor() 69 | local config = params.config 70 | local typ = params.type 71 | local name = params.name 72 | local values = params.values 73 | 74 | if not rpc.acl_match(session, config, 'uci') then 75 | return nil, rpc.ERROR_CODE_PERMISSION_DENIED 76 | end 77 | 78 | if name then 79 | c:set(config, name, typ) 80 | else 81 | name = c:add(config, typ) 82 | end 83 | 84 | for option, value in pairs(values) do 85 | c:set(config, name, option, value) 86 | end 87 | 88 | c:commit(config) 89 | end 90 | 91 | return M 92 | -------------------------------------------------------------------------------- /applications/oui-app-acl/htdoc/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /applications/oui-app-login/htdoc/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 63 | 64 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /applications/oui-app-home/htdoc/locale.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "CPU Usage": "CPU Usage", 4 | "Memory Usage": "Memory Usage", 5 | "Storage Usage": "Storage Usage", 6 | "Available": "Available", 7 | "Used": "Used", 8 | "Buffered": "Buffered", 9 | "Cached": "Cached", 10 | "Total": "Total", 11 | "System": "System", 12 | "Hostname": "Hostname", 13 | "Model": "Model", 14 | "Architecture": "Architecture", 15 | "Target Platform": "Target Platform", 16 | "Firmware Version": "Firmware Version", 17 | "Kernel Version": "Kernel Version", 18 | "Uptime": "Uptime", 19 | "Load Average": "Load Average", 20 | "Upstream": "Upstream", 21 | "Protocol": "Protocol", 22 | "Prefix Delegated": "Prefix Delegated", 23 | "Address": "Address", 24 | "Gateway": "Gateway", 25 | "DNS": "DNS", 26 | "Connected": "Connected" 27 | }, 28 | "zh-CN": { 29 | "CPU Usage": "CPU使用率", 30 | "Memory Usage": "内存使用率", 31 | "Storage Usage": "存储使用率", 32 | "Available": "可使用", 33 | "Used": "已使用", 34 | "Buffered": "已缓冲", 35 | "Cached": "已缓存", 36 | "Total": "总计", 37 | "System": "系统", 38 | "Hostname": "主机名", 39 | "Model": "型号", 40 | "Architecture": "架构", 41 | "Target Platform": "目标平台", 42 | "Firmware Version": "固件版本", 43 | "Kernel Version": "内核版本", 44 | "Uptime": "运行时间", 45 | "Load Average": "平均负载", 46 | "Upstream": "上游", 47 | "Protocol": "协议", 48 | "Prefix Delegated": "分发前缀", 49 | "Address": "地址", 50 | "Gateway": "网关", 51 | "DNS": "DNS", 52 | "Connected": "已连接" 53 | }, 54 | "zh-TW": { 55 | "CPU Usage": "CPU使用率", 56 | "Memory Usage": "内存使用率", 57 | "Storage Usage": "存儲使用率", 58 | "Available": "可使用", 59 | "Used": "已使用", 60 | "Buffered": "已緩沖", 61 | "Cached": "已緩存", 62 | "Total": "總計", 63 | "System": "系統", 64 | "Hostname": "主機名", 65 | "Model": "型號", 66 | "Architecture": "架構", 67 | "Target Platform": "目標平臺", 68 | "Firmware Version": "固件版本", 69 | "Kernel Version": "內核版本", 70 | "Uptime": "運行時間", 71 | "Load Average": "平均負載", 72 | "Upstream": "上游", 73 | "Protocol": "協議", 74 | "Prefix Delegated": "前綴委派", 75 | "Address": "地址", 76 | "Gateway": "網關", 77 | "DNS": "DNS", 78 | "Connected": "已連接" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /docs/src/zh/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | # 快速上手 2 | 3 | ## 编译/安装 4 | 5 | :::tip 6 | Oui 依赖最新版本的 `Lua-eco`。 7 | 8 | 请确保你使用的 `OpenWrt` 中的 `Lua-eco` 已更新到最新版本。 9 | 10 | 你可以直接使用 [https://github.com/openwrt/packages/blob/master/lang/lua-eco/Makefile](https://github.com/openwrt/packages/blob/master/lang/lua-eco/Makefile) 11 | 替换 `feeds/packages/lang/lua-eco/Makefile` 12 | ::: 13 | 14 | ### 添加 feed 15 | 16 | ``` bash 17 | echo "src-git oui https://github.com/zhaojh329/oui.git" >> feeds.conf.default 18 | ``` 19 | 20 | ### 更新feed 21 | 22 | ``` bash 23 | ./scripts/feeds update -a 24 | ./scripts/feeds install -a -p oui 25 | ``` 26 | 27 | ### 配置 28 | 29 | ``` 30 | OUI ---> 31 | Applications ---> 32 | <*> oui-app-acl. ACL 33 | <*> oui-app-backup. Backup / Restore 34 | <*> oui-app-dhcp-lease. DHCP lease 35 | <*> oui-app-home. OUI built-in home page 36 | <*> oui-app-layout. OUI built-in layout page 37 | <*> oui-app-login. OUI built-in login page 38 | <*> oui-app-stations. Stations 39 | <*> oui-app-system. System Configure 40 | <*> oui-app-upgrade. Upgrade 41 | <*> oui-app-user. User 42 | -*- oui-rpc-core. Oui rpc core 43 | -*- oui-ui-core. Oui ui core 44 | [*] Use existing nodejs installation on the host system 45 | ``` 46 | 47 | ::: tip 48 | 编译 Oui 需要用到 Node,而且版本不能低于 20.9。 49 | 50 | 勾选 `Use existing nodejs installation on the host system` 可节约编译时间,需要确保主机上安装的 Node 版本不低于 20.9。 51 | 52 | 你可以使用 [nvm](https://github.com/nvm-sh/nvm) 管理多个 Node 版本。 53 | ::: 54 | 55 | ### 编译 56 | 57 | ``` bash 58 | make V=s 59 | ``` 60 | 61 | ::: tip 62 | 默认用户名:admin 63 | 64 | 默认密码:123456 65 | ::: 66 | 67 | ## 开发/调试 68 | 69 | 首先修改 http 代理: oui-ui-core/htdoc/vite.config.js 70 | ```js 71 | { 72 | server: { 73 | proxy: { 74 | '/oui-rpc': { 75 | target: 'http://openwrt.lan', 76 | secure: false 77 | }, 78 | '/oui-upload': { 79 | target: 'http://openwrt.lan', 80 | secure: false 81 | }, 82 | '/oui-download': { 83 | target: 'http://openwrt.lan', 84 | secure: false 85 | } 86 | } 87 | } 88 | } 89 | ``` 90 | 将其中的 `http://openwrt.lan` 修改为你的调试设备的地址,如 `http://192.168.1.1` 91 | 92 | 1. 使用 vscode 打开 oui 项目 93 | 2. 进入 `oui-ui-core/htdoc` 目录 94 | 3. 执行 `npm install` 95 | 4. 执行 `npm run dev` 96 | 97 | 执行完 `npm run dev` 后,根据提示打开浏览器。此时对代码中的任何修改,都将立即呈现在浏览器中。 98 | 99 | :::tip 100 | 创建新的 app 后,需要重新执行 `npm run dev` 101 | 102 | 建议在 wsl 或 linux 虚拟机里做开发 103 | ::: 104 | -------------------------------------------------------------------------------- /applications/oui-app-upgrade/htdoc/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /applications/oui-app-system/htdoc/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /docs/src/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | import { defineUserConfig } from 'vuepress' 2 | import { registerComponentsPlugin } from '@vuepress/plugin-register-components' 3 | import { searchPlugin } from '@vuepress/plugin-search' 4 | import { getDirname, path } from '@vuepress/utils' 5 | import { defaultTheme } from '@vuepress/theme-default' 6 | 7 | const __dirname = getDirname(import.meta.url) 8 | 9 | export default defineUserConfig({ 10 | base: '/oui/', 11 | title: 'Oui', 12 | locales: { 13 | '/': { 14 | lang: 'en', 15 | description: 'A framework used to develop Web interface for OpenWrt' 16 | }, 17 | '/zh/': { 18 | lang: 'zh-CN', 19 | description: '一个用于开发 OpenWrt Web 接口的框架' 20 | } 21 | }, 22 | theme: defaultTheme({ 23 | repo: 'zhaojh329/oui', 24 | docsBranch: 'master', 25 | docsDir: 'docs/src', 26 | locales: { 27 | '/': { 28 | navbar: [ 29 | { 30 | text: 'Guide', 31 | link: '/guide/' 32 | } 33 | ], 34 | sidebar: { 35 | '/guide/': [ 36 | { 37 | text: 'Guide', 38 | children: [ 39 | '/guide/README.md', 40 | '/guide/getting-started.md', 41 | '/guide/page.md', 42 | '/guide/vue-api.md', 43 | '/guide/lua-api.md', 44 | '/guide/acl.md' 45 | ] 46 | } 47 | ] 48 | } 49 | }, 50 | '/zh/': { 51 | selectLanguageName: '简体中文', 52 | selectLanguageText: '选择语言', 53 | selectLanguageAriaLabel: '选择语言', 54 | editLinkText: '在 GitHub 上编辑此页', 55 | lastUpdatedText: '上次更新', 56 | contributorsText: '贡献者', 57 | tip: '提示', 58 | warning: '注意', 59 | danger: '警告', 60 | navbar: [ 61 | { 62 | text: '指南', 63 | link: '/zh/guide/' 64 | } 65 | ], 66 | sidebar: { 67 | '/zh/guide/': [ 68 | { 69 | text: '指南', 70 | children: [ 71 | '/zh/guide/README.md', 72 | '/zh/guide/getting-started.md', 73 | '/zh/guide/page.md', 74 | '/zh/guide/vue-api.md', 75 | '/zh/guide/lua-api.md', 76 | '/zh/guide/acl.md' 77 | ] 78 | } 79 | ] 80 | } 81 | } 82 | } 83 | }), 84 | plugins: [ 85 | registerComponentsPlugin({ 86 | componentsDir: path.resolve(__dirname, './components') 87 | }), 88 | searchPlugin({ 89 | locales: { 90 | '/': { 91 | placeholder: 'Search', 92 | }, 93 | '/zh/': { 94 | placeholder: '搜索' 95 | } 96 | } 97 | }) 98 | ] 99 | }) 100 | -------------------------------------------------------------------------------- /docs/src/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | # Get Started 2 | 3 | ## Build & Install 4 | 5 | :::tip 6 | Oui depends on the latest version of `Lua-eco`. 7 | 8 | Make sure that the `Lua-eco` in OpenWrt you are using is up to date. 9 | 10 | You can replace `feeds/packages/lang/lua-eco/Makefile` with 11 | [https://github.com/openwrt/packages/blob/master/lang/lua-eco/Makefile](https://github.com/openwrt/packages/blob/master/lang/lua-eco/Makefile). 12 | ::: 13 | 14 | ### Add feed 15 | 16 | ``` bash 17 | echo "src-git oui https://github.com/zhaojh329/oui.git" >> feeds.conf.default 18 | ``` 19 | 20 | ### Update feed 21 | 22 | ``` bash 23 | ./scripts/feeds update -a 24 | ./scripts/feeds install -a -p oui 25 | ``` 26 | 27 | ### Configure 28 | 29 | ``` 30 | OUI ---> 31 | Applications ---> 32 | <*> oui-app-acl. ACL 33 | <*> oui-app-backup. Backup / Restore 34 | <*> oui-app-dhcp-lease. DHCP lease 35 | <*> oui-app-home. OUI built-in home page 36 | <*> oui-app-layout. OUI built-in layout page 37 | <*> oui-app-login. OUI built-in login page 38 | <*> oui-app-stations. Stations 39 | <*> oui-app-system. System Configure 40 | <*> oui-app-upgrade. Upgrade 41 | <*> oui-app-user. User 42 | -*- oui-rpc-core. Oui rpc core 43 | -*- oui-ui-core. Oui ui core 44 | [*] Use existing nodejs installation on the host system 45 | ``` 46 | 47 | ::: tip 48 | The `Node.js 20.9+` is required to compile Oui. 49 | 50 | Select `Use existing nodejs installation on the host system` to reduce compilation time. 51 | 52 | You can manage multiple versions of Node with [nvm](https://github.com/nvm-sh/nvm). 53 | ::: 54 | 55 | ### Build 56 | 57 | ``` bash 58 | make V=s 59 | ``` 60 | 61 | ::: tip 62 | Default username: admin 63 | 64 | Default password: 123456 65 | ::: 66 | 67 | ## Development & Debugging 68 | 69 | Start by modifying the HTTP proxy: oui-ui-core/htdoc/vite.config.js 70 | ```js 71 | { 72 | server: { 73 | proxy: { 74 | '/oui-rpc': { 75 | target: 'http://openwrt.lan', 76 | secure: false 77 | }, 78 | '/oui-upload': { 79 | target: 'http://openwrt.lan', 80 | secure: false 81 | }, 82 | '/oui-download': { 83 | target: 'http://openwrt.lan', 84 | secure: false 85 | } 86 | } 87 | } 88 | } 89 | ``` 90 | Change the `http://openwrt.lan` to the address of your debug device, such as `http://192.168.1.1` 91 | 92 | 1. Open the OUI project using VSCode 93 | 2. Enter into the directory: `oui-ui-core/htdoc` 94 | 3. Execute `npm install` 95 | 4. Execute `npm run dev` 96 | 97 | After running `npm run dev`, open the browser as prompted. Any changes made to the code at this point are immediately rendered in the browser. 98 | 99 | :::tip 100 | After creating a new app, you need to run `npm run dev` again. 101 | ::: 102 | -------------------------------------------------------------------------------- /oui-ui-core/htdoc/vite.config.js: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | /* 3 | * Author: Jianhui Zhao 4 | */ 5 | 6 | import { defineConfig } from 'vite' 7 | import viteCompression from 'vite-plugin-compression' 8 | import vueI18n from '@intlify/unplugin-vue-i18n/vite' 9 | import eslint from '@nabla/vite-plugin-eslint' 10 | import vue from '@vitejs/plugin-vue' 11 | import path from 'path' 12 | import fs from 'fs' 13 | 14 | function transformRoutes() { 15 | let config 16 | 17 | return { 18 | name: 'transform-routes', 19 | 20 | configResolved(resolvedConfig) { 21 | config = resolvedConfig 22 | }, 23 | 24 | transform(src, id) { 25 | if (config.command === 'serve') 26 | return 27 | 28 | if (/src\/router\/development\.js$/.test(id)) { 29 | return { 30 | code: 'export default function(routes, menus, loginView, layoutView, homeView){}' 31 | } 32 | } 33 | } 34 | } 35 | } 36 | 37 | export default defineConfig(({ mode }) => { 38 | let menus 39 | 40 | if (mode === 'development') { 41 | menus = JSON.parse(fs.readFileSync(path.resolve(path.dirname(__dirname), 'files', 'menu.json'))) 42 | 43 | const appsDir = path.resolve(path.dirname(path.dirname(__dirname)), 'applications') 44 | const destDir = path.resolve(__dirname, 'src', 'applications') 45 | 46 | fs.rmSync(destDir, {force: true, recursive: true}) 47 | 48 | fs.readdirSync(appsDir).forEach(appName => { 49 | const appDir = path.join(appsDir, appName) 50 | 51 | if (!fs.statSync(appDir).isDirectory()) 52 | return 53 | 54 | fs.mkdirSync(path.resolve(destDir, appName), {recursive: true}) 55 | fs.symlinkSync(path.resolve(appDir, 'htdoc'), path.resolve(destDir, appName, 'htdoc')) 56 | 57 | const menuFile = path.resolve(appDir, 'files', 'menu.json') 58 | if (fs.existsSync(menuFile)) { 59 | const menu = JSON.parse(fs.readFileSync(menuFile)) 60 | Object.assign(menus, menu) 61 | } 62 | }) 63 | } 64 | 65 | return { 66 | define: { 67 | __MENUS__: menus 68 | }, 69 | resolve: { 70 | preserveSymlinks: true 71 | }, 72 | build: { 73 | chunkSizeWarningLimit: 1500 74 | }, 75 | plugins: [ 76 | transformRoutes(), 77 | vue(), 78 | viteCompression({ 79 | deleteOriginFile: true 80 | }), 81 | vueI18n({ 82 | compositionOnly: false 83 | }), 84 | eslint() 85 | ], 86 | server: { 87 | proxy: { 88 | '/oui-rpc': { 89 | target: 'http://openwrt.lan', 90 | secure: false 91 | }, 92 | '/oui-upload': { 93 | target: 'http://openwrt.lan', 94 | secure: false 95 | }, 96 | '/oui-download': { 97 | target: 'http://openwrt.lan', 98 | secure: false 99 | } 100 | } 101 | } 102 | } 103 | }) 104 | -------------------------------------------------------------------------------- /applications/oui-app-dhcp-lease/htdoc/index.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /docs/src/guide/acl.md: -------------------------------------------------------------------------------- 1 | # ACL 2 | 3 | ## Introduction 4 | 5 | Oui divides permissions into permission groups, and each permission group is divided into permission classes. 6 | Each permission class consists of multiple matching items. Each user needs to be assigned a permission group. 7 | 8 | By default, Oui has a permission group named `admin`, whose configuration file is: /usr/share/oui/acl/admin.json 9 | 10 | ```json 11 | { 12 | "rpc": { 13 | "matchs": [".+"] 14 | }, 15 | "menu": { 16 | "matchs": [".+"] 17 | }, 18 | "ubus": { 19 | "matchs": [".+"] 20 | }, 21 | "uci": { 22 | "matchs": [".+"] 23 | } 24 | } 25 | ``` 26 | 27 | Currently, there are four permission classes: 28 | 29 | * rpc - `rpc` interface call permission 30 | * menu - Hidden or show menu 31 | * ubus - `ubus` call permission 32 | * uci - `uci` operating permission 33 | 34 | The matching items are an array, and all the matching items in the `admin` permission group are `.+ `, indicating any matching, that is, each category has all permissions. 35 | 36 | :::tip 37 | The match here is actually a regular expression. Can be any `Lua` regular expression. 38 | ::: 39 | 40 | ## Reverse matching 41 | 42 | ```json 43 | { 44 | "rpc": { 45 | "matchs": ["^uci.get$"], 46 | "reverse": true 47 | } 48 | } 49 | ``` 50 | Set the `reverse` attribute of the permission class to `true` to reverse the matching. 51 | 52 | ## Examples of matches 53 | 54 | ### rpc 55 | 56 | ```json 57 | { 58 | "rpc": { 59 | "matchs": [".+"] 60 | } 61 | } 62 | ``` 63 | Matches all `rpc` interfaces 64 | 65 | ```json 66 | { 67 | "rpc": { 68 | "matchs": ["^uci%..+"] 69 | } 70 | } 71 | ``` 72 | Matches all methods in the `uci` module 73 | 74 | ```json 75 | { 76 | "rpc": { 77 | "matchs": ["^uci%..+", "^system%..+"] 78 | } 79 | } 80 | ``` 81 | Matches all methods in the `uci` and `system` modules 82 | 83 | ```json 84 | { 85 | "rpc": { 86 | "matchs": ["^uci%.get$"] 87 | } 88 | } 89 | ``` 90 | Matches the `get` method in the `uci` module 91 | 92 | ```json 93 | { 94 | "rpc": { 95 | "matchs": ["^uci%.get$"], 96 | "reverse": true 97 | } 98 | } 99 | ``` 100 | Does not match the `get` method of the `uci` module, that is, except the `get` method of the `uci` module cannot be called, all other interfaces can be called. 101 | 102 | ### menu 103 | 104 | ```json 105 | { 106 | "menu": { 107 | "matchs": ["^/system/"] 108 | } 109 | } 110 | ``` 111 | Matches menus starting with `/system/` 112 | 113 | ```json 114 | { 115 | "menu": { 116 | "matchs": ["^/system/upgrade$"] 117 | } 118 | } 119 | ``` 120 | Match `/system/upgrade` menu 121 | 122 | ```json 123 | { 124 | "menu": { 125 | "matchs": ["^/system/upgrade$"], 126 | "reverse": true 127 | } 128 | } 129 | ``` 130 | Hide the `/system/upgrade` menu 131 | 132 | ### uci 133 | 134 | ```json 135 | { 136 | "uci": { 137 | "matchs": ["^system$"] 138 | } 139 | } 140 | ``` 141 | Only `/etc/config/system` is allowed 142 | -------------------------------------------------------------------------------- /oui-ui-core/htdoc/src/router/index.js: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | /* 3 | * Author: Jianhui Zhao 4 | */ 5 | 6 | import {createRouter, createWebHashHistory} from 'vue-router' 7 | import addRoutesDev from './development.js' 8 | import oui from '../oui' 9 | 10 | function loadView(name) { 11 | return new Promise((resolve, reject) => { 12 | const script = document.createElement('script') 13 | script.setAttribute('src', `/views/${name}.umd.js?_t=${new Date().getTime()}`) 14 | document.head.appendChild(script) 15 | 16 | script.addEventListener('load', () => { 17 | document.head.removeChild(script) 18 | resolve(window['oui-com-' + name]) 19 | }) 20 | 21 | script.addEventListener('error', () => { 22 | document.head.removeChild(script), 23 | reject() 24 | }) 25 | }) 26 | } 27 | 28 | const loginView = import.meta.env.VITE_OUI_LOGIN_VIEW || 'login' 29 | const layoutView = import.meta.env.VITE_OUI_LAYOUT_VIEW || 'layout' 30 | const homeView = import.meta.env.VITE_OUI_HOME_VIEW || 'home' 31 | 32 | const routes = [] 33 | 34 | if (import.meta.env.MODE === 'development') { 35 | // eslint-disable-next-line no-undef 36 | const menus = oui.parseMenus(__MENUS__) 37 | addRoutesDev(routes, menus, loginView, layoutView, homeView) 38 | } else { 39 | routes.push({ 40 | path: '/login', 41 | name: 'login', 42 | component: () => loadView(loginView) 43 | }) 44 | 45 | routes.push({ 46 | path: '/', 47 | name: '/', 48 | component: () => loadView(layoutView), 49 | props: () => ({menus: oui.menus}), 50 | children: [ 51 | { 52 | path: '/home', 53 | name: 'home', 54 | component: () => loadView(homeView) 55 | }, 56 | { 57 | path: '/:pathMatch(.*)*', 58 | name: 'NotFound', 59 | component: () => import('../components/NotFound.vue') 60 | } 61 | ] 62 | }) 63 | } 64 | 65 | function addRoutes(menu) { 66 | if (menu.view && menu.path !== '/') 67 | router.addRoute('/', { 68 | name: Symbol(), 69 | path: menu.path, 70 | component: () => loadView(menu.view), 71 | meta: { menu: menu } 72 | }) 73 | else if (menu.children) 74 | menu.children.forEach(m => addRoutes(m)) 75 | } 76 | 77 | const router = createRouter({ 78 | history: createWebHashHistory(), 79 | routes 80 | }) 81 | 82 | router.beforeEach(async to => { 83 | await oui.waitUntil(() => oui.inited) 84 | 85 | if (to.path === '/login') { 86 | oui.logout() 87 | return 88 | } 89 | 90 | if (!(await oui.isAlived())) 91 | return '/login' 92 | 93 | if (import.meta.env.MODE === 'development') 94 | return 95 | 96 | if (!oui.menus) { 97 | router.getRoutes().forEach(r => { 98 | const name = r.name 99 | if (typeof(name) === 'string') 100 | return 101 | router.removeRoute(name) 102 | }) 103 | const menus = await oui.loadMenus() 104 | menus.forEach(m => addRoutes(m)) 105 | return to.fullPath 106 | } 107 | }) 108 | 109 | router.afterEach(to => { 110 | if (to.path === '/') 111 | router.push('/home') 112 | }) 113 | 114 | export default router 115 | -------------------------------------------------------------------------------- /applications/oui-app-layout/htdoc/openwrt-logo-black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 14 | 17 | 20 | 21 | 24 | 27 | 28 | 29 | 32 | 35 | 38 | 41 | 45 | 46 | 49 | 53 | 54 | 57 | 61 | 62 | 65 | 69 | 70 | 73 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /applications/oui-app-layout/htdoc/openwrt-logo-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 14 | 17 | 20 | 21 | 24 | 27 | 28 | 31 | 34 | 35 | 36 | 39 | 42 | 45 | 48 | 52 | 53 | 56 | 60 | 61 | 64 | 68 | 69 | 72 | 76 | 77 | 80 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /applications/oui-app-backup/htdoc/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 97 | 98 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /docs/src/zh/guide/vue-api.md: -------------------------------------------------------------------------------- 1 | # Vue API 2 | 3 | Oui 框架在 Vue 中注册了一些实例对象,方便各个页面调用。 4 | 文档中使用 `vm` (ViewModel 的缩写) 这个变量名表示 Vue 实例。 5 | 6 | ## vm.$oui 7 | 8 | ### state: 全局状态 9 | 10 | 一个响应式对象,包括如下字段 11 | 12 | | 名称 | 类型 | 描述 | 13 | | ---------- | --------| ------------- | 14 | | locale | String | 当前语言 | 15 | | theme | String | 当前主题 | 16 | | hostname | String |当前系统的主机名 | 17 | 18 | ```vue 19 |
{{ $oui.state.locale }}
20 |
{{ $oui.state.theme }}
21 |
{{ $oui.state.hostname }}
22 | ``` 23 | 24 | ### call: 调用后端接口 25 | 26 | vm.$oui.call(mod, func, [param]) 27 | 28 | 29 | 30 | 31 | ```js 32 | this.$oui.call('system', 'get_cpu_time').then(({ times }) => { 33 | ... 34 | }) 35 | ``` 36 | 37 | 38 | 39 | 40 | 41 | ```lua 42 | -- /usr/share/oui/rpc/system.lua 43 | 44 | local fs = require 'oui.fs' 45 | 46 | local M = {} 47 | 48 | function M.get_cpu_time() 49 | local result = {} 50 | 51 | for line in io.lines('/proc/stat') do 52 | local cpu = line:match('^(cpu%d?)') 53 | if cpu then 54 | local times = {} 55 | for field in line:gmatch('%S+') do 56 | if not field:match('cpu') then 57 | times[#times + 1] = tonumber(field) 58 | end 59 | end 60 | result[cpu] = times 61 | end 62 | end 63 | 64 | return { times = result } 65 | end 66 | 67 | return M 68 | ``` 69 | 70 | 71 | 72 | 73 | ### ubus: 对 `call` 的封装 74 | 75 | ```js 76 | this.$oui.ubus('system', 'validate_firmware_image', 77 | { 78 | path: '/tmp/firmware.bin' 79 | } 80 | ).then(({ valid }) => { 81 | }) 82 | ``` 83 | 等价于 84 | ```js 85 | this.$oui.call('ubus', 'call', { 86 | object: 'system', 87 | method: 'validate_firmware_image', 88 | { path: '/tmp/firmware.bin' } 89 | }).then(r => { 90 | }) 91 | ``` 92 | 93 | ### login:登录 94 | 95 | ```js 96 | this.$oui.login('admin', '123456').then(() => { 97 | }) 98 | ``` 99 | 100 | ### logout: 退出登录 101 | 102 | ```js 103 | this.$oui.logout().then(() => { 104 | }) 105 | ``` 106 | 107 | ### setLocale: 切换语言 108 | 109 | ```js 110 | this.$oui.setLocale('en') 111 | ``` 112 | 113 | ### setTheme: 切换主题 114 | 115 | ```js 116 | this.$oui.setTheme('dark') 117 | ``` 118 | 119 | ### setHostname: 设置系统主机名 120 | 121 | ```js 122 | this.$oui.setHostname('OpenWrt') 123 | ``` 124 | 125 | :::tip 126 | 你需要通过调用该函数来设置主机名,这样 `$oui.state.hostname` 才能得到更新。 127 | ::: 128 | 129 | ### reloadConfig: 重载配置 130 | 131 | 对下面的 ubus 操作的封装 132 | ```sh 133 | ubus call service event '{"type":"config.change", "data": {"package": "system"}}' 134 | ``` 135 | 136 | ```js 137 | this.$oui.reloadConfig('system') 138 | ``` 139 | 140 | ### reconnect: 等待系统重启完成 141 | 142 | 当执行重启操作时,该方法比较有用。 143 | 144 | ```js 145 | this.$oui.reconnect().then(() => { 146 | this.$router.push('/login') 147 | }) 148 | ``` 149 | 150 | ## $timer 151 | 152 | 你以前可能是这样写的: 153 | 154 | ```vue 155 | 178 | ``` 179 | 180 | 使用 `vm.$timer` 后,是这样的: 181 | 182 | ```vue 183 | 195 | ``` 196 | 197 | `vm.$timer.create` 接受 3 个参数: 198 | 199 | * name: 定时器名称(不能重复) 200 | * callback: 回调方法 201 | * option: 选项 202 | 203 | 其中 `option` 包括如下字段: 204 | 205 | | 名称 | 类型 | 描述 | 206 | | ---------- | --------| ------------- | 207 | | time | Number | 超时时间或者间隔时间(默认值为 1000)| 208 | | autostart | Boolean | 是否创建后自动启动(默认为 true) | 209 | | immediate | Boolean | 创建后是否立即执行一次回调函数 | 210 | | repeat | Boolean | 是否重复 | 211 | 212 | `vm.$timer.start`:启动定时器(如果你设置 autostart 为 false,你需要调用该函数) 213 | 214 | `vm.$timer.stop`:停止定时器(用户无需调用该函数,除非有特别需要) 215 | 216 | ```js 217 | this.$timer.start('test') 218 | this.$timer.stop('test') 219 | ``` 220 | 221 | ## $md5 222 | 223 | ```js 224 | const md5 = this.$md5('123') 225 | ``` 226 | -------------------------------------------------------------------------------- /applications/oui-app-user/htdoc/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /docs/src/zh/guide/page.md: -------------------------------------------------------------------------------- 1 | # 页面 2 | 3 | 通常,一个页面对应一个 oui-app-xx 4 | 5 | 一个基本的页面的目录结构是这样的 6 | ``` 7 | oui-app-demo/ 8 | ├── files 9 | │   ├── menu.json 10 | │   └── rpc 11 | │   └── demo.lua 12 | ├── htdoc 13 | │   ├── index.vue 14 | │   ├── locale.json 15 | │   ├── package.json 16 | │   ├── package-lock.json 17 | │   └── vite.config.js 18 | └── Makefile 19 | 20 | 3 directories, 8 files 21 | ``` 22 | 23 | ::: tip 24 | 如需创建新的页面,直接复制 oui-app-demo 目录,然后重命名即可 25 | ::: 26 | 27 | ## Makefile 配置 28 | 29 | ```makefile{9,10} 30 | # 31 | # Copyright (C) 2022 Jianhui Zhao 32 | # 33 | # This is free software, licensed under the MIT. 34 | # 35 | 36 | include $(TOPDIR)/rules.mk 37 | 38 | APP_TITLE:=Demo 39 | APP_NAME:=demo 40 | 41 | include ../../oui.mk 42 | 43 | # call BuildPackage - OpenWrt buildroot signature 44 | ``` 45 | 46 | * `APP_TITLE` - 对应 OpenWrt 软件包中的 TITLE 47 | * `APP_NAME` - 编译过程,菜单配置文件和打包的 js 文件会以 `APP_NAME` 命名 48 | 49 | :::warning 50 | `APP_NAME` 不能重复 51 | ::: 52 | 53 | ## 菜单配置 54 | 55 | 对于 `login`, `layout`, `home` 这三种页面,不需要菜单配置文件。 56 | 57 | ``` json 58 | { 59 | "/demo": { 60 | "title": "Oui Demo", 61 | "view": "demo", 62 | "index": 60, 63 | "locales": { 64 | "en": "Oui Demo", 65 | "zh-CN": "Oui 示范", 66 | "zh-TW": "Oui 示範" 67 | }, 68 | "svg":{"-xmlns":"http://www.w3.org/2000/svg","-xmlns:xlink":"http://www.w3.org/1999/xlink","-viewBox":"0 0 512 512","path":{"-d":"M407.72 208c-2.72 0-14.44.08-18.67.31l-57.77 1.52L198.06 48h-62.81l74.59 164.61l-97.31 1.44L68.25 160H16.14l20.61 94.18c.15.54.33 1.07.53 1.59a.26.26 0 0 1 0 .15a15.42 15.42 0 0 0-.53 1.58L15.86 352h51.78l45.45-55l96.77 2.17L135.24 464h63l133-161.75l57.77 1.54c4.29.23 16 .31 18.66.31c24.35 0 44.27-3.34 59.21-9.94C492.22 283 496 265.46 496 256c0-30.06-33-48-88.28-48zm-71.29 87.9z","-fill":"currentColor"}} 69 | } 70 | } 71 | ``` 72 | 73 | * `view` - 和 Makefile 中的 `APP_NAME` 一致 74 | * `index` - 用于菜单排序 75 | * `locales` - 菜单标题翻译 76 | * `svg` - 菜单图标 77 | 78 | :::tip 79 | 如何配置菜单图标:到 [xicons](https://www.xicons.org/#/) 复制所需图标的 svg 代码,然后到 80 | [xml2json](https://www.w3cschool.cn/tools/index?name=xmljson) 这个网站上将 svg 的代码转换为 json 格式。 81 | ::: 82 | 83 | 菜单分为一级菜单和二级菜单。oui-ui-core 默认提供了一些常用的一级菜单 84 | ```json 85 | { 86 | "/status": { 87 | "title": "Status", 88 | "icon": "md-stats", 89 | "index": 10, 90 | "locales": { 91 | "en": "Status", 92 | "zh-CN": "状态", 93 | "zh-TW": "狀態" 94 | }, 95 | "svg":{"-xmlns":"http://www.w3.org/2000/svg","-xmlns:xlink":"http://www.w3.org/1999/xlink","-viewBox":"0 0 24 24","path":{"-d":"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z","-fill":"currentColor"}} 96 | }, 97 | "/system": { 98 | "title": "System", 99 | "icon": "md-settings", 100 | "index": 20, 101 | "locales": { 102 | "en": "System", 103 | "zh-CN": "系统", 104 | "zh-TW": "系統" 105 | }, 106 | "svg":{"-xmlns":"http://www.w3.org/2000/svg","-xmlns:xlink":"http://www.w3.org/1999/xlink","-viewBox":"0 0 24 24","g":{"-fill":"none","path":{"-d":"M4.946 5h14.108C20.678 5 22 6.304 22 7.92v8.16c0 1.616-1.322 2.92-2.946 2.92H4.946C3.322 19 2 17.696 2 16.08V7.92C2 6.304 3.322 5 4.946 5zm0 2A.933.933 0 0 0 4 7.92v8.16c0 .505.42.92.946.92h14.108a.933.933 0 0 0 .946-.92V7.92c0-.505-.42-.92-.946-.92H4.946z","-fill":"currentColor"}}} 107 | }, 108 | "/network": { 109 | "title": "Network", 110 | "icon": "md-git-network", 111 | "index": 30, 112 | "locales": { 113 | "en": "Network", 114 | "zh-CN": "网络", 115 | "zh-TW": "網絡" 116 | }, 117 | "svg":{"-xmlns":"http://www.w3.org/2000/svg","-xmlns:xlink":"http://www.w3.org/1999/xlink","-viewBox":"0 0 640 512","path":{"-d":"M640 264v-16c0-8.84-7.16-16-16-16H344v-40h72c17.67 0 32-14.33 32-32V32c0-17.67-14.33-32-32-32H224c-17.67 0-32 14.33-32 32v128c0 17.67 14.33 32 32 32h72v40H16c-8.84 0-16 7.16-16 16v16c0 8.84 7.16 16 16 16h104v40H64c-17.67 0-32 14.33-32 32v128c0 17.67 14.33 32 32 32h160c17.67 0 32-14.33 32-32V352c0-17.67-14.33-32-32-32h-56v-40h304v40h-56c-17.67 0-32 14.33-32 32v128c0 17.67 14.33 32 32 32h160c17.67 0 32-14.33 32-32V352c0-17.67-14.33-32-32-32h-56v-40h104c8.84 0 16-7.16 16-16zM256 128V64h128v64H256zm-64 320H96v-64h96v64zm352 0h-96v-64h96v64z","-fill":"currentColor"}} 118 | } 119 | } 120 | ``` 121 | 122 | ## 自定义 `login` `layout` `home` 页面 123 | 124 | 以自定义 `login` 页面为例 125 | 126 | * 首先创建一个 app,比如 `applications/oui-app-login-x`,然后修改其 Makefile: 127 | 128 | ```makefile{9,10} 129 | # 130 | # Copyright (C) 2022 Jianhui Zhao 131 | # 132 | # This is free software, licensed under the MIT. 133 | # 134 | 135 | include $(TOPDIR)/rules.mk 136 | 137 | APP_TITLE:=Login X 138 | APP_NAME:=login-x 139 | 140 | include ../../oui.mk 141 | 142 | # call BuildPackage - OpenWrt buildroot signature 143 | ``` 144 | 145 | * 配置 `oui-ui-core` 146 | 147 | ```sh 148 | Oui ---> 149 | (login-x) Customize the login view 150 | ``` 151 | 152 | * 开发/调试 153 | 154 | 创建文件: oui-ui-core/htdoc/.env.local 155 | 156 | ``` 157 | VITE_OUI_LOGIN_VIEW=login-x 158 | ``` 159 | -------------------------------------------------------------------------------- /docs/src/guide/vue-api.md: -------------------------------------------------------------------------------- 1 | # Vue API 2 | 3 | The Oui framework registers some instance objects in Vue for easy invocation by individual pages. 4 | The variable name 'vm' (short for ViewModel) is used in the documentation to denote Vue instances. 5 | 6 | ## vm.$oui 7 | 8 | ### state: global state 9 | 10 | A reactive object with the following fields 11 | 12 | | Name | Type | description | 13 | | ---------- | --------| ------------- | 14 | | locale | String | The current language | 15 | | theme | String | The current theme | 16 | | hostname | String | The current hostname of the system | 17 | 18 | ```vue 19 |
{{ $oui.state.locale }}
20 |
{{ $oui.state.theme }}
21 |
{{ $oui.state.hostname }}
22 | ``` 23 | 24 | ### call: Call the backend API 25 | 26 | vm.$oui.call(mod, func, [param]) 27 | 28 | 29 | 30 | 31 | ```js 32 | this.$oui.call('system', 'get_cpu_time').then(({ times }) => { 33 | ... 34 | }) 35 | ``` 36 | 37 | 38 | 39 | 40 | 41 | ```lua 42 | -- /usr/share/oui/rpc/system.lua 43 | 44 | local fs = require 'oui.fs' 45 | 46 | local M = {} 47 | 48 | function M.get_cpu_time() 49 | local result = {} 50 | 51 | for line in io.lines('/proc/stat') do 52 | local cpu = line:match('^(cpu%d?)') 53 | if cpu then 54 | local times = {} 55 | for field in line:gmatch('%S+') do 56 | if not field:match('cpu') then 57 | times[#times + 1] = tonumber(field) 58 | end 59 | end 60 | result[cpu] = times 61 | end 62 | end 63 | 64 | return { times = result } 65 | end 66 | 67 | return M 68 | ``` 69 | 70 | 71 | 72 | 73 | ### ubus: Encapsulation of `call` 74 | 75 | ```js 76 | this.$oui.ubus('system', 'validate_firmware_image', 77 | { 78 | path: '/tmp/firmware.bin' 79 | } 80 | ).then(({ valid }) => { 81 | }) 82 | ``` 83 | Equivalent to 84 | 85 | ```js 86 | this.$oui.call('ubus', 'call', { 87 | object: 'system', 88 | method: 'validate_firmware_image', 89 | { path: '/tmp/firmware.bin' } 90 | }).then(r => { 91 | }) 92 | ``` 93 | 94 | ### login: log in 95 | 96 | ```js 97 | this.$oui.login('admin', '123456').then(() => { 98 | }) 99 | ``` 100 | 101 | ### logout: log out 102 | 103 | ```js 104 | this.$oui.logout().then(() => { 105 | }) 106 | ``` 107 | 108 | ### setLocale: switch the language 109 | 110 | ```js 111 | this.$oui.setLocale('en') 112 | ``` 113 | 114 | ### setTheme: Switch the theme 115 | 116 | ```js 117 | this.$oui.setTheme('dark') 118 | ``` 119 | 120 | ### setHostname: Set the system's hostname 121 | 122 | ```js 123 | this.$oui.setHostname('OpenWrt') 124 | ``` 125 | 126 | :::tip 127 | You need to set the hostname by calling this function so that `$oui.state.hostname` can be updated. 128 | ::: 129 | 130 | ### reloadConfig: reload config 131 | 132 | Encapsulation of the following UBUS operations 133 | 134 | ```sh 135 | ubus call service event '{"type":"config.change", "data": {"package": "system"}}' 136 | ``` 137 | 138 | ```js 139 | this.$oui.reloadConfig('system') 140 | ``` 141 | 142 | ### reconnect: Wait until the system restarts finish 143 | 144 | This method is useful when performing a restart operation. 145 | 146 | ```js 147 | this.$oui.reconnect().then(() => { 148 | this.$router.push('/login') 149 | }) 150 | ``` 151 | 152 | ## $timer 153 | 154 | You might have written something like this before: 155 | 156 | ```vue 157 | 180 | ``` 181 | 182 | After using `vm.$timer`, it looks like this: 183 | 184 | ```vue 185 | 197 | ``` 198 | 199 | `vm.$timer.create` takes three arguments: 200 | 201 | * name: Timer name (cannot be repeated) 202 | * callback: The callback method 203 | * option: options 204 | 205 | `option` includes the following fields: 206 | 207 | | Name | Type | Description | 208 | | ---------- | --------| ------------- | 209 | | time | Number | Timeout or interval (default value: 1000) | 210 | | autostart | Boolean | Whether to automatically start after creation (default is true) | 211 | | immediate | Boolean | Whether to execute a callback function immediately after creation | 212 | | repeat | Boolean | Whether to repeat | 213 | 214 | `vm.$timer.start`: Start timer(If you set autostart as false, you need to call the function) 215 | 216 | `vm.$timer.stop`: Stop the timer (the user does not need to call this function unless otherwise required) 217 | 218 | ```js 219 | this.$timer.start('test') 220 | this.$timer.stop('test') 221 | ``` 222 | 223 | ## $md5 224 | 225 | ```js 226 | const md5 = this.$md5('123') 227 | ``` 228 | -------------------------------------------------------------------------------- /docs/src/guide/page.md: -------------------------------------------------------------------------------- 1 | # Page 2 | 3 | Typically, one page corresponds to an oui-app-xx 4 | 5 | The directory structure of a basic page looks like this 6 | 7 | ``` 8 | oui-app-demo/ 9 | ├── files 10 | │   ├── menu.json 11 | │   └── rpc 12 | │   └── demo.lua 13 | ├── htdoc 14 | │   ├── index.vue 15 | │   ├── locale.json 16 | │   ├── package.json 17 | │   ├── package-lock.json 18 | │   └── vite.config.js 19 | └── Makefile 20 | 21 | 3 directories, 8 files 22 | ``` 23 | 24 | ::: tip 25 | To create a new page, copy the `oui-app-demo` directory and rename it. 26 | ::: 27 | 28 | ## Makefile 29 | 30 | ```makefile{9,10} 31 | # 32 | # Copyright (C) 2022 Jianhui Zhao 33 | # 34 | # This is free software, licensed under the MIT. 35 | # 36 | 37 | include $(TOPDIR)/rules.mk 38 | 39 | APP_TITLE:=Demo 40 | APP_NAME:=demo 41 | 42 | include ../../oui.mk 43 | 44 | # call BuildPackage - OpenWrt buildroot signature 45 | ``` 46 | 47 | * `APP_TITLE` - Corresponds to the `TITLE` in the OpenWrt software package 48 | * `APP_NAME` - During compilation, menu configuration file and packaged JS file will be named `APP_NAME` 49 | 50 | :::warning 51 | `APP NAME` cannot be repeated 52 | ::: 53 | 54 | ## Menu Configuration 55 | 56 | For the `login`, `layout`, and `home` pages, no menu configuration file are required. 57 | 58 | ``` json 59 | { 60 | "/demo": { 61 | "title": "Oui Demo", 62 | "view": "demo", 63 | "index": 60, 64 | "locales": { 65 | "en": "Oui Demo", 66 | "zh-CN": "Oui 示范", 67 | "zh-TW": "Oui 示範" 68 | }, 69 | "svg":{"-xmlns":"http://www.w3.org/2000/svg","-xmlns:xlink":"http://www.w3.org/1999/xlink","-viewBox":"0 0 512 512","path":{"-d":"M407.72 208c-2.72 0-14.44.08-18.67.31l-57.77 1.52L198.06 48h-62.81l74.59 164.61l-97.31 1.44L68.25 160H16.14l20.61 94.18c.15.54.33 1.07.53 1.59a.26.26 0 0 1 0 .15a15.42 15.42 0 0 0-.53 1.58L15.86 352h51.78l45.45-55l96.77 2.17L135.24 464h63l133-161.75l57.77 1.54c4.29.23 16 .31 18.66.31c24.35 0 44.27-3.34 59.21-9.94C492.22 283 496 265.46 496 256c0-30.06-33-48-88.28-48zm-71.29 87.9z","-fill":"currentColor"}} 70 | } 71 | } 72 | ``` 73 | 74 | * `view` - Same as `APP NAME` in Makefile 75 | * `index` - For menu sorting 76 | * `locales` - Menu Title Translation 77 | * `svg` - The menu icon 78 | 79 | :::tip 80 | How to configure menu icon: Copy the SVG code for the icon you want from [xicons](https://www.xicons.org/#/), 81 | and then go to the [xml2json](https://jsonformatter.org/xml-to-json) site to convert the SVG code to JSON format. 82 | ::: 83 | 84 | The menu is divided into primary menu and secondary menu. Oui-ui-core provides some common primary menus by default 85 | 86 | ```json 87 | { 88 | "/status": { 89 | "title": "Status", 90 | "icon": "md-stats", 91 | "index": 10, 92 | "locales": { 93 | "en": "Status", 94 | "zh-CN": "状态", 95 | "zh-TW": "狀態" 96 | }, 97 | "svg":{"-xmlns":"http://www.w3.org/2000/svg","-xmlns:xlink":"http://www.w3.org/1999/xlink","-viewBox":"0 0 24 24","path":{"-d":"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z","-fill":"currentColor"}} 98 | }, 99 | "/system": { 100 | "title": "System", 101 | "icon": "md-settings", 102 | "index": 20, 103 | "locales": { 104 | "en": "System", 105 | "zh-CN": "系统", 106 | "zh-TW": "系統" 107 | }, 108 | "svg":{"-xmlns":"http://www.w3.org/2000/svg","-xmlns:xlink":"http://www.w3.org/1999/xlink","-viewBox":"0 0 24 24","g":{"-fill":"none","path":{"-d":"M4.946 5h14.108C20.678 5 22 6.304 22 7.92v8.16c0 1.616-1.322 2.92-2.946 2.92H4.946C3.322 19 2 17.696 2 16.08V7.92C2 6.304 3.322 5 4.946 5zm0 2A.933.933 0 0 0 4 7.92v8.16c0 .505.42.92.946.92h14.108a.933.933 0 0 0 .946-.92V7.92c0-.505-.42-.92-.946-.92H4.946z","-fill":"currentColor"}}} 109 | }, 110 | "/network": { 111 | "title": "Network", 112 | "icon": "md-git-network", 113 | "index": 30, 114 | "locales": { 115 | "en": "Network", 116 | "zh-CN": "网络", 117 | "zh-TW": "網絡" 118 | }, 119 | "svg":{"-xmlns":"http://www.w3.org/2000/svg","-xmlns:xlink":"http://www.w3.org/1999/xlink","-viewBox":"0 0 640 512","path":{"-d":"M640 264v-16c0-8.84-7.16-16-16-16H344v-40h72c17.67 0 32-14.33 32-32V32c0-17.67-14.33-32-32-32H224c-17.67 0-32 14.33-32 32v128c0 17.67 14.33 32 32 32h72v40H16c-8.84 0-16 7.16-16 16v16c0 8.84 7.16 16 16 16h104v40H64c-17.67 0-32 14.33-32 32v128c0 17.67 14.33 32 32 32h160c17.67 0 32-14.33 32-32V352c0-17.67-14.33-32-32-32h-56v-40h304v40h-56c-17.67 0-32 14.33-32 32v128c0 17.67 14.33 32 32 32h160c17.67 0 32-14.33 32-32V352c0-17.67-14.33-32-32-32h-56v-40h104c8.84 0 16-7.16 16-16zM256 128V64h128v64H256zm-64 320H96v-64h96v64zm352 0h-96v-64h96v64z","-fill":"currentColor"}} 120 | } 121 | } 122 | ``` 123 | 124 | ## Customize the `login` `layout` `home` page 125 | 126 | Take the custom `login` page as an example 127 | 128 | * First create an app, such as `applications/oui-app-login-x`, and then modify its Makefile 129 | 130 | ```makefile{9,10} 131 | # 132 | # Copyright (C) 2022 Jianhui Zhao 133 | # 134 | # This is free software, licensed under the MIT. 135 | # 136 | 137 | include $(TOPDIR)/rules.mk 138 | 139 | APP_TITLE:=Login X 140 | APP_NAME:=login-x 141 | 142 | include ../../oui.mk 143 | 144 | # call BuildPackage - OpenWrt buildroot signature 145 | ``` 146 | 147 | * Configure `oui-ui-core` 148 | 149 | ```sh 150 | Oui ---> 151 | (login-x) Customize the login view 152 | ``` 153 | 154 | * Development/Debugging 155 | 156 | Create a file: oui-ui-core/htdoc/.env.local 157 | 158 | ``` 159 | VITE_OUI_LOGIN_VIEW=login-x 160 | ``` 161 | -------------------------------------------------------------------------------- /oui-rpc-core/files/rpc.lua: -------------------------------------------------------------------------------- 1 | local hex = require 'eco.encoding.hex' 2 | local md5 = require 'eco.hash.md5' 3 | local time = require 'eco.time' 4 | local file = require 'eco.file' 5 | local log = require 'eco.log' 6 | local cjson = require 'cjson' 7 | local uci = require 'eco.uci' 8 | 9 | local concat = table.concat 10 | local random = math.random 11 | 12 | local M = { 13 | ERROR_CODE_NOT_FOUND = -1, 14 | ERROR_CODE_INVALID_ARGUMENT = -2, 15 | ERROR_CODE_UNAUTHORIZED = -3, 16 | ERROR_CODE_PERMISSION_DENIED = -4, 17 | ERROR_CODE_LOAD_SCRIPT = -5, 18 | ERROR_CODE_UNKNOWN = -6 19 | } 20 | 21 | local SESSION_TIMEOUT = 300 22 | local MAX_NONCE_CNT = 5 23 | 24 | local no_auth_funcs = {} 25 | local funcs = {} 26 | local acls = {} 27 | local nonces = {} 28 | local sessions = {} 29 | local lua_code_cache = true 30 | 31 | local function random_string(n) 32 | local t = { 33 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 34 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 35 | 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 36 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 37 | 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' 38 | } 39 | local s = {} 40 | 41 | for _ = 1, n do 42 | s[#s + 1] = t[random(#t)] 43 | end 44 | 45 | return concat(s) 46 | end 47 | 48 | function M.init() 49 | local c = uci.cursor() 50 | 51 | lua_code_cache = c:get('oui', 'global', 'lua_code_cache') ~= '0' 52 | 53 | c:foreach('oui', 'no-auth', function(s) 54 | no_auth_funcs[s.module] = {} 55 | 56 | for _, func in ipairs(s.func or {}) do 57 | no_auth_funcs[s.module][func] = true 58 | end 59 | end) 60 | end 61 | 62 | function M.create_nonce() 63 | local cnt = #nonces 64 | 65 | if cnt > MAX_NONCE_CNT then 66 | log.err('The number of nonce too more than ' .. MAX_NONCE_CNT) 67 | return nil 68 | end 69 | 70 | local nonce = random_string(32) 71 | 72 | -- expires in 2s 73 | nonces[nonce] = time.at(2.0, function() nonces[nonce] = nil end) 74 | 75 | return nonce 76 | end 77 | 78 | function M.create_session(username, acl, remote_addr) 79 | local sid = random_string(32) 80 | local session = { 81 | tmr = time.at(SESSION_TIMEOUT, function() sessions[sid] = nil end), 82 | remote_addr = remote_addr, 83 | username = username, 84 | acls = acls[acl], 85 | acl = acl 86 | } 87 | 88 | sessions[sid] = session 89 | 90 | return sid 91 | end 92 | 93 | function M.login(username, password) 94 | local c = uci.cursor() 95 | local valid = false 96 | local acl 97 | 98 | c:foreach('oui', 'user', function(s) 99 | if s.username == username then 100 | if not s.password then 101 | acl = s.acl 102 | valid = true 103 | return false 104 | end 105 | 106 | for nonce in pairs(nonces) do 107 | if hex.encode(md5.sum(table.concat({s.password, nonce}, ':'))) == password then 108 | acl = s.acl 109 | valid = true 110 | return false 111 | end 112 | end 113 | return false 114 | end 115 | end) 116 | 117 | if not valid then 118 | return nil 119 | end 120 | 121 | return acl 122 | end 123 | 124 | function M.get_session(sid) 125 | local s = sessions[sid] 126 | if s then s.tmr:set(SESSION_TIMEOUT) end 127 | return s 128 | end 129 | 130 | function M.delete_session(sid) 131 | sessions[sid] = nil 132 | end 133 | 134 | function M.get_acls() 135 | return acls 136 | end 137 | 138 | function M.load_acl() 139 | acls = {} 140 | 141 | for name in file.dir('/usr/share/oui/acl') do 142 | local acl = name:match('(.*).json') 143 | if acl then 144 | local data = file.readfile('/usr/share/oui/acl/' .. name) 145 | acls[acl] = cjson.decode(data) 146 | end 147 | end 148 | end 149 | 150 | local function is_local_session(session) 151 | return session.remote_addr == '127.0.0.1' or session.remote_addr == '::1' 152 | end 153 | 154 | local function need_auth(session, mod, func) 155 | if is_local_session(session) then 156 | return false 157 | end 158 | 159 | return not no_auth_funcs[mod] or not no_auth_funcs[mod][func] 160 | end 161 | 162 | function M.acl_match(session, content, class) 163 | if is_local_session(session) then 164 | return true 165 | end 166 | 167 | if not session.acls then 168 | return false 169 | end 170 | 171 | if not session.acls[class] then return false end 172 | 173 | local matchs = session.acls[class].matchs 174 | if not matchs then 175 | return false 176 | end 177 | 178 | for _, pattern in ipairs(matchs) do 179 | if content:match(pattern) then 180 | return not session.acls[class].reverse 181 | end 182 | end 183 | 184 | return session.acls[class].reverse 185 | end 186 | 187 | function M.call(mod, func, args, session) 188 | if not lua_code_cache or not funcs[mod] then 189 | local script = '/usr/share/oui/rpc/' .. mod .. '.lua' 190 | 191 | if not file.access(script) then 192 | log.err('module "' .. mod .. '" not found') 193 | return nil, M.ERROR_CODE_NOT_FOUND 194 | end 195 | 196 | local ok, tb = pcall(dofile, script) 197 | if not ok then 198 | log.err('load module "' .. mod .. '":', tb) 199 | return nil, M.ERROR_CODE_LOAD_SCRIPT 200 | end 201 | 202 | if type(tb) == 'table' then 203 | funcs[mod] = tb 204 | end 205 | end 206 | 207 | if not funcs[mod] or not funcs[mod][func] then 208 | log.err('module "' .. mod .. '.' .. func .. '" not found') 209 | return nil, M.ERROR_CODE_NOT_FOUND 210 | end 211 | 212 | if need_auth(session, mod, func) then 213 | if not session.username then 214 | return nil, M.ERROR_CODE_UNAUTHORIZED 215 | end 216 | 217 | if not M.acl_match(session, mod .. '.' .. func, 'rpc') then 218 | return nil, M.ERROR_CODE_PERMISSION_DENIED 219 | end 220 | end 221 | 222 | return funcs[mod][func](args, session) 223 | end 224 | 225 | return M 226 | -------------------------------------------------------------------------------- /applications/oui-app-layout/htdoc/index.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 176 | 177 | 228 | 229 | 230 | -------------------------------------------------------------------------------- /oui-ui-core/htdoc/src/oui/index.js: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: MIT */ 2 | /* 3 | * Author: Jianhui Zhao 4 | */ 5 | 6 | import { computed, reactive } from 'vue' 7 | import { useDark } from '@vueuse/core' 8 | import * as Vue from 'vue' 9 | import axios from 'axios' 10 | import md5 from 'js-md5' 11 | import i18n from '../i18n' 12 | 13 | const isDark = useDark() 14 | 15 | function mergeLocaleMessage(key, locales) { 16 | for (const locale in locales) { 17 | const messages = i18n.global.getLocaleMessage(locale) 18 | if (!messages.menus) 19 | messages.menus = {} 20 | messages.menus[key] = locales[locale] 21 | } 22 | } 23 | 24 | function getSID() { 25 | return sessionStorage.getItem('__oui__sid__') || '' 26 | } 27 | 28 | class Oui { 29 | constructor() { 30 | window.Vue = Vue 31 | this.menus = null 32 | this.inited = false 33 | this.aliveTimer = null 34 | this.state = reactive({ 35 | sid: '', 36 | locale: '', 37 | hostname: '' 38 | }) 39 | 40 | this.state.isDark = computed({ 41 | get() { 42 | return isDark.value 43 | }, 44 | set: dark => { 45 | isDark.value = dark 46 | const theme = dark ? 'dark' : 'light' 47 | if (this.inited) 48 | this.call('uci', 'set', { config: 'oui', section: 'global', values: { theme }}) 49 | } 50 | }) 51 | 52 | const p = [ 53 | this.call('ui', 'get_locale'), 54 | this.call('ui', 'get_theme') 55 | ] 56 | 57 | const sid = getSID() 58 | if (sid) 59 | p.push(this.rpc('alive', { sid })) 60 | 61 | Promise.all(p).then(results => { 62 | let locale = results[0].locale 63 | if (!locale) 64 | locale = 'auto' 65 | 66 | this.state.locale = locale 67 | 68 | if (locale === 'auto') 69 | i18n.global.locale = navigator.language 70 | else 71 | i18n.global.locale = locale 72 | 73 | this.state.isDark = results[1].theme === 'dark' 74 | 75 | if (sid) { 76 | const alive = results[2].alive 77 | if (alive) 78 | this.initWithAlived(sid) 79 | } 80 | 81 | this.inited = true 82 | }) 83 | } 84 | 85 | waitUntil(conditionFn) { 86 | if (conditionFn()) 87 | return 88 | 89 | return new Promise((resolve) => { 90 | const intervalId = setInterval(() => { 91 | if (conditionFn()) { 92 | clearInterval(intervalId) 93 | resolve() 94 | } 95 | }, 10) 96 | }) 97 | } 98 | 99 | initWithAlived(sid) { 100 | this.state.sid = sid 101 | 102 | this.ubus('system', 'board').then(({ hostname }) => this.state.hostname = hostname) 103 | 104 | this.aliveTimer = setInterval(() => { 105 | this.rpc('alive', { sid }) 106 | }, 5000) 107 | } 108 | 109 | async rpc(method, params) { 110 | return (await axios.post('/oui-rpc', { method, params })).data 111 | } 112 | 113 | async call(mod, func, params = {}) { 114 | return (await this.rpc('call', [getSID(), mod, func, params])).result 115 | } 116 | 117 | ubus(obj, method, params) { 118 | return this.call('ubus', 'call', {object: obj, method, params}) 119 | } 120 | 121 | reloadConfig(config) { 122 | return this.ubus('service', 'event', { type: 'config.change', data: { package: config }}) 123 | } 124 | 125 | async login(username, password) { 126 | const { nonce } = await this.rpc('challenge', { username }) 127 | const hash1 = md5(`${username}:${password}`) 128 | const hash2 = md5(`${hash1}:${nonce}`) 129 | const { sid } = await this.rpc('login', { username, password: hash2 }) 130 | 131 | sessionStorage.setItem('__oui__sid__', sid) 132 | 133 | this.initWithAlived(sid) 134 | } 135 | 136 | logout() { 137 | this.menus = null 138 | const sid = getSID() 139 | if (!sid) 140 | return 141 | 142 | if (this.aliveTimer) { 143 | clearInterval(this.aliveTimer) 144 | this.aliveTimer = null 145 | } 146 | 147 | sessionStorage.removeItem('__oui__sid__') 148 | 149 | return this.rpc('logout', { sid }) 150 | } 151 | 152 | async isAlived() { 153 | const sid = getSID() 154 | if (!sid) 155 | return false 156 | return (await this.rpc('alive', { sid })).alive 157 | } 158 | 159 | parseMenus(raw) { 160 | const menus = {} 161 | 162 | for (const path in raw) { 163 | const paths = path.split('/') 164 | if (paths.length === 2) 165 | menus[path] = raw[path] 166 | } 167 | 168 | for (const path in raw) { 169 | const paths = path.split('/') 170 | if (paths.length === 3) { 171 | const parent = menus['/' + paths[1]] 172 | if (!parent || parent.view) 173 | continue 174 | 175 | if (!parent.children) 176 | parent.children = {} 177 | 178 | parent.children[path] = raw[path] 179 | parent.children[path].parent = parent 180 | } 181 | } 182 | 183 | const menusArray = [] 184 | 185 | for (const path in menus) { 186 | const m = menus[path] 187 | if (m.view || m.children) { 188 | menusArray.push({ 189 | path: path, 190 | ...m 191 | }) 192 | 193 | mergeLocaleMessage(m.title, m.locales) 194 | } 195 | } 196 | 197 | menusArray.sort((a, b) => (a.index ?? 0) - (b.index ?? 0)) 198 | 199 | menusArray.forEach(m => { 200 | if (!m.children) 201 | return 202 | 203 | const children = [] 204 | 205 | for (const path in m.children) { 206 | const c = m.children[path] 207 | children.push({ 208 | path: path, 209 | ...c 210 | }) 211 | 212 | mergeLocaleMessage(c.title, c.locales) 213 | } 214 | 215 | children.sort((a, b) => (a.index ?? 0) - (b.index ?? 0)) 216 | 217 | m.children = children 218 | }) 219 | 220 | return menusArray 221 | } 222 | 223 | async loadMenus() { 224 | if (this.menus) 225 | return this.menus 226 | const { menus } = await this.call('ui', 'get_menus') 227 | this.menus = this.parseMenus(menus) 228 | return this.menus 229 | } 230 | 231 | async setLocale(locale) { 232 | if (locale !== 'auto' && !i18n.global.availableLocales.includes(locale)) 233 | return 234 | 235 | await this.call('uci', 'set', { config: 'oui', section: 'global', values: { locale }}) 236 | 237 | this.state.locale = locale 238 | 239 | if (locale === 'auto') { 240 | let language = navigator.language 241 | if (language === 'en-US') 242 | language = 'en' 243 | i18n.global.locale = language 244 | } else { 245 | i18n.global.locale = locale 246 | } 247 | } 248 | 249 | async setHostname(hostname) { 250 | await this.call('uci', 'set', { config: 'system', section: '@system[0]', values: { hostname }}) 251 | await this.reloadConfig('system') 252 | this.state.hostname = hostname 253 | } 254 | 255 | reconnect(delay) { 256 | return new Promise(resolve => { 257 | let interval 258 | 259 | const img = document.createElement('img') 260 | 261 | img.addEventListener('load', () => { 262 | window.clearInterval(interval) 263 | img.remove() 264 | resolve() 265 | }) 266 | 267 | window.setTimeout(() => { 268 | interval = window.setInterval(() => { 269 | img.src = '/favicon.ico?r=' + Math.random() 270 | }, 1000) 271 | }, delay || 5000) 272 | }) 273 | } 274 | 275 | install(app) { 276 | app.config.globalProperties.$oui = this 277 | app.config.globalProperties.$md5 = md5 278 | app.provide('$oui', this) 279 | } 280 | } 281 | 282 | export default new Oui() 283 | -------------------------------------------------------------------------------- /applications/oui-app-home/htdoc/index.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 230 | 231 | 232 | -------------------------------------------------------------------------------- /oui-rpc-core/files/oui.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env eco 2 | 3 | -- SPDX-License-Identifier: MIT 4 | -- Author: Jianhui Zhao 5 | 6 | local hex = require 'eco.encoding.hex' 7 | local socket = require 'eco.socket' 8 | local md5 = require 'eco.hash.md5' 9 | local file = require 'eco.file' 10 | local ubus = require 'eco.ubus' 11 | local time = require 'eco.time' 12 | local sys = require 'eco.sys' 13 | local log = require 'eco.log' 14 | local cjson = require 'cjson' 15 | local uci = require 'eco.uci' 16 | 17 | local rpc = require 'oui.rpc' 18 | 19 | eco.panic_hook = function(err) 20 | os.remove('/var/run/oui-rpc.sock') 21 | log.err('panic:', err) 22 | end 23 | 24 | local function trace_hook(event) 25 | local info3 = debug.getinfo(3, 'Sl') 26 | 27 | if not info3 then 28 | return 29 | end 30 | 31 | local info2 = debug.getinfo(2, 'nf') 32 | 33 | local src = info3.short_src 34 | 35 | if info3.currentline ~= -1 then 36 | src = src .. ':' .. info3.currentline 37 | end 38 | 39 | log.info(event:upper(), info2.name or info2.func, src) 40 | end 41 | 42 | local function set_trace() 43 | for _, co in ipairs(eco.all()) do 44 | debug.sethook(co, trace_hook, 'rc') 45 | end 46 | end 47 | 48 | if arg[1] == '-trace' then 49 | set_trace() 50 | end 51 | 52 | local function init_signal() 53 | sys.signal(sys.SIGINT, function() 54 | log.info('\nGot SIGINT, now quit') 55 | eco.unloop() 56 | end) 57 | 58 | sys.signal(sys.SIGTERM, function() 59 | log.info('\nGot SIGTERM, now quit') 60 | eco.unloop() 61 | end) 62 | end 63 | 64 | local function parse_config() 65 | local c = uci.cursor() 66 | 67 | log.set_ident('oui') 68 | log.set_flags(log.FLAG_LF | log.FLAG_PATH) 69 | 70 | c:foreach('oui', 'global', function(s) 71 | log.set_level(log[s.log_level or 'INFO']) 72 | 73 | if s.log_path then 74 | log.set_path(s.log_path) 75 | end 76 | 77 | return false 78 | end) 79 | end 80 | 81 | local function get_lighttpd_username() 82 | for line in io.lines('/etc/lighttpd/lighttpd.conf') do 83 | local username = line:match('server.username[%s]+=%s+"(.+)"') 84 | if username then 85 | return username 86 | end 87 | end 88 | end 89 | 90 | local rpc_methods = {} 91 | 92 | rpc_methods['challenge'] = function(req, params) 93 | if type(params.username) ~= 'string' then 94 | log.err('call challenge: username is required') 95 | return 401 96 | end 97 | 98 | local c = uci.cursor() 99 | local found = false 100 | 101 | c:foreach('oui', 'user', function(s) 102 | if s.username == params.username then 103 | found = true 104 | return false 105 | end 106 | end) 107 | 108 | if not found then 109 | return 401 110 | end 111 | 112 | local nonce = rpc.create_nonce() 113 | if not nonce then 114 | return 401 115 | end 116 | 117 | return { nonce = nonce } 118 | end 119 | 120 | rpc_methods['login'] = function(req, params) 121 | local username = params.username 122 | local password = params.password 123 | 124 | if type(username) ~= 'string' then 125 | return 401 126 | end 127 | 128 | local acl = rpc.login(username, password) 129 | if not acl then 130 | return 401 131 | end 132 | 133 | local remote_addr = req.headers['REMOTE_ADDR'] 134 | 135 | local sid = rpc.create_session(username, acl, remote_addr) 136 | 137 | return { sid = sid } 138 | end 139 | 140 | rpc_methods['logout'] = function(req, params) 141 | local sid = params.sid 142 | 143 | if type(sid) == 'string' then 144 | rpc.delete_session(sid) 145 | end 146 | end 147 | 148 | rpc_methods['alive'] = function(req, params) 149 | local sid = params.sid 150 | local alive = false 151 | 152 | if type(sid) == 'string' and rpc.get_session(sid) then 153 | alive = true 154 | end 155 | 156 | return { alive = alive } 157 | end 158 | 159 | rpc_methods['call'] = function(req, params) 160 | local sid = params[1] 161 | local mod = params[2] 162 | local func = params[3] 163 | local args = params[4] or {} 164 | 165 | if type(sid) ~= 'string' or type(mod) ~= 'string' or type(func) ~= 'string' or type(args) ~= 'table' then 166 | return 400 167 | end 168 | 169 | local remote_addr = req.headers['REMOTE_ADDR'] 170 | local session = rpc.get_session(sid) or { remote_addr = remote_addr } 171 | 172 | local result, err = rpc.call(mod, func, args, session) 173 | if type(err) == 'number' then 174 | if err == rpc.ERROR_CODE_INVALID_ARGUMENT then 175 | return 400 176 | end 177 | 178 | if err == rpc.ERROR_CODE_NOT_FOUND then 179 | return 404 180 | end 181 | 182 | if err == rpc.ERROR_CODE_UNAUTHORIZED then 183 | return 401 184 | end 185 | 186 | if err == rpc.ERROR_CODE_PERMISSION_DENIED then 187 | return 403 188 | end 189 | 190 | return 500 191 | end 192 | 193 | if result then 194 | return { result = result } 195 | end 196 | end 197 | 198 | 199 | local function send_scgi_status(con, status) 200 | con:send('Status: ' .. status .. '\r\n\r\n') 201 | end 202 | 203 | local function read_scgi_body(con, req) 204 | local headers = req.headers 205 | 206 | if headers['REQUEST_METHOD'] ~= 'POST' then 207 | return nil, 400 208 | end 209 | 210 | local content_length = tonumber(headers['CONTENT_LENGTH']) 211 | if content_length then 212 | local data, err = con:readfull(content_length) 213 | if not data then 214 | log.err('read scgi:', err) 215 | return nil, 400 216 | end 217 | 218 | return data 219 | end 220 | 221 | return '' 222 | end 223 | 224 | local function handle_rpc(con, req) 225 | local body, err = read_scgi_body(con, req) 226 | if not body then 227 | return err 228 | end 229 | 230 | if body == '' then 231 | return 400 232 | end 233 | 234 | local ok, msg = pcall(cjson.decode, body) 235 | if not ok or type(msg) ~= 'table' or type(msg.method) ~= 'string' then 236 | return 400 237 | end 238 | 239 | if type(msg.params or {}) ~= 'table' then 240 | return 400 241 | end 242 | 243 | if not rpc_methods[msg.method] then 244 | log.err('Oui: Not supported rpc method: ', msg.method) 245 | return 404 246 | end 247 | 248 | local res = rpc_methods[msg.method](req, msg.params or {}) 249 | if type(res) == 'number' then 250 | return res 251 | end 252 | 253 | send_scgi_status(con, 200) 254 | 255 | if res then 256 | local data = cjson.encode(res) 257 | con:send(data:gsub('{}','[]')) 258 | end 259 | end 260 | 261 | local function handle_download(con, req) 262 | local query = req.query 263 | local path = query.path or '' 264 | local sid = query.sid or '' 265 | 266 | if path == '' then 267 | return 400 268 | end 269 | 270 | local s = rpc.get_session(sid) 271 | if not s then 272 | return 401 273 | end 274 | 275 | local f = io.open(path) 276 | if not f then 277 | return 404 278 | end 279 | 280 | send_scgi_status(con, 200) 281 | 282 | while true do 283 | local data = f:read(4096) 284 | if not data then 285 | break 286 | end 287 | 288 | if not con:send(data) then 289 | break 290 | end 291 | end 292 | 293 | f:close() 294 | end 295 | 296 | local function read_formdata(con, req, state) 297 | if not state.boundary then 298 | local content_type = req.headers['CONTENT_TYPE'] or '' 299 | local boundary = content_type:match('multipart/form%-data; *boundary=(----[%w%p]+)') 300 | if not boundary then 301 | return nil, 'bad request' 302 | end 303 | 304 | state.boundary = '--' .. boundary 305 | state.state = 'init' 306 | end 307 | 308 | if state.state == 'init' then 309 | local line, err = con:recv('l') 310 | if not line then 311 | return nil, err 312 | end 313 | 314 | if line ~= state.boundary .. '\r' then 315 | return nil, 'bad request' 316 | end 317 | 318 | state.boundary = '\r\n' .. state.boundary 319 | 320 | state.state = 'header' 321 | end 322 | 323 | if state.state == 'header' then 324 | local line, err = con:recv('l') 325 | if not line then 326 | return nil, err 327 | end 328 | 329 | if line == '\r' then 330 | state.state = 'body' 331 | else 332 | local name, value = line:match('([%w%p]+) *: *(.+)\r?$') 333 | if not name or not value then 334 | return nil, 'invalid http header' 335 | end 336 | 337 | return 'header', { name:lower(), value } 338 | end 339 | end 340 | 341 | if state.state == 'body' then 342 | local data, found = con:readuntil(state.boundary) 343 | if not data then 344 | return nil, found 345 | end 346 | 347 | if found then 348 | local x, err = con:peek(2) 349 | if not x then 350 | return nil, err 351 | end 352 | 353 | if x == '--' then 354 | state.state = 'end' 355 | else 356 | if x ~= '\r\n' then 357 | return nil, 'bad request' 358 | end 359 | 360 | con:recv(2) 361 | 362 | state.state = 'header' 363 | end 364 | end 365 | 366 | return 'body', { data, found } 367 | end 368 | 369 | return 'end' 370 | end 371 | 372 | local function handle_upload(con, req) 373 | local headers = req.headers 374 | 375 | if headers['REQUEST_METHOD'] ~= 'POST' then 376 | return 400 377 | end 378 | 379 | local part, sid, f, md5ctx 380 | local total = 0 381 | local state = {} 382 | 383 | while true do 384 | local typ, data = read_formdata(con, req, state) 385 | if typ == 'header' then 386 | if data[1] == 'content-disposition' then 387 | part = data[2]:match('name="([%w_-]+)"') 388 | end 389 | elseif typ == 'body' then 390 | if part == 'sid' then 391 | sid = data[1] 392 | if not rpc.get_session(sid) then 393 | return 401 394 | end 395 | elseif part == 'path' then 396 | f = io.open(data[1], 'w') 397 | if not f then 398 | return 403 399 | end 400 | 401 | md5ctx = md5.new() 402 | elseif part == 'file' then 403 | if not f then 404 | return 400 405 | end 406 | 407 | if not sid then 408 | return 401 409 | end 410 | 411 | f:write(data[1]) 412 | 413 | md5ctx:update(data[1]) 414 | total = total + #data[1] 415 | end 416 | elseif typ == 'end' then 417 | break 418 | else 419 | return 400 420 | end 421 | end 422 | 423 | if f then f:close() end 424 | 425 | if not f then 426 | return 400 427 | end 428 | 429 | send_scgi_status(con, 200) 430 | 431 | con:send(cjson.encode({ size = total, md5 = hex.encode(md5ctx:final()) })) 432 | end 433 | 434 | local handlers = { 435 | ['/oui-rpc'] = handle_rpc, 436 | ['/oui-download'] = handle_download, 437 | ['/oui-upload'] = handle_upload 438 | } 439 | 440 | local function url_unescape(s) 441 | return string.gsub(s, "%%(%x%x)", function(hex) 442 | return string.char(tonumber(hex, 16)) 443 | end) 444 | end 445 | 446 | local function handle_scgi(c) 447 | local con = c 448 | local data, err = con:recvuntil(':', 3) 449 | if not data then 450 | log.err('read scgi:', err) 451 | return send_scgi_status(con, 400) 452 | end 453 | 454 | local length = tonumber(data) 455 | if not length then 456 | return send_scgi_status(con, 400) 457 | end 458 | 459 | data, err = con:readfull(length) 460 | if not data then 461 | log.err('read scgi:', err) 462 | return send_scgi_status(con, 400) 463 | end 464 | 465 | local headers = {} 466 | 467 | repeat 468 | local pos = data:find('%z') 469 | if not pos or pos < 2 then 470 | return send_scgi_status(con, 400) 471 | end 472 | 473 | local name = data:sub(1, pos - 1) 474 | 475 | data = data:sub(pos + 1) 476 | 477 | pos = data:find('%z') 478 | if not pos then 479 | return send_scgi_status(con, 400) 480 | end 481 | 482 | headers[name] = data:sub(1, pos - 1) 483 | data = data:sub(pos + 1) 484 | until #data == 0 485 | 486 | data, err = con:recv(1) 487 | if not data then 488 | log.err('read scgi:', err) 489 | return send_scgi_status(con, 400) 490 | end 491 | 492 | if data ~= ',' then 493 | return send_scgi_status(con, 400) 494 | end 495 | 496 | if headers['SCGI'] ~= '1' then 497 | return send_scgi_status(con, 400) 498 | end 499 | 500 | local req = { headers = headers } 501 | 502 | local query_string = headers['QUERY_STRING'] 503 | local query = {} 504 | 505 | for q in query_string:gmatch('[^&]+') do 506 | local name, value = q:match('(.+)=(.+)') 507 | if name then 508 | name = url_unescape(name) 509 | query[name] = url_unescape(value) 510 | end 511 | end 512 | 513 | req.query = query 514 | 515 | local script_name = headers['SCRIPT_NAME'] 516 | local handler = handlers[script_name] 517 | if not handler then 518 | return send_scgi_status(con, 404) 519 | end 520 | 521 | err = handler(con, req) 522 | 523 | if type(err) == 'number' then 524 | send_scgi_status(con, err) 525 | end 526 | end 527 | 528 | local function run_scgi_server() 529 | local path = '/var/run/oui.sock' 530 | local s, err = socket.listen_unix(path) 531 | if not s then 532 | error(err) 533 | end 534 | 535 | local username = get_lighttpd_username() 536 | if username then 537 | local pw = sys.getpwnam(username) 538 | if pw then 539 | file.chown(path, pw.uid) 540 | end 541 | end 542 | 543 | while true do 544 | local c, peer = s:accept() 545 | if not c then 546 | log.err('accept:', peer) 547 | break 548 | end 549 | 550 | eco.run(handle_scgi, c) 551 | end 552 | end 553 | 554 | local function ubus_init() 555 | local con, err = ubus.connect() 556 | if not con then 557 | error(err) 558 | end 559 | 560 | con:add('oui', { 561 | get_session = { 562 | function(req, msg) 563 | local sid = msg.sid 564 | if type(sid) ~= 'string' then 565 | return ubus.STATUS_INVALID_ARGUMENT 566 | end 567 | 568 | local s = rpc.get_session(sid) 569 | if not s then 570 | return ubus.STATUS_NOT_FOUND 571 | end 572 | 573 | con:reply(req, { 574 | remote_addr = s.remote_addr, 575 | username = s.username, 576 | acls = s.acls, 577 | acl = s.acl 578 | }) 579 | end, { sid = ubus.STRING } 580 | } 581 | }) 582 | 583 | while true do 584 | time.sleep(1000) 585 | end 586 | end 587 | 588 | rpc.init() 589 | rpc.load_acl() 590 | 591 | parse_config() 592 | init_signal() 593 | 594 | eco.run(ubus_init) 595 | 596 | run_scgi_server() 597 | --------------------------------------------------------------------------------