Jacky преди 3 години
родител
ревизия
2287b1792c
променени са 56 файла, в които са добавени 391 реда и са изтрити 112 реда
  1. 1 1
      frontend/.env.development
  2. 0 1
      frontend/dist/index.html
  3. 0 0
      frontend/dist/js/chunk-1188de6e-legacy.f8e092e0.js
  4. 0 0
      frontend/dist/js/chunk-1188de6e.f8e092e0.js
  5. 0 0
      frontend/dist/js/chunk-15551262-legacy.4339ed58.js
  6. 0 0
      frontend/dist/js/chunk-15551262.4339ed58.js
  7. 0 0
      frontend/dist/js/chunk-17d07320-legacy.c9527253.js
  8. 0 0
      frontend/dist/js/chunk-17d07320.c9527253.js
  9. 0 0
      frontend/dist/js/chunk-1e5147a5-legacy.d3bc1e29.js
  10. 0 0
      frontend/dist/js/chunk-1e5147a5.d3bc1e29.js
  11. 0 0
      frontend/dist/js/chunk-228c37e8-legacy.f45481a4.js
  12. 0 0
      frontend/dist/js/chunk-228c37e8.f45481a4.js
  13. 0 0
      frontend/dist/js/chunk-23e3da44-legacy.bae4ce87.js
  14. 0 0
      frontend/dist/js/chunk-23e3da44.bae4ce87.js
  15. 0 0
      frontend/dist/js/chunk-2b94df79-legacy.8dc71139.js
  16. 0 0
      frontend/dist/js/chunk-2b94df79.8dc71139.js
  17. 0 0
      frontend/dist/js/chunk-2d0cf277-legacy.c3db42df.js
  18. 0 0
      frontend/dist/js/chunk-2d0cf277.c3db42df.js
  19. 0 0
      frontend/dist/js/chunk-532b3473-legacy.ccea521d.js
  20. 0 0
      frontend/dist/js/chunk-532b3473.ccea521d.js
  21. 0 0
      frontend/dist/js/chunk-56341220-legacy.34a9fceb.js
  22. 0 0
      frontend/dist/js/chunk-56341220.34a9fceb.js
  23. 0 0
      frontend/dist/js/chunk-742ad954-legacy.463a068e.js
  24. 0 0
      frontend/dist/js/chunk-742ad954.463a068e.js
  25. 1 1
      frontend/dist/js/chunk-96068e84-legacy.5cf4468c.js
  26. 1 1
      frontend/dist/js/chunk-96068e84.5cf4468c.js
  27. 0 0
      frontend/dist/js/chunk-d4ac245c-legacy.d0d1ca92.js
  28. 0 0
      frontend/dist/js/chunk-d4ac245c.d0d1ca92.js
  29. 0 0
      frontend/dist/js/chunk-e0ad5fdc-legacy.44fe5a47.js
  30. 0 0
      frontend/dist/js/chunk-e0ad5fdc.44fe5a47.js
  31. 0 0
      frontend/dist/js/chunk-e71b472c-legacy.e8ed19fc.js
  32. 0 0
      frontend/dist/js/chunk-e71b472c.e8ed19fc.js
  33. 0 0
      frontend/dist/js/chunk-vendors-legacy.04985da6.js
  34. 0 0
      frontend/dist/js/chunk-vendors.04985da6.js
  35. 0 0
      frontend/dist/js/index-legacy.008a35ba.js
  36. 0 0
      frontend/dist/js/index-legacy.a529f66e.js
  37. 0 0
      frontend/dist/js/index.204ef68a.js
  38. 0 0
      frontend/dist/js/index.c8496a25.js
  39. 1 1
      frontend/dist/version.json
  40. 3 1
      frontend/src/api/index.js
  41. 12 0
      frontend/src/api/install.js
  42. 5 3
      frontend/src/lib/utils/index.js
  43. 6 0
      frontend/src/router/index.js
  44. 127 0
      frontend/src/views/Install.vue
  45. 9 3
      frontend/src/views/Login.vue
  46. 1 1
      frontend/version.json
  47. 1 12
      frontend/vue.config.js
  48. 33 29
      server/api/api.go
  49. 74 0
      server/api/install.go
  50. 1 1
      server/app.ini
  51. 1 0
      server/go.mod
  52. 1 0
      server/go.sum
  53. 8 11
      server/main.go
  54. 4 5
      server/model/models.go
  55. 72 34
      server/router/routers.go
  56. 29 7
      server/settings/settings.go

+ 1 - 1
frontend/.env.development

@@ -1,2 +1,2 @@
 VUE_APP_API_ROOT = /
-VUE_APP_API_WSS_ROOT = /ws
+VUE_APP_API_WSS_ROOT = wss://nginx.jackyu.cn/api

Файловите разлики са ограничени, защото са твърде много
+ 0 - 1
frontend/dist/index.html


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/js/chunk-1188de6e-legacy.f8e092e0.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/js/chunk-1188de6e.f8e092e0.js


+ 0 - 0
frontend/dist/js/chunk-15551262-legacy.ab2ff742.js → frontend/dist/js/chunk-15551262-legacy.4339ed58.js


+ 0 - 0
frontend/dist/js/chunk-15551262.ab2ff742.js → frontend/dist/js/chunk-15551262.4339ed58.js


+ 0 - 0
frontend/dist/js/chunk-17d07320-legacy.f20a6ac5.js → frontend/dist/js/chunk-17d07320-legacy.c9527253.js


+ 0 - 0
frontend/dist/js/chunk-17d07320.f20a6ac5.js → frontend/dist/js/chunk-17d07320.c9527253.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/js/chunk-1e5147a5-legacy.d3bc1e29.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/js/chunk-1e5147a5.d3bc1e29.js


+ 0 - 0
frontend/dist/js/chunk-228c37e8-legacy.3f33dfe8.js → frontend/dist/js/chunk-228c37e8-legacy.f45481a4.js


+ 0 - 0
frontend/dist/js/chunk-228c37e8.3f33dfe8.js → frontend/dist/js/chunk-228c37e8.f45481a4.js


+ 0 - 0
frontend/dist/js/chunk-23e3da44-legacy.08117b5e.js → frontend/dist/js/chunk-23e3da44-legacy.bae4ce87.js


+ 0 - 0
frontend/dist/js/chunk-23e3da44.08117b5e.js → frontend/dist/js/chunk-23e3da44.bae4ce87.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/js/chunk-2b94df79-legacy.8dc71139.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/js/chunk-2b94df79.8dc71139.js


+ 0 - 0
frontend/dist/js/chunk-2d0cf277-legacy.99e2e71b.js → frontend/dist/js/chunk-2d0cf277-legacy.c3db42df.js


+ 0 - 0
frontend/dist/js/chunk-2d0cf277.99e2e71b.js → frontend/dist/js/chunk-2d0cf277.c3db42df.js


+ 0 - 0
frontend/dist/js/chunk-532b3473-legacy.eaa829bf.js → frontend/dist/js/chunk-532b3473-legacy.ccea521d.js


+ 0 - 0
frontend/dist/js/chunk-532b3473.eaa829bf.js → frontend/dist/js/chunk-532b3473.ccea521d.js


+ 0 - 0
frontend/dist/js/chunk-56341220-legacy.4ea9419d.js → frontend/dist/js/chunk-56341220-legacy.34a9fceb.js


+ 0 - 0
frontend/dist/js/chunk-56341220.4ea9419d.js → frontend/dist/js/chunk-56341220.34a9fceb.js


+ 0 - 0
frontend/dist/js/chunk-742ad954-legacy.a1467ad6.js → frontend/dist/js/chunk-742ad954-legacy.463a068e.js


+ 0 - 0
frontend/dist/js/chunk-742ad954.a1467ad6.js → frontend/dist/js/chunk-742ad954.463a068e.js


+ 1 - 1
frontend/dist/js/chunk-96068e84-legacy.dcec99db.js → frontend/dist/js/chunk-96068e84-legacy.5cf4468c.js

@@ -1 +1 @@
-(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-96068e84"],{"0d92":function(e,t,a){"use strict";a("e6a6")},"7c22":function(e,t,a){var n=a("24fb");t=n(!1),t.push([e.i,".egg[data-v-0db462a3]{padding:10px 0}.ant-btn[data-v-0db462a3]{margin:10px 10px 0 0}",""]),e.exports=t},e6a6:function(e,t,a){var n=a("7c22");n.__esModule&&(n=n.default),"string"===typeof n&&(n=[[e.i,n,""]]),n.locals&&(e.exports=n.locals);var r=a("499e").default;r("1fb1813a",n,!0,{sourceMap:!1,shadowMode:!1})},f820:function(e,t,a){"use strict";a.r(t);var n=function(){var e=this,t=e.$createElement,a=e._self._c||t;return a("a-card",[a("h2",[e._v("Nginx UI")]),a("p",[e._v("Yet another WebUI for Nginx")]),a("p",[e._v("Version: "+e._s(e.version)+" ("+e._s(e.build_id)+")")]),a("h3",[e._v("项目组")]),a("p",[e._v("Designer:"),a("a",{attrs:{href:"https://jackyu.cn/"}},[e._v("@0xJacky")])]),a("h3",[e._v("技术栈")]),a("p",[e._v("Go")]),a("p",[e._v("Gin")]),a("p",[e._v("Vue")]),a("p",[e._v("Websocket")]),a("h3",[e._v("开源协议")]),a("p",[e._v("GNU General Public License v2.0")]),a("p",[e._v("Copyright © 2020 - "+e._s(e.this_year)+" 0xJacky ")])])},r=[],s=a("1da1"),i=(a("96cf"),{name:"About",data:function(){var e,t=new Date;return{this_year:t.getFullYear(),version:"1.0.0",build_id:null!==(e="6")&&void 0!==e?e:"开发模式",api_root:"/api"}},methods:{changeUserPower:function(e){var t=this;return Object(s["a"])(regeneratorRuntime.mark((function a(){return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:return a.next=2,t.$store.dispatch("update_mock_user",{power:e});case 2:return a.next=4,t.$api.user.info();case 4:return a.next=6,t.$message.success("修改成功");case 6:case"end":return a.stop()}}),a)})))()}}}),o=i,c=(a("0d92"),a("2877")),u=Object(c["a"])(o,n,r,!1,null,"0db462a3",null);t["default"]=u.exports}}]);
+(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-96068e84"],{"0d92":function(e,t,a){"use strict";a("e6a6")},"7c22":function(e,t,a){var n=a("24fb");t=n(!1),t.push([e.i,".egg[data-v-0db462a3]{padding:10px 0}.ant-btn[data-v-0db462a3]{margin:10px 10px 0 0}",""]),e.exports=t},e6a6:function(e,t,a){var n=a("7c22");n.__esModule&&(n=n.default),"string"===typeof n&&(n=[[e.i,n,""]]),n.locals&&(e.exports=n.locals);var r=a("499e").default;r("1fb1813a",n,!0,{sourceMap:!1,shadowMode:!1})},f820:function(e,t,a){"use strict";a.r(t);var n=function(){var e=this,t=e.$createElement,a=e._self._c||t;return a("a-card",[a("h2",[e._v("Nginx UI")]),a("p",[e._v("Yet another WebUI for Nginx")]),a("p",[e._v("Version: "+e._s(e.version)+" ("+e._s(e.build_id)+")")]),a("h3",[e._v("项目组")]),a("p",[e._v("Designer:"),a("a",{attrs:{href:"https://jackyu.cn/"}},[e._v("@0xJacky")])]),a("h3",[e._v("技术栈")]),a("p",[e._v("Go")]),a("p",[e._v("Gin")]),a("p",[e._v("Vue")]),a("p",[e._v("Websocket")]),a("h3",[e._v("开源协议")]),a("p",[e._v("GNU General Public License v2.0")]),a("p",[e._v("Copyright © 2020 - "+e._s(e.this_year)+" 0xJacky ")])])},r=[],s=a("1da1"),i=(a("96cf"),{name:"About",data:function(){var e,t=new Date;return{this_year:t.getFullYear(),version:"1.0.0",build_id:null!==(e="8")&&void 0!==e?e:"开发模式",api_root:"/api"}},methods:{changeUserPower:function(e){var t=this;return Object(s["a"])(regeneratorRuntime.mark((function a(){return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:return a.next=2,t.$store.dispatch("update_mock_user",{power:e});case 2:return a.next=4,t.$api.user.info();case 4:return a.next=6,t.$message.success("修改成功");case 6:case"end":return a.stop()}}),a)})))()}}}),o=i,c=(a("0d92"),a("2877")),u=Object(c["a"])(o,n,r,!1,null,"0db462a3",null);t["default"]=u.exports}}]);

+ 1 - 1
frontend/dist/js/chunk-96068e84.dcec99db.js → frontend/dist/js/chunk-96068e84.5cf4468c.js

@@ -1 +1 @@
-(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-96068e84"],{"0d92":function(e,t,a){"use strict";a("e6a6")},"7c22":function(e,t,a){var n=a("24fb");t=n(!1),t.push([e.i,".egg[data-v-0db462a3]{padding:10px 0}.ant-btn[data-v-0db462a3]{margin:10px 10px 0 0}",""]),e.exports=t},e6a6:function(e,t,a){var n=a("7c22");n.__esModule&&(n=n.default),"string"===typeof n&&(n=[[e.i,n,""]]),n.locals&&(e.exports=n.locals);var r=a("499e").default;r("1fb1813a",n,!0,{sourceMap:!1,shadowMode:!1})},f820:function(e,t,a){"use strict";a.r(t);var n=function(){var e=this,t=e.$createElement,a=e._self._c||t;return a("a-card",[a("h2",[e._v("Nginx UI")]),a("p",[e._v("Yet another WebUI for Nginx")]),a("p",[e._v("Version: "+e._s(e.version)+" ("+e._s(e.build_id)+")")]),a("h3",[e._v("项目组")]),a("p",[e._v("Designer:"),a("a",{attrs:{href:"https://jackyu.cn/"}},[e._v("@0xJacky")])]),a("h3",[e._v("技术栈")]),a("p",[e._v("Go")]),a("p",[e._v("Gin")]),a("p",[e._v("Vue")]),a("p",[e._v("Websocket")]),a("h3",[e._v("开源协议")]),a("p",[e._v("GNU General Public License v2.0")]),a("p",[e._v("Copyright © 2020 - "+e._s(e.this_year)+" 0xJacky ")])])},r=[],s=a("1da1"),i=(a("96cf"),{name:"About",data:function(){var e,t=new Date;return{this_year:t.getFullYear(),version:"1.0.0",build_id:null!==(e="6")&&void 0!==e?e:"开发模式",api_root:"/api"}},methods:{changeUserPower:function(e){var t=this;return Object(s["a"])(regeneratorRuntime.mark((function a(){return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:return a.next=2,t.$store.dispatch("update_mock_user",{power:e});case 2:return a.next=4,t.$api.user.info();case 4:return a.next=6,t.$message.success("修改成功");case 6:case"end":return a.stop()}}),a)})))()}}}),o=i,c=(a("0d92"),a("2877")),u=Object(c["a"])(o,n,r,!1,null,"0db462a3",null);t["default"]=u.exports}}]);
+(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-96068e84"],{"0d92":function(e,t,a){"use strict";a("e6a6")},"7c22":function(e,t,a){var n=a("24fb");t=n(!1),t.push([e.i,".egg[data-v-0db462a3]{padding:10px 0}.ant-btn[data-v-0db462a3]{margin:10px 10px 0 0}",""]),e.exports=t},e6a6:function(e,t,a){var n=a("7c22");n.__esModule&&(n=n.default),"string"===typeof n&&(n=[[e.i,n,""]]),n.locals&&(e.exports=n.locals);var r=a("499e").default;r("1fb1813a",n,!0,{sourceMap:!1,shadowMode:!1})},f820:function(e,t,a){"use strict";a.r(t);var n=function(){var e=this,t=e.$createElement,a=e._self._c||t;return a("a-card",[a("h2",[e._v("Nginx UI")]),a("p",[e._v("Yet another WebUI for Nginx")]),a("p",[e._v("Version: "+e._s(e.version)+" ("+e._s(e.build_id)+")")]),a("h3",[e._v("项目组")]),a("p",[e._v("Designer:"),a("a",{attrs:{href:"https://jackyu.cn/"}},[e._v("@0xJacky")])]),a("h3",[e._v("技术栈")]),a("p",[e._v("Go")]),a("p",[e._v("Gin")]),a("p",[e._v("Vue")]),a("p",[e._v("Websocket")]),a("h3",[e._v("开源协议")]),a("p",[e._v("GNU General Public License v2.0")]),a("p",[e._v("Copyright © 2020 - "+e._s(e.this_year)+" 0xJacky ")])])},r=[],s=a("1da1"),i=(a("96cf"),{name:"About",data:function(){var e,t=new Date;return{this_year:t.getFullYear(),version:"1.0.0",build_id:null!==(e="8")&&void 0!==e?e:"开发模式",api_root:"/api"}},methods:{changeUserPower:function(e){var t=this;return Object(s["a"])(regeneratorRuntime.mark((function a(){return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:return a.next=2,t.$store.dispatch("update_mock_user",{power:e});case 2:return a.next=4,t.$api.user.info();case 4:return a.next=6,t.$message.success("修改成功");case 6:case"end":return a.stop()}}),a)})))()}}}),o=i,c=(a("0d92"),a("2877")),u=Object(c["a"])(o,n,r,!1,null,"0db462a3",null);t["default"]=u.exports}}]);

+ 0 - 0
frontend/dist/js/chunk-d4ac245c-legacy.36cefb3a.js → frontend/dist/js/chunk-d4ac245c-legacy.d0d1ca92.js


+ 0 - 0
frontend/dist/js/chunk-d4ac245c.36cefb3a.js → frontend/dist/js/chunk-d4ac245c.d0d1ca92.js


+ 0 - 0
frontend/dist/js/chunk-e0ad5fdc-legacy.02ec34ba.js → frontend/dist/js/chunk-e0ad5fdc-legacy.44fe5a47.js


+ 0 - 0
frontend/dist/js/chunk-e0ad5fdc.02ec34ba.js → frontend/dist/js/chunk-e0ad5fdc.44fe5a47.js


+ 0 - 0
frontend/dist/js/chunk-e71b472c-legacy.345ce2d9.js → frontend/dist/js/chunk-e71b472c-legacy.e8ed19fc.js


+ 0 - 0
frontend/dist/js/chunk-e71b472c.345ce2d9.js → frontend/dist/js/chunk-e71b472c.e8ed19fc.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/js/chunk-vendors-legacy.04985da6.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/js/chunk-vendors.04985da6.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/js/index-legacy.008a35ba.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/js/index-legacy.a529f66e.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/js/index.204ef68a.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
frontend/dist/js/index.c8496a25.js


+ 1 - 1
frontend/dist/version.json

@@ -1 +1 @@
-{"version":"1.0.0","build_id":2,"total_build":6}
+{"version":"1.0.0","build_id":4,"total_build":8}

+ 3 - 1
frontend/src/api/index.js

@@ -2,10 +2,12 @@ import domain from './domain'
 import config from './config'
 import auth from './auth'
 import user from './user'
+import install from './install'
 
 export default {
     domain,
     config,
     auth,
-    user
+    user,
+    install
 }

+ 12 - 0
frontend/src/api/install.js

@@ -0,0 +1,12 @@
+import http from "@/lib/http";
+
+const install = {
+    get_lock() {
+        return http.get('/install')
+    },
+    install_nginx_ui(data) {
+        return http.post('/install', data)
+    }
+}
+
+export default install

+ 5 - 3
frontend/src/lib/utils/index.js

@@ -32,9 +32,11 @@ export default {
         Vue.prototype.scrollPosition = scrollPosition
 
         Vue.prototype.getWebSocketRoot = () => {
-           const protocol = location.protocol === "https:" ? "wss://" : "ws://"
-
-            return protocol + location.host + process.env["VUE_APP_API_WSS_ROOT"]
+            const protocol = location.protocol === "https:" ? "wss://" : "ws://"
+            if (process.env.NODE_ENV === 'development' && process.env["VUE_APP_API_WSS_ROOT"]) {
+                return process.env["VUE_APP_API_WSS_ROOT"]
+            }
+            return protocol + location.host + '/' + process.env["VUE_APP_API_WSS_ROOT"]
         }
     }
 }

+ 6 - 0
frontend/src/router/index.js

@@ -80,6 +80,12 @@ export const routes = [
             },
         ]
     },
+    {
+        path: '/install',
+        name: '安装',
+        component: () => import('@/views/Install'),
+        meta: {noAuth: true}
+    },
     {
         path: '/login',
         name: '登录',

+ 127 - 0
frontend/src/views/Install.vue

@@ -0,0 +1,127 @@
+<template>
+    <div class="login-form">
+        <div class="project-title">
+            <h1>Nginx UI</h1>
+        </div>
+        <a-form
+            id="components-form-demo-normal-login"
+            :form="form"
+            class="login-form"
+            @submit="handleSubmit"
+        >
+            <a-form-item>
+                <a-input
+                    v-decorator="[
+          'email',
+          { rules: [{
+                type: 'email',
+                message: 'The input is not valid E-mail!',
+              },
+              {
+                required: true,
+                message: 'Please input your E-mail!',
+              },] },
+        ]"
+                    placeholder="Email"
+                >
+                    <a-icon slot="prefix" type="mail" style="color: rgba(0,0,0,.25)"/>
+                </a-input>
+            </a-form-item>
+            <a-form-item>
+                <a-input
+                    v-decorator="[
+          'username',
+          { rules: [{ required: true, message: 'Please input your username!' }] },
+        ]"
+                    placeholder="Username"
+                >
+                    <a-icon slot="prefix" type="user" style="color: rgba(0,0,0,.25)"/>
+                </a-input>
+            </a-form-item>
+            <a-form-item>
+                <a-input
+                    v-decorator="[
+          'password',
+          { rules: [{ required: true, message: 'Please input your Password!' }] },
+        ]"
+                    type="password"
+                    placeholder="Password"
+                >
+                    <a-icon slot="prefix" type="lock" style="color: rgba(0,0,0,.25)"/>
+                </a-input>
+            </a-form-item>
+            <a-form-item>
+                <a-button type="primary" :block="true" html-type="submit" :loading="loading">
+                    安装
+                </a-button>
+            </a-form-item>
+        </a-form>
+        <footer>
+            Copyright © 2020 - {{ thisYear }} 0xJacky
+        </footer>
+    </div>
+
+</template>
+
+<script>
+export default {
+    name: 'Login',
+    data() {
+        return {
+            form: {},
+            lock: true,
+            thisYear: new Date().getFullYear(),
+            loading: false
+        }
+    },
+    created() {
+        this.form = this.$form.createForm(this)
+    },
+    mounted() {
+        this.$api.install.get_lock().then(r => {
+            if (r.lock) {
+                this.$router.push('/login')
+            }
+        })
+    },
+    methods: {
+        handleSubmit: async function (e) {
+            e.preventDefault()
+            this.loading = true
+            await this.form.validateFields(async (err, values) => {
+                if (!err) {
+                    this.$api.install.install_nginx_ui(values).then(() => {
+                        this.$router.push('/login')
+                    })
+                }
+                this.loading = false
+            })
+        },
+    },
+};
+</script>
+<style lang="less">
+.project-title {
+    margin: 50px;
+
+    h1 {
+        font-size: 50px;
+        font-weight: 100;
+        text-align: center;
+    }
+}
+
+.login-form {
+    max-width: 500px;
+    margin: 0 auto;
+}
+
+.login-form-button {
+
+}
+
+footer {
+    padding: 30px;
+    text-align: center;
+}
+</style>

+ 9 - 3
frontend/src/views/Login.vue

@@ -56,10 +56,16 @@ export default {
         }
     },
     created() {
+        this.form = this.$form.createForm(this)
+    },
+    mounted() {
+        this.$api.install.get_lock().then(r=>{
+            if (!r.lock) {
+                this.$router.push('/install')
+            }
+        })
         if (this.$store.state.user.token) {
             this.$router.push('/')
-        } else {
-            this.form = this.$form.createForm(this)
         }
     },
     methods: {
@@ -80,8 +86,8 @@ export default {
                 if (!err) {
                     await this.login(values)
                 }
+                this.loading = false
             })
-            this.loading = false
         },
     },
 };

+ 1 - 1
frontend/version.json

@@ -1 +1 @@
-{"version":"1.0.0","build_id":2,"total_build":6}
+{"version":"1.0.0","build_id":4,"total_build":8}

+ 1 - 12
frontend/vue.config.js

@@ -19,18 +19,7 @@ module.exports = {
         },
     },
     devServer: {
-        proxy: {
-            '/ws': {
-                target: 'wss://nginx.jackyu.cn/api',
-                changeOrigin: true,
-                pathRewrite: {
-                    '^/ws': '',
-                },
-            },
-            '': {
-                target: 'https://nginx.jackyu.cn/api',
-            }
-        }
+        proxy: 'https://nginx.jackyu.cn/api'
     },
 
     productionSourceMap: false,

+ 33 - 29
server/api/api.go

@@ -1,46 +1,50 @@
 package api
 
 import (
-    "github.com/gin-gonic/gin"
-    "log"
-    "net/http"
+	"github.com/gin-gonic/gin"
+	"log"
+	"net/http"
+    "strings"
 
     ut "github.com/go-playground/universal-translator"
-    val "github.com/go-playground/validator/v10"
+	val "github.com/go-playground/validator/v10"
 )
 
 func ErrorHandler(c *gin.Context, err error) {
-    log.Println(err)
-    c.JSON(http.StatusInternalServerError, gin.H{
-        "message": err.Error(),
-    })
+	log.Println(err)
+	c.JSON(http.StatusInternalServerError, gin.H{
+		"message": err.Error(),
+	})
 }
 
 type ValidError struct {
-    Key     string
-    Message string
+	Key     string
+	Message string
 }
 
 type ValidErrors gin.H
 
 func BindAndValid(c *gin.Context, v interface{}) (bool, ValidErrors) {
-    errs := make(ValidErrors)
-    err := c.ShouldBind(v)
-    if err != nil {
-
-        v := c.Value("trans")
-        trans, _ := v.(ut.Translator)
-        verrs, ok := err.(val.ValidationErrors)
-        if !ok {
-            return false, errs
-        }
-
-        for key, value := range verrs.Translate(trans) {
-            errs[key] = value
-        }
-
-        return false, errs
-    }
-
-    return true, nil
+	errs := make(ValidErrors)
+	err := c.ShouldBind(v)
+	if err != nil {
+        log.Println(err)
+		v := c.Value("trans")
+		trans, _ := v.(ut.Translator)
+
+		verrs, ok := err.(val.ValidationErrors)
+		if !ok {
+			return false, errs
+		}
+
+		for key, value := range verrs.Translate(trans) {
+		    k := strings.Split(key, ".")
+		    sub := strings.ToLower(k[1])
+			errs[sub] = value
+		}
+
+		return false, errs
+	}
+
+	return true, nil
 }

+ 74 - 0
server/api/install.go

@@ -0,0 +1,74 @@
+package api
+
+import (
+    "github.com/0xJacky/Nginx-UI/server/model"
+    "github.com/0xJacky/Nginx-UI/server/settings"
+    "github.com/gin-gonic/gin"
+    "github.com/google/uuid"
+    "golang.org/x/crypto/bcrypt"
+    "net/http"
+    "os"
+    "path"
+)
+
+func installLockStatus() bool {
+    lockPath := path.Join(settings.DataDir, "app.ini")
+	_, err := os.Stat(lockPath)
+
+	return !os.IsNotExist(err)
+
+}
+
+func InstallLockCheck(c *gin.Context) {
+	c.JSON(http.StatusOK, gin.H{
+		"lock": installLockStatus(),
+	})
+}
+
+type InstallJson struct {
+	Email    string `json:"email" binding:"required,email"`
+	Username string `json:"username" binding:"required,max=255"`
+	Password string `json:"password" binding:"required,max=255"`
+}
+
+func InstallNginxUI(c *gin.Context) {
+    // 安装过就别访问了
+    if installLockStatus() {
+        c.JSON(http.StatusForbidden, gin.H{
+            "message": "installed",
+        })
+        return
+    }
+    var json InstallJson
+    ok, verrs := BindAndValid(c, &json)
+    if !ok {
+        c.JSON(http.StatusNotAcceptable, gin.H{
+            "errors": verrs,
+        })
+        return
+    }
+
+    serverSettings := settings.Conf.Section("server")
+    serverSettings.Key("JwtSecret").SetValue(uuid.New().String())
+    serverSettings.Key("Email").SetValue(json.Email)
+    err := settings.Save()
+    if err != nil {
+        ErrorHandler(c, err)
+        return
+    }
+
+    curd := model.NewCurd(&model.Auth{})
+    pwd, _ := bcrypt.GenerateFromPassword([]byte(json.Password), bcrypt.DefaultCost)
+    err = curd.Add(&model.Auth{
+        Name: json.Username,
+        Password: string(pwd),
+    })
+    if err != nil {
+        ErrorHandler(c, err)
+        return
+    }
+    c.JSON(http.StatusOK, gin.H{
+        "message": "ok",
+    })
+
+}

+ 1 - 1
server/app.ini

@@ -1,5 +1,5 @@
 [server]
-HttpPort = 9000
+HttpPort = 9100
 RunMode = debug
 JwtSecret = F7AFBC0E-227E-40AC-918D-60122D725398
 Email = me@jackyu.cn

+ 1 - 0
server/go.mod

@@ -9,6 +9,7 @@ require (
 	github.com/go-acme/lego/v4 v4.4.0
 	github.com/go-playground/universal-translator v0.17.0
 	github.com/go-playground/validator/v10 v10.4.1
+	github.com/google/uuid v1.1.1
 	github.com/gorilla/websocket v1.4.2
 	github.com/shirou/gopsutil/v3 v3.21.7
 	github.com/spf13/cast v1.3.1

+ 1 - 0
server/go.sum

@@ -173,6 +173,7 @@ github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hf
 github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=

+ 8 - 11
server/main.go

@@ -3,25 +3,22 @@ package main
 import (
     "flag"
     "github.com/0xJacky/Nginx-UI/server/model"
-	"github.com/0xJacky/Nginx-UI/server/router"
-	"github.com/0xJacky/Nginx-UI/server/settings"
-	"github.com/0xJacky/Nginx-UI/server/tool"
-	"log"
+    "github.com/0xJacky/Nginx-UI/server/router"
+    "github.com/0xJacky/Nginx-UI/server/settings"
+    "github.com/0xJacky/Nginx-UI/server/tool"
+    "log"
 )
 
 func main() {
-    var dbPath string
-    var confPath string
-    flag.StringVar(&confPath, "c", "app.ini", "Specify the conf path to load")
-    flag.StringVar(&dbPath, "d", "database.db", "Specify the database path to load")
+    var dataDir string
+    flag.StringVar(&dataDir, "d", ".", "Specify the data dir")
     flag.Parse()
 
-	settings.Init(confPath)
+    settings.Init(dataDir)
+    model.Init()
 
 	r := router.InitRouter()
 
-	model.Init(dbPath)
-
 	log.Printf("nginx config dir path: %s", tool.GetNginxConfPath(""))
 
 	err := r.Run(":" + settings.ServerSettings.HttpPort)

+ 4 - 5
server/model/models.go

@@ -1,10 +1,12 @@
 package model
 
 import (
+    "github.com/0xJacky/Nginx-UI/server/settings"
     "gorm.io/driver/sqlite"
     "gorm.io/gorm"
     "gorm.io/gorm/logger"
     "log"
+    "path"
     "time"
 )
 
@@ -17,19 +19,16 @@ type Model struct {
 	DeletedAt *time.Time `gorm:"index" json:"deleted_at"`
 }
 
-func Init(dbPath string) {
+func Init() {
+    dbPath := path.Join(settings.DataDir, "database.db")
 	var err error
-
 	db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{
         Logger:      logger.Default.LogMode(logger.Info),
         PrepareStmt: true,
     })
-	log.Println("database.db")
-
 	if err != nil {
 		log.Println(err)
 	}
-
 	// Migrate the schema
 	AutoMigrate(&ConfigBackup{})
 	AutoMigrate(&Auth{})

+ 72 - 34
server/router/routers.go

@@ -5,35 +5,67 @@ import (
     "github.com/0xJacky/Nginx-UI/server/api"
     "github.com/0xJacky/Nginx-UI/server/model"
     "github.com/gin-gonic/gin"
+    "github.com/gin-gonic/gin/binding"
+    "github.com/go-playground/locales/en"
+    "github.com/go-playground/locales/zh"
+    ut "github.com/go-playground/universal-translator"
+    "github.com/go-playground/validator/v10"
+    en_translations "github.com/go-playground/validator/v10/translations/en"
+    zh_translations "github.com/go-playground/validator/v10/translations/zh"
     "net/http"
 )
 
 func authRequired() gin.HandlerFunc {
-    return func(c *gin.Context) {
-        token := c.GetHeader("Authorization")
-        if token == "" {
-            tmp, _ := base64.StdEncoding.DecodeString(c.Query("token"))
-            token = string(tmp)
-            if token == "" {
-                c.JSON(http.StatusForbidden, gin.H{
-                    "message": "auth fail",
-                })
-                c.Abort()
-                return
-            }
-        }
-
-        n := model.CheckToken(token)
-
-        if n < 1 {
-            c.JSON(http.StatusForbidden, gin.H{
-                "message": "auth fail",
-            })
-            c.Abort()
-            return
-        }
-        c.Next()
-    }
+	return func(c *gin.Context) {
+		token := c.GetHeader("Authorization")
+		if token == "" {
+			tmp, _ := base64.StdEncoding.DecodeString(c.Query("token"))
+			token = string(tmp)
+			if token == "" {
+				c.JSON(http.StatusForbidden, gin.H{
+					"message": "auth fail",
+				})
+				c.Abort()
+				return
+			}
+		}
+
+		n := model.CheckToken(token)
+
+		if n < 1 {
+			c.JSON(http.StatusForbidden, gin.H{
+				"message": "auth fail",
+			})
+			c.Abort()
+			return
+		}
+		c.Next()
+	}
+}
+
+func Translations() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		uni := ut.New(en.New(), zh.New())
+		locale := c.GetHeader("locale")
+		trans, _ := uni.GetTranslator(locale)
+		v, ok := binding.Validator.Engine().(*validator.Validate)
+		if ok {
+			switch locale {
+			case "zh":
+				_ = zh_translations.RegisterDefaultTranslations(v, trans)
+				break
+			case "en":
+				_ = en_translations.RegisterDefaultTranslations(v, trans)
+				break
+			default:
+				_ = zh_translations.RegisterDefaultTranslations(v, trans)
+				break
+			}
+			c.Set("trans", trans)
+		}
+
+		c.Next()
+	}
 }
 
 func InitRouter() *gin.Engine {
@@ -41,23 +73,29 @@ func InitRouter() *gin.Engine {
 	r.Use(gin.Logger())
 
 	r.Use(gin.Recovery())
+	r.Use(Translations())
 
 	r.GET("/", func(c *gin.Context) {
 		c.JSON(http.StatusOK, gin.H{
 			"message": "Hello World",
 		})
 	})
-    r.GET("/analytic", api.Analytic)
+
+	r.GET("install", api.InstallLockCheck)
+	r.POST("install", api.InstallNginxUI)
+
 	r.POST("/login", api.Login)
-    r.DELETE("/logout", api.Logout)
+	r.DELETE("/logout", api.Logout)
 
 	g := r.Group("/", authRequired())
 	{
-	    g.GET("/users", api.GetUsers)
-	    g.GET("/user/:id", api.GetUser)
-	    g.POST("/user", api.AddUser)
-	    g.POST("/user/:id", api.EditUser)
-        g.DELETE("/user/:id", api.DeleteUser)
+		r.GET("/analytic", api.Analytic)
+
+		g.GET("/users", api.GetUsers)
+		g.GET("/user/:id", api.GetUser)
+		g.POST("/user", api.AddUser)
+		g.POST("/user/:id", api.EditUser)
+		g.DELETE("/user/:id", api.DeleteUser)
 
 		g.GET("domains", api.GetDomains)
 		g.GET("domain/:name", api.GetDomain)
@@ -74,10 +112,10 @@ func InitRouter() *gin.Engine {
 		g.GET("backups", api.GetFileBackupList)
 		g.GET("backup/:id", api.GetFileBackup)
 
-        g.GET("template/:name", api.GetTemplate)
+		g.GET("template/:name", api.GetTemplate)
 
 		g.GET("cert/issue/:domain", api.IssueCert)
-        g.GET("cert/:domain/info", api.CertInfo)
+		g.GET("cert/:domain/info", api.CertInfo)
 	}
 
 	return r

+ 29 - 7
server/settings/settings.go

@@ -3,6 +3,8 @@ package settings
 import (
     "gopkg.in/ini.v1"
     "log"
+    "os"
+    "path"
 )
 
 var Conf *ini.File
@@ -18,16 +20,26 @@ type Server struct {
 
 var ServerSettings = &Server{}
 
-func Init(confPath string) {
-	var err error
+var DataDir string
+var confPath string
 
-	Conf, err = ini.Load(confPath)
-	if err != nil {
-		log.Fatalf("setting.Setup, fail to parse '%s': %v", confPath, err)
-	}
+func Init(dataDir string)  {
+    DataDir = dataDir
+    confPath = path.Join(dataDir, "app.ini")
+    if _, err := os.Stat(confPath); os.IsNotExist(err) {
+        confPath = path.Join(dataDir, "app.example.ini")
+    }
+    Setup()
+}
 
-	mapTo("server", ServerSettings)
+func Setup()  {
+    var err error
+    Conf, err = ini.Load(confPath)
+    if err != nil {
+        log.Fatalf("setting.Setup, fail to parse '%s': %v", confPath, err)
+    }
 
+    mapTo("server", ServerSettings)
 }
 
 func mapTo(section string, v interface{}) {
@@ -36,3 +48,13 @@ func mapTo(section string, v interface{}) {
 		log.Fatalf("Cfg.MapTo %s err: %v", section, err)
 	}
 }
+
+func Save() (err error) {
+    confPath = path.Join(DataDir, "app.ini")
+    err = Conf.SaveTo(confPath)
+    if err != nil {
+        return
+    }
+    Setup()
+    return
+}

Някои файлове не бяха показани, защото твърде много файлове са промени