├── .DS_Store ├── .github └── workflows │ └── dotnet.yml ├── .gitignore ├── .idea ├── .gitignore ├── StartVue.iml ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml └── vcs.xml ├── ClientApp ├── .env ├── .eslintrc.js ├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── orders_example_input.json │ ├── social_preview.png │ └── tasks_example_input.json └── src │ ├── App.vue │ ├── assets │ ├── bootstrap.webp │ ├── bootstrap_disabled.webp │ ├── bootstrap_green.webp │ ├── bootstrap_white.webp │ ├── css.webp │ ├── css_disabled.webp │ ├── css_green.webp │ ├── css_white.webp │ ├── tailwind.webp │ ├── tailwind_disabled.webp │ ├── tailwind_green.webp │ ├── tailwind_white.webp │ ├── vue_logo.webp │ ├── vuecoon_default.webp │ ├── vuecoon_error.webp │ ├── vuecoon_loading.webp │ └── vuecoon_success.webp │ ├── components │ ├── BrowserFrame.vue │ ├── CodeMirror.vue │ ├── Editor.vue │ ├── GenerateSettings.vue │ ├── GitHubUser.vue │ ├── ModalPanel.vue │ ├── NotFound.vue │ ├── Supporters.vue │ └── Tab.vue │ ├── main.js │ └── utils │ ├── Helper.js │ ├── PrettyPrint.js │ ├── Schema.js │ ├── Tip.js │ └── Validate.js ├── LICENSE ├── MobileApp ├── .eslintrc.js ├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public │ ├── example_input.json │ ├── favicon.ico │ ├── index.html │ └── social_preview.png └── src │ ├── App.vue │ ├── assets │ ├── bootstrap.webp │ ├── bootstrap_green.webp │ ├── bootstrap_white.webp │ ├── css.webp │ ├── css_green.webp │ ├── css_white.webp │ ├── html_css_js_logo.webp │ ├── nuxt_coming_soon.webp │ ├── nuxt_logo.webp │ ├── tailwind.webp │ ├── tailwind_green.webp │ ├── tailwind_white.webp │ ├── vue_coming_soon.webp │ ├── vue_logo.webp │ ├── vuecoon_default.webp │ ├── vuecoon_error.webp │ ├── vuecoon_loading.webp │ └── vuecoon_success.webp │ ├── components │ ├── Alert.vue │ ├── BrowserFrame.vue │ ├── BrowserOptions.vue │ ├── CodeMirror.vue │ ├── Editor.vue │ ├── GitHubUser.vue │ ├── Landing.vue │ ├── NotFound.vue │ ├── Settings.vue │ └── Supporters.vue │ ├── main.js │ └── utils │ ├── Helper.js │ ├── PrettyPrint.js │ ├── Schema.js │ └── Validate.js ├── README.md ├── Server ├── .gitignore ├── .vscode │ ├── launch.json │ └── tasks.json ├── ApplicationDbContext.cs ├── Authorization │ ├── AllowAnonymousAttribute.cs │ ├── AuthorizeAttribute.cs │ └── BasicAuthMiddleware.cs ├── Controllers │ ├── AdminController.cs │ ├── ClientErrorsController.cs │ ├── DownloadController.cs │ ├── FilesController.cs │ ├── GenerationController.cs │ └── ShareController.cs ├── Data │ ├── ActionType.cs │ └── Frontend.cs ├── Entities │ ├── ClientError.cs │ ├── InputData.cs │ ├── ServerError.cs │ ├── ShareableLink.cs │ ├── StatisticRecord.cs │ ├── User.cs │ ├── Visit.cs │ └── Visitor.cs ├── Migrations │ ├── 20220509123222_InitialCreate.Designer.cs │ ├── 20220509123222_InitialCreate.cs │ ├── 20220527143241_ShareableLinkSettings.Designer.cs │ ├── 20220527143241_ShareableLinkSettings.cs │ └── ApplicationDbContextModelSnapshot.cs ├── Models │ ├── AuthenticateModel.cs │ ├── ChangePasswordModel.cs │ ├── EventData.cs │ ├── GenerateRequest.cs │ ├── GenerateSettings.cs │ └── VisitorData.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Services │ ├── ErrorHandlerService.cs │ ├── GenerationService.cs │ ├── GeoLocationService.cs │ ├── InputStatisticService.cs │ ├── StatisticsService.cs │ ├── UserService.cs │ └── VisitorStatisticService.cs ├── Settings │ ├── ClassSettings.cs │ └── PropertySettings.cs ├── Startup.cs ├── VueStart.csproj ├── appsettings.Development.json ├── appsettings.json └── templates │ ├── bootstrap-index.sbn │ ├── bootstrap-table-editable.sbn │ ├── bootstrap-table.sbn │ ├── tailwind-index.sbn │ ├── tailwind-table-editable.sbn │ ├── tailwind-table.sbn │ ├── vanilla-index.sbn │ ├── vanilla-table-editable.sbn │ └── vanilla-table.sbn └── vs_demo.webp /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/.DS_Store -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Setup .NET 13 | uses: actions/setup-dotnet@v1 14 | with: 15 | dotnet-version: 6.0.x 16 | lfs: true 17 | - name: Use Node.js 16.x 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: 16.x 21 | - name: Checkout LFS objects 22 | run: git lfs pull 23 | - name: Install dependencies (nuget) 24 | run: dotnet restore 25 | working-directory: ./Server 26 | - name: Build Server 27 | run: dotnet publish -c Release 28 | working-directory: ./Server 29 | - name: Archive built server application 30 | uses: actions/upload-artifact@v2 31 | with: 32 | name: server 33 | path: Server/bin/Release/net6.0/publish 34 | - name: Install dependencies (npm, desktop) 35 | run: npm ci 36 | working-directory: ./ClientApp 37 | - name: Build Client (desktop) 38 | run: npm run build 39 | working-directory: ./ClientApp 40 | - name: Archive built client application (desktop) 41 | uses: actions/upload-artifact@v2 42 | with: 43 | name: client 44 | path: ClientApp/dist 45 | - name: Install dependencies (npm, mobile) 46 | run: npm ci 47 | working-directory: ./MobileApp 48 | - name: Build Client (mobile) 49 | run: npm run build 50 | working-directory: ./MobileApp 51 | - name: Archive built client application (mobile) 52 | uses: actions/upload-artifact@v2 53 | with: 54 | name: mobile 55 | path: MobileApp/dist 56 | 57 | 58 | deploy: 59 | needs: build 60 | runs-on: ubuntu-latest 61 | steps: 62 | - name: Trigger webhook 63 | run: curl -s http://vuestart.com:9000/hooks/update-vuestart?token=${{ secrets.WEBHOOK_TOKEN }} 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Generated -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/StartVue.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ClientApp/.env: -------------------------------------------------------------------------------- 1 | VUE_APP_UI=vanilla -------------------------------------------------------------------------------- /ClientApp/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | "node": true 5 | }, 6 | extends: [ 7 | "plugin:vue/vue3-essential", 8 | "eslint:recommended" 9 | ], 10 | rules: {} 11 | } -------------------------------------------------------------------------------- /ClientApp/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /ClientApp/README.md: -------------------------------------------------------------------------------- 1 | # Vue Start 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /ClientApp/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /ClientApp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-start", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "@codemirror/basic-setup": "^0.19.3", 12 | "@codemirror/lang-html": "^0.19.4", 13 | "@codemirror/lang-javascript": "^0.19.7", 14 | "@codemirror/lang-json": "^0.19.2", 15 | "@codemirror/next": "^0.16.0", 16 | "@codemirror/view": "^0.19.48", 17 | "axios": "^0.26.1", 18 | "core-js": "^3.22.1", 19 | "vue": "^3.2.33" 20 | }, 21 | "devDependencies": { 22 | "@babel/eslint-parser": "^7.17.0", 23 | "@vue/cli-plugin-babel": "^5.0.4", 24 | "@vue/cli-service": "^5.0.4", 25 | "@vue/compiler-sfc": "^3.2.33", 26 | "eslint": "^8.13.0", 27 | "eslint-plugin-vue": "^8.6.0" 28 | }, 29 | "browserslist": [ 30 | "> 1%", 31 | "last 2 versions", 32 | "not dead" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /ClientApp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/public/favicon.ico -------------------------------------------------------------------------------- /ClientApp/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vue Start 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 35 | 36 | 37 | 38 | 44 | 45 | 46 | 49 |
50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /ClientApp/public/orders_example_input.json: -------------------------------------------------------------------------------- 1 | { 2 | "customers": [ 3 | { 4 | "name": "Microsoft Corporation", 5 | "registrationTime": "2010-05-23T12:00", 6 | "orders": [ 7 | { 8 | "date": "2010-05-23T12:00", 9 | "product": "printer paper", 10 | "quantity": 13 11 | }, 12 | { 13 | "date": "2010-06-23T12:00", 14 | "product": "printer paper", 15 | "quantity": 24 16 | } 17 | ] 18 | }, 19 | { 20 | "name": "Google LLC", 21 | "registrationTime": "2010-07-29T12:00", 22 | "orders": [ 23 | { 24 | "date": "2010-07-29T12:00", 25 | "product": "ink cartridge", 26 | "quantity": 3 27 | } 28 | ] 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /ClientApp/public/social_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/public/social_preview.png -------------------------------------------------------------------------------- /ClientApp/public/tasks_example_input.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": [ 3 | { 4 | "title": "Buy Sour Cream", 5 | "done": true, 6 | "due": "2022-04-19T10:00" 7 | }, 8 | { 9 | "title": "Chop Onions", 10 | "done": true, 11 | "due": "2022-04-19T12:00" 12 | }, 13 | { 14 | "title": "Cut the meat", 15 | "done": false, 16 | "due": "2022-04-19T12:20" 17 | }, 18 | { 19 | "title": "Taste the wine", 20 | "done": false, 21 | "due": "2022-04-19T13:00" 22 | }, 23 | { 24 | "title": "Cook Goulash", 25 | "done": false, 26 | "due": "2022-04-19T17:00" 27 | }, 28 | { 29 | "title": "Start Dishwasher", 30 | "done": false, 31 | "due": "2022-04-19T17:15" 32 | }, 33 | { 34 | "title": "Set the table", 35 | "done": false, 36 | "due": "2022-04-19T17:30" 37 | }, 38 | { 39 | "title": "Light the candles", 40 | "done": false, 41 | "due": "2022-04-19T17:45" 42 | } 43 | ] 44 | } -------------------------------------------------------------------------------- /ClientApp/src/assets/bootstrap.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/bootstrap.webp -------------------------------------------------------------------------------- /ClientApp/src/assets/bootstrap_disabled.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/bootstrap_disabled.webp -------------------------------------------------------------------------------- /ClientApp/src/assets/bootstrap_green.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/bootstrap_green.webp -------------------------------------------------------------------------------- /ClientApp/src/assets/bootstrap_white.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/bootstrap_white.webp -------------------------------------------------------------------------------- /ClientApp/src/assets/css.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/css.webp -------------------------------------------------------------------------------- /ClientApp/src/assets/css_disabled.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/css_disabled.webp -------------------------------------------------------------------------------- /ClientApp/src/assets/css_green.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/css_green.webp -------------------------------------------------------------------------------- /ClientApp/src/assets/css_white.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/css_white.webp -------------------------------------------------------------------------------- /ClientApp/src/assets/tailwind.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/tailwind.webp -------------------------------------------------------------------------------- /ClientApp/src/assets/tailwind_disabled.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/tailwind_disabled.webp -------------------------------------------------------------------------------- /ClientApp/src/assets/tailwind_green.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/tailwind_green.webp -------------------------------------------------------------------------------- /ClientApp/src/assets/tailwind_white.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/tailwind_white.webp -------------------------------------------------------------------------------- /ClientApp/src/assets/vue_logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/vue_logo.webp -------------------------------------------------------------------------------- /ClientApp/src/assets/vuecoon_default.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/vuecoon_default.webp -------------------------------------------------------------------------------- /ClientApp/src/assets/vuecoon_error.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/vuecoon_error.webp -------------------------------------------------------------------------------- /ClientApp/src/assets/vuecoon_loading.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/vuecoon_loading.webp -------------------------------------------------------------------------------- /ClientApp/src/assets/vuecoon_success.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/vuecoon_success.webp -------------------------------------------------------------------------------- /ClientApp/src/components/BrowserFrame.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 126 | 127 | 221 | -------------------------------------------------------------------------------- /ClientApp/src/components/CodeMirror.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 199 | 200 | -------------------------------------------------------------------------------- /ClientApp/src/components/GenerateSettings.vue: -------------------------------------------------------------------------------- 1 | 114 | 115 | 172 | 173 | -------------------------------------------------------------------------------- /ClientApp/src/components/GitHubUser.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 30 | 31 | -------------------------------------------------------------------------------- /ClientApp/src/components/ModalPanel.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | 20 | -------------------------------------------------------------------------------- /ClientApp/src/components/NotFound.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | 21 | -------------------------------------------------------------------------------- /ClientApp/src/components/Supporters.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 57 | 58 | -------------------------------------------------------------------------------- /ClientApp/src/components/Tab.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 30 | 31 | 80 | -------------------------------------------------------------------------------- /ClientApp/src/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.vue' 2 | import { createApp } from 'vue' 3 | import axios from 'axios'; 4 | 5 | 6 | const app = createApp(App); 7 | app.mount('#app') 8 | 9 | app.config.errorHandler = function (err, vm, info) { 10 | axios.post('api/errors', { ...err, info, vm }) 11 | }; 12 | 13 | window.onerror = function(event, source, lineno, colno, error) { 14 | axios.post('api/errors', { ...error, event, lineno, colno, source }) 15 | }; -------------------------------------------------------------------------------- /ClientApp/src/utils/Helper.js: -------------------------------------------------------------------------------- 1 | export function toPascalCase(str) { 2 | return str 3 | .replace(/\s(.)/g, function($1) { return $1.toUpperCase(); }) 4 | .replace(/\s/g, '') 5 | .replace(/^(.)/, function($1) { return $1.toUpperCase(); }); 6 | } 7 | 8 | export function toCamelCase(str) { 9 | return str 10 | .replace(/\s(.)/g, function($1) { return $1.toUpperCase(); }) 11 | .replace(/\s/g, '') 12 | .replace(/^(.)/, function($1) { return $1.toLowerCase(); }); 13 | } 14 | 15 | export function getJsonLength (json){ 16 | json = json.replace(/ {2}/g, ''); 17 | json = json.replace(/": /g, '":'); 18 | json = json.replace(/[\n\t\r]/g, ''); 19 | return json.length; 20 | } 21 | 22 | export function getJsonLineNumber(text){ 23 | return text.split('\n').length; 24 | } 25 | 26 | export function debounce (func, wait) { 27 | let timeout; 28 | return function executedFunction(...args) { 29 | const later = () => { 30 | clearTimeout(timeout); 31 | func(...args); 32 | }; 33 | clearTimeout(timeout); 34 | timeout = setTimeout(later, wait); 35 | }; 36 | } -------------------------------------------------------------------------------- /ClientApp/src/utils/PrettyPrint.js: -------------------------------------------------------------------------------- 1 | import { toCamelCase } from './Helper'; 2 | 3 | export function formatJson(json){ 4 | json = json.replaceAll(/\s/g,''); 5 | json = json.replaceAll('{','{\n'); 6 | json = json.replaceAll('}','\n}'); 7 | json = json.replaceAll('[','[\n'); 8 | json = json.replaceAll(']','\n]'); 9 | json = json.replaceAll(',',',\n'); 10 | json = json.replaceAll(':',': '); 11 | return json; 12 | } 13 | function indentLines(lines){ 14 | let tabCount = 0; 15 | for(let i = 0; i < lines.length; i++){ 16 | if(lines[i].includes('{') || lines[i].includes('[')){ 17 | lines[i] = '\t'.repeat(tabCount).concat(lines[i].trim()); 18 | tabCount++; 19 | }else if(lines[i].includes('}') || lines[i].includes(']')){ 20 | --tabCount; 21 | lines[i] = '\t'.repeat(tabCount).concat(lines[i].trim()); 22 | }else{ 23 | lines[i] = '\t'.repeat(tabCount).concat(lines[i].trim()); 24 | } 25 | } 26 | return lines; 27 | } 28 | function replaceToKey(strings, lines){ 29 | strings.forEach(key => { 30 | const name = key.replace(/"([^"]*)":/g, '$1') 31 | for(let i = 0; i < lines.length; i++){ 32 | if(lines[i].includes('key')){ 33 | lines[i] = lines[i].replace('key', `"${toCamelCase(name)}": `); 34 | break; 35 | } 36 | } 37 | }); 38 | return lines; 39 | } 40 | function replaceToString(strings, lines){ 41 | strings.forEach(string => { 42 | for(let i = 0; i < lines.length; i++){ 43 | if(lines[i].includes('""')){ 44 | lines[i] = lines[i].replace('""', string); 45 | break; 46 | } 47 | } 48 | }); 49 | return lines; 50 | } 51 | export function prettyPrint(json) { 52 | json = json.replaceAll('\r',''); 53 | json = json.replaceAll('""','"‌"'); 54 | const keys = json.match(/"([^"]*)":/g); 55 | json = json.replace(/"([^"]*)":/g, 'key'); 56 | const strings = json.match(/"[^"]*"/g); 57 | json = json.replace(/"[^"]*"/g, '""'); 58 | json = formatJson(json); 59 | let lines = indentLines(json.split('\n')); 60 | if (keys) { 61 | lines = replaceToKey(keys, lines); 62 | } 63 | if (strings) { 64 | lines = replaceToString(strings, lines); 65 | } 66 | lines.forEach((line, idx) =>{ 67 | if((/^(\t)+$/g).test(line) || line === ''){ 68 | lines.splice(idx, 1); 69 | } 70 | }) 71 | lines = lines.join('\n'); 72 | lines = lines.replaceAll('"‌"','""'); 73 | return lines; 74 | } 75 | -------------------------------------------------------------------------------- /ClientApp/src/utils/Schema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function mergeDeep(obj1, obj2) { 4 | const isObject = (obj) => obj && typeof obj === 'object'; 5 | 6 | if (!isObject(obj1) || !isObject(obj2)) { 7 | return obj1 || obj2; 8 | } 9 | 10 | let result = {}; 11 | 12 | mergeArrays(Object.keys(obj2), Object.keys(obj1)).forEach(key => { 13 | const value1 = obj1[key]; 14 | const value2 = obj2[key]; 15 | if (Array.isArray(value1) && Array.isArray(value2)) { 16 | let mergedArray = mergeArrays(value1, value2); 17 | result[key] = mergedArray; 18 | } else if (isObject(value1) && isObject(value2)) { 19 | result[key] = mergeDeep(value1, value2); 20 | } else if (value1) { 21 | result[key] = value1; 22 | } else { 23 | result[key] = value2; 24 | } 25 | }); 26 | 27 | return result; 28 | } 29 | 30 | function mergeArrays(array1, array2) { 31 | let mergedArray = [...array1]; 32 | array2.forEach(val => { 33 | if (!mergedArray.includes(val)) { 34 | mergedArray.push(val); 35 | } 36 | }); 37 | return mergedArray; 38 | } 39 | 40 | function getArraySchema(val) { 41 | let r = {type: [], properties: {}} 42 | val.forEach( function (v) { 43 | const type = getType(v); 44 | if (!r.type.includes(type)) { 45 | r.type.push(type); 46 | } 47 | r.properties = mergeDeep(r.properties, getSchema(v).properties) 48 | }) 49 | return r; 50 | } 51 | 52 | 53 | function getProperties(j) { 54 | if (!j) { 55 | return null; 56 | } 57 | let r = {}; 58 | let k = Object.keys(j); 59 | k.forEach(function(name) { 60 | r[name] = getSchema(j[name]); 61 | }) 62 | return r; 63 | } 64 | 65 | function getType(val) { 66 | 67 | if (Array.isArray(val)) { 68 | return 'array'; 69 | } 70 | 71 | if (typeof val == 'object') { 72 | return 'object'; 73 | } 74 | 75 | if (typeof val === 'number') { 76 | if (Number.isInteger(val)) { 77 | return 'integer'; 78 | } 79 | return 'float'; 80 | } 81 | 82 | if (typeof val === 'string') { 83 | let regexExp = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?)$/gi; 84 | if (regexExp.test(val)) 85 | return 'datetime' 86 | regexExp = /^(http|https):\/\/.*/gi; 87 | if (regexExp.test(val)) 88 | return 'link' 89 | } 90 | 91 | return typeof val; 92 | } 93 | 94 | export function getSchema(val) { 95 | let type = getType(val); 96 | 97 | if (type === 'array') { 98 | return {type: [type], items: getArraySchema(val)}; 99 | } 100 | 101 | if (type === 'object') { 102 | return {type: [type], properties: getProperties(val)}; 103 | } 104 | 105 | return { type: [type] }; 106 | } 107 | 108 | -------------------------------------------------------------------------------- /ClientApp/src/utils/Tip.js: -------------------------------------------------------------------------------- 1 | 2 | export default class Tip { 3 | constructor() { 4 | this.tipIdx = parseInt(localStorage.getItem('tipIdx')) || 0; 5 | this.tips = [ 6 | 'Try to edit the JSON data, and see the changes in the application.', 7 | 'If you make structural changes, the application is regenerated.', 8 | 'Try out multiple layouts with the button in the bottom right corner.', 9 | 'When you are done, click the download button.' 10 | ]; 11 | } 12 | 13 | hideTips() { 14 | this.tipIdx = this.tips.length; 15 | localStorage.setItem('tipIdx', this.tipIdx.toString()); 16 | } 17 | 18 | modified() { 19 | if (this.tipIdx === 0) { 20 | this.tipIdx = 1; 21 | localStorage.setItem('tipIdx', this.tipIdx.toString()); 22 | return true 23 | } 24 | return false 25 | } 26 | generated() { 27 | if (this.tipIdx === 1) { 28 | this.tipIdx = 2; 29 | localStorage.setItem('tipIdx', this.tipIdx.toString()); 30 | return true 31 | } 32 | return false 33 | } 34 | typeChanged() { 35 | if (this.tipIdx === 2) { 36 | this.tipIdx = 3; 37 | localStorage.setItem('tipIdx', this.tipIdx.toString()); 38 | return true 39 | } 40 | return false 41 | } 42 | downloaded() { 43 | if (this.tipIdx === 3) { 44 | this.tipIdx = 4; 45 | localStorage.setItem('tipIdx', this.tipIdx.toString()); 46 | return true 47 | } 48 | return false 49 | } 50 | getTip() { 51 | let msg = this.tips[this.tipIdx]; 52 | if (this.tipIdx === 4) { 53 | this.tipIdx = 5; 54 | localStorage.setItem('tipIdx', this.tipIdx.toString()); 55 | } 56 | return msg; 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /ClientApp/src/utils/Validate.js: -------------------------------------------------------------------------------- 1 | 2 | function getFrom(str, position, line){ 3 | if(!line){ 4 | return parseInt(position); 5 | } else { 6 | let charCount = 0; 7 | const strArray = str.split('\n'); 8 | for(let i = 0; i < line-1; i++){ 9 | charCount += strArray[i].length+1; 10 | } 11 | return charCount + parseInt(position) - 1; 12 | } 13 | } 14 | 15 | function getErrorPosition(str, err) { 16 | let userAgent = navigator.userAgent; 17 | let from = -1; 18 | let to = -1; 19 | let match = err.message.match(/\d+/g); 20 | if (match === null || match.length === 0) 21 | return { from: -1, to: -1 }; 22 | if(userAgent.match(/firefox|fxios/i)){ 23 | if (match.length === 1) 24 | return { from: -1, to: -1 }; 25 | from = getFrom(str, match[1], match[0]); 26 | }else if(userAgent.match(/opr\//i)){ 27 | from = getFrom(str, match[0]); 28 | } else if(userAgent.match(/edg/i)){ 29 | from = getFrom(str, match[0]); 30 | }else{ 31 | from = getFrom(str, match[0]); //chrome 32 | } 33 | if (from > 0) { 34 | let charCount = 0; 35 | const strArray = str.split('\n'); 36 | for(let i = 0; i < strArray.length; i++){ 37 | charCount += strArray[i].length+1; 38 | if(charCount >= from){ 39 | to = charCount-1; 40 | break; 41 | } 42 | } 43 | } 44 | return { from, to }; 45 | } 46 | 47 | export function validateJson(text) { 48 | try { 49 | JSON.parse(text); 50 | return {error: false, message: ''}; 51 | } catch (err) { 52 | const positions = getErrorPosition(text, err); 53 | return {error: true, from: positions.from, to: positions.to, message: err.message}; 54 | } 55 | } -------------------------------------------------------------------------------- /MobileApp/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | "node": true 5 | }, 6 | extends: [ 7 | "plugin:vue/vue3-essential", 8 | "eslint:recommended" 9 | ], 10 | rules: {} 11 | } -------------------------------------------------------------------------------- /MobileApp/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /MobileApp/README.md: -------------------------------------------------------------------------------- 1 | # Vue Start 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /MobileApp/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /MobileApp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-start", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "@codemirror/basic-setup": "^0.19.3", 12 | "@codemirror/lang-html": "^0.19.4", 13 | "@codemirror/lang-json": "^0.19.2", 14 | "@codemirror/next": "^0.16.0", 15 | "@codemirror/view": "^0.19.48", 16 | "axios": "^0.26.1", 17 | "core-js": "^3.22.1", 18 | "vue": "^3.2.33" 19 | }, 20 | "devDependencies": { 21 | "@babel/eslint-parser": "^7.17.0", 22 | "@vue/cli-plugin-babel": "^5.0.4", 23 | "@vue/cli-service": "^5.0.4", 24 | "@vue/compiler-sfc": "^3.2.33", 25 | "eslint": "^8.13.0", 26 | "eslint-plugin-vue": "^8.6.0" 27 | }, 28 | "browserslist": [ 29 | "> 1%", 30 | "last 2 versions", 31 | "not dead" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /MobileApp/public/example_input.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": [ 3 | { 4 | "title": "Buy Sour Cream", 5 | "done": true, 6 | "due": "2022-04-19T10:00" 7 | }, 8 | { 9 | "title": "Cook Goulash", 10 | "done": true, 11 | "due": "2022-04-19T12:00" 12 | }, 13 | { 14 | "title": "Start Dishwasher", 15 | "done": false, 16 | "due": "2022-04-19T16:00" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /MobileApp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/public/favicon.ico -------------------------------------------------------------------------------- /MobileApp/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vue Start 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 35 | 36 | 37 | 40 |
41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /MobileApp/public/social_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/public/social_preview.png -------------------------------------------------------------------------------- /MobileApp/src/App.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 119 | 120 | 193 | -------------------------------------------------------------------------------- /MobileApp/src/assets/bootstrap.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/bootstrap.webp -------------------------------------------------------------------------------- /MobileApp/src/assets/bootstrap_green.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/bootstrap_green.webp -------------------------------------------------------------------------------- /MobileApp/src/assets/bootstrap_white.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/bootstrap_white.webp -------------------------------------------------------------------------------- /MobileApp/src/assets/css.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/css.webp -------------------------------------------------------------------------------- /MobileApp/src/assets/css_green.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/css_green.webp -------------------------------------------------------------------------------- /MobileApp/src/assets/css_white.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/css_white.webp -------------------------------------------------------------------------------- /MobileApp/src/assets/html_css_js_logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/html_css_js_logo.webp -------------------------------------------------------------------------------- /MobileApp/src/assets/nuxt_coming_soon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/nuxt_coming_soon.webp -------------------------------------------------------------------------------- /MobileApp/src/assets/nuxt_logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/nuxt_logo.webp -------------------------------------------------------------------------------- /MobileApp/src/assets/tailwind.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/tailwind.webp -------------------------------------------------------------------------------- /MobileApp/src/assets/tailwind_green.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/tailwind_green.webp -------------------------------------------------------------------------------- /MobileApp/src/assets/tailwind_white.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/tailwind_white.webp -------------------------------------------------------------------------------- /MobileApp/src/assets/vue_coming_soon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/vue_coming_soon.webp -------------------------------------------------------------------------------- /MobileApp/src/assets/vue_logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/vue_logo.webp -------------------------------------------------------------------------------- /MobileApp/src/assets/vuecoon_default.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/vuecoon_default.webp -------------------------------------------------------------------------------- /MobileApp/src/assets/vuecoon_error.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/vuecoon_error.webp -------------------------------------------------------------------------------- /MobileApp/src/assets/vuecoon_loading.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/vuecoon_loading.webp -------------------------------------------------------------------------------- /MobileApp/src/assets/vuecoon_success.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/vuecoon_success.webp -------------------------------------------------------------------------------- /MobileApp/src/components/Alert.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 31 | 32 | -------------------------------------------------------------------------------- /MobileApp/src/components/BrowserFrame.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 100 | 101 | 106 | -------------------------------------------------------------------------------- /MobileApp/src/components/BrowserOptions.vue: -------------------------------------------------------------------------------- 1 | 25 | 37 | 38 | 88 | -------------------------------------------------------------------------------- /MobileApp/src/components/CodeMirror.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 201 | 202 | -------------------------------------------------------------------------------- /MobileApp/src/components/Editor.vue: -------------------------------------------------------------------------------- 1 | 19 | 160 | 161 | 209 | -------------------------------------------------------------------------------- /MobileApp/src/components/GitHubUser.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 30 | 31 | -------------------------------------------------------------------------------- /MobileApp/src/components/Landing.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 25 | 26 | -------------------------------------------------------------------------------- /MobileApp/src/components/NotFound.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | 21 | -------------------------------------------------------------------------------- /MobileApp/src/components/Settings.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 96 | 97 | -------------------------------------------------------------------------------- /MobileApp/src/components/Supporters.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 64 | 65 | -------------------------------------------------------------------------------- /MobileApp/src/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.vue' 2 | import { createApp } from 'vue' 3 | import axios from 'axios'; 4 | 5 | 6 | const app = createApp(App); 7 | app.mount('#app') 8 | 9 | app.config.errorHandler = function (err, vm, info) { 10 | axios.post('api/errors', { ...err, info, vm }) 11 | }; 12 | 13 | window.onerror = function(event, source, lineno, colno, error) { 14 | axios.post('api/errors', { ...error, event, lineno, colno, source }) 15 | }; -------------------------------------------------------------------------------- /MobileApp/src/utils/Helper.js: -------------------------------------------------------------------------------- 1 | export function toPascalCase(str) { 2 | return str 3 | .replace(/\s(.)/g, function($1) { return $1.toUpperCase(); }) 4 | .replace(/\s/g, '') 5 | .replace(/^(.)/, function($1) { return $1.toUpperCase(); }); 6 | } 7 | 8 | export function toCamelCase(str) { 9 | return str 10 | .replace(/\s(.)/g, function($1) { return $1.toUpperCase(); }) 11 | .replace(/\s/g, '') 12 | .replace(/^(.)/, function($1) { return $1.toLowerCase(); }); 13 | } 14 | 15 | export function getJsonLength (json){ 16 | json = json.replace(/ {2}/g, ''); 17 | json = json.replace(/": /g, '":'); 18 | json = json.replace(/[\n\t\r]/g, ''); 19 | return json.length; 20 | } 21 | 22 | export function getJsonLineNumber(text){ 23 | return text.split('\n').length; 24 | } 25 | 26 | export function debounce (func, wait) { 27 | let timeout; 28 | return function executedFunction(...args) { 29 | const later = () => { 30 | clearTimeout(timeout); 31 | func(...args); 32 | }; 33 | clearTimeout(timeout); 34 | timeout = setTimeout(later, wait); 35 | }; 36 | } -------------------------------------------------------------------------------- /MobileApp/src/utils/PrettyPrint.js: -------------------------------------------------------------------------------- 1 | import { toCamelCase } from './Helper'; 2 | 3 | export function formatJson(json){ 4 | json = json.replaceAll(/\s/g,''); 5 | json = json.replaceAll('{','{\n'); 6 | json = json.replaceAll('}','\n}'); 7 | json = json.replaceAll('[','[\n'); 8 | json = json.replaceAll(']','\n]'); 9 | json = json.replaceAll(',',',\n'); 10 | json = json.replaceAll(':',': '); 11 | return json; 12 | } 13 | function indentLines(lines){ 14 | let tabCount = 0; 15 | for(let i = 0; i < lines.length; i++){ 16 | if(lines[i].includes('{') || lines[i].includes('[')){ 17 | lines[i] = '\t'.repeat(tabCount).concat(lines[i].trim()); 18 | tabCount++; 19 | }else if(lines[i].includes('}') || lines[i].includes(']')){ 20 | --tabCount; 21 | lines[i] = '\t'.repeat(tabCount).concat(lines[i].trim()); 22 | }else{ 23 | lines[i] = '\t'.repeat(tabCount).concat(lines[i].trim()); 24 | } 25 | } 26 | return lines; 27 | } 28 | function replaceToKey(strings, lines){ 29 | strings.forEach(key => { 30 | const name = key.replace(/"([^"]*)":/g, '$1') 31 | for(let i = 0; i < lines.length; i++){ 32 | if(lines[i].includes('key')){ 33 | lines[i] = lines[i].replace('key', `"${toCamelCase(name)}": `); 34 | break; 35 | } 36 | } 37 | }); 38 | return lines; 39 | } 40 | function replaceToString(strings, lines){ 41 | strings.forEach(string => { 42 | for(let i = 0; i < lines.length; i++){ 43 | if(lines[i].includes('""')){ 44 | lines[i] = lines[i].replace('""', string); 45 | break; 46 | } 47 | } 48 | }); 49 | return lines; 50 | } 51 | export function prettyPrint(json) { 52 | json = json.replaceAll('\r',''); 53 | json = json.replaceAll('""','"‌"'); 54 | const keys = json.match(/"([^"]*)":/g); 55 | json = json.replace(/"([^"]*)":/g, 'key'); 56 | const strings = json.match(/"[^"]*"/g); 57 | json = json.replace(/"[^"]*"/g, '""'); 58 | json = formatJson(json); 59 | let lines = indentLines(json.split('\n')); 60 | if (keys) { 61 | lines = replaceToKey(keys, lines); 62 | } 63 | if (strings) { 64 | lines = replaceToString(strings, lines); 65 | } 66 | lines.forEach((line, idx) =>{ 67 | if((/^(\t)+$/g).test(line) || line === ''){ 68 | lines.splice(idx, 1); 69 | } 70 | }) 71 | lines = lines.join('\n'); 72 | lines = lines.replaceAll('"‌"','""'); 73 | return lines; 74 | } 75 | -------------------------------------------------------------------------------- /MobileApp/src/utils/Schema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function mergeDeep(obj1, obj2) { 4 | const isObject = (obj) => obj && typeof obj === 'object'; 5 | 6 | if (!isObject(obj1) || !isObject(obj2)) { 7 | return obj1 || obj2; 8 | } 9 | 10 | let result = {}; 11 | 12 | mergeArrays(Object.keys(obj2), Object.keys(obj1)).forEach(key => { 13 | const value1 = obj1[key]; 14 | const value2 = obj2[key]; 15 | if (Array.isArray(value1) && Array.isArray(value2)) { 16 | let mergedArray = mergeArrays(value1, value2); 17 | result[key] = mergedArray; 18 | } else if (isObject(value1) && isObject(value2)) { 19 | result[key] = mergeDeep(value1, value2); 20 | } else if (value1) { 21 | result[key] = value1; 22 | } else { 23 | result[key] = value2; 24 | } 25 | }); 26 | 27 | return result; 28 | } 29 | 30 | function mergeArrays(array1, array2) { 31 | let mergedArray = [...array1]; 32 | array2.forEach(val => { 33 | if (!mergedArray.includes(val)) { 34 | mergedArray.push(val); 35 | } 36 | }); 37 | return mergedArray; 38 | } 39 | 40 | function getArraySchema(val) { 41 | let r = {type: [], properties: {}} 42 | val.forEach( function (v) { 43 | const type = getType(v); 44 | if (!r.type.includes(type)) { 45 | r.type.push(type); 46 | } 47 | r.properties = mergeDeep(r.properties, getSchema(v).properties) 48 | }) 49 | return r; 50 | } 51 | 52 | 53 | function getProperties(j) { 54 | if (!j) { 55 | return null; 56 | } 57 | let r = {}; 58 | let k = Object.keys(j); 59 | k.forEach(function(name) { 60 | r[name] = getSchema(j[name]); 61 | }) 62 | return r; 63 | } 64 | 65 | function getType(val) { 66 | 67 | if (Array.isArray(val)) { 68 | return 'array'; 69 | } 70 | 71 | if (typeof val == 'object') { 72 | return 'object'; 73 | } 74 | 75 | if (typeof val === 'number') { 76 | if (Number.isInteger(val)) { 77 | return 'integer'; 78 | } 79 | return 'float'; 80 | } 81 | 82 | if (typeof val === 'string') { 83 | const regexExp = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2})$/gi; 84 | if (regexExp.test(val)) 85 | return 'datetime' 86 | } 87 | 88 | return typeof val; 89 | } 90 | 91 | export function getSchema(val) { 92 | let type = getType(val); 93 | 94 | if (type === 'array') { 95 | return {type: [type], items: getArraySchema(val)}; 96 | } 97 | 98 | if (type === 'object') { 99 | return {type: [type], properties: getProperties(val)}; 100 | } 101 | 102 | return { type: [type] }; 103 | } 104 | 105 | -------------------------------------------------------------------------------- /MobileApp/src/utils/Validate.js: -------------------------------------------------------------------------------- 1 | 2 | function getFrom(str, position, line){ 3 | if(!line){ 4 | return parseInt(position); 5 | } else { 6 | let charCount = 0; 7 | const strArray = str.split('\n'); 8 | for(let i = 0; i < line-1; i++){ 9 | charCount += strArray[i].length+1; 10 | } 11 | return charCount + parseInt(position) - 1; 12 | } 13 | } 14 | 15 | function getErrorPosition(str, err) { 16 | let userAgent = navigator.userAgent; 17 | let from = -1; 18 | let to = -1; 19 | let match = err.message.match(/\d+/g); 20 | if (match === null || match.length === 0) 21 | return { from: -1, to: -1 }; 22 | if(userAgent.match(/firefox|fxios/i)){ 23 | if (match.length === 1) 24 | return { from: -1, to: -1 }; 25 | from = getFrom(str, match[1], match[0]); 26 | }else if(userAgent.match(/opr\//i)){ 27 | from = getFrom(str, match[0]); 28 | } else if(userAgent.match(/edg/i)){ 29 | from = getFrom(str, match[0]); 30 | }else{ 31 | from = getFrom(str, match[0]); //chrome 32 | } 33 | if (from > 0) { 34 | let charCount = 0; 35 | const strArray = str.split('\n'); 36 | for(let i = 0; i < strArray.length; i++){ 37 | charCount += strArray[i].length+1; 38 | if(charCount >= from){ 39 | to = charCount-1; 40 | break; 41 | } 42 | } 43 | } 44 | return { from, to }; 45 | } 46 | 47 | export function validateJson(text) { 48 | try { 49 | const obj = JSON.parse(text); 50 | if (typeof obj !== 'object' || Array.isArray(obj)) 51 | return {error: true, message: 'The root elements must be an object!'}; 52 | let idx = 0; 53 | for (const prop in obj) { 54 | if (typeof obj[prop] !== 'object') 55 | return {error: true, message: 'The root elements must only have a single property with object or array type!'}; 56 | idx += 1; 57 | } 58 | return {error: false, message: ''}; 59 | } catch (err) { 60 | const positions = getErrorPosition(text, err); 61 | return {error: true, from: positions.from, to: positions.to, message: err.message}; 62 | } 63 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | Vue Start 6 |

7 |

8 | Turn any JSON into Vue 3 tables! 9 |

10 |

11 | 12 | Deploy 13 | 14 | 15 | Licence 16 | 17 | 18 | Website 19 | 20 |

21 | 22 | ## Introduction 23 | 24 | Create well coded Vue 3 tables with Bootstrap, Tailwind CSS or vanilla CSS. Try it online at [vuestart.com](https://vuestart.com)! 25 | 26 | **Turn this:** 27 | ```js 28 | { 29 | "customers": [ 30 | { 31 | "name": "Microsoft Corporation", 32 | "registrationTime": "2010-05-23T12:00", 33 | "orders": [ 34 | { 35 | "date": "2010-05-23T12:00", 36 | "product": "printer paper", 37 | "quantity": 13 38 | }, 39 | { 40 | "date": "2010-06-23T12:00", 41 | "product": "printer paper", 42 | "quantity": 24 43 | } 44 | ] 45 | }, 46 | { 47 | "name": "Google LLC", 48 | "registrationTime": "2010-07-29T12:00", 49 | "orders": [ 50 | { 51 | "date": "2010-07-29T12:00", 52 | "product": "ink cartridge", 53 | "quantity": 3 54 | } 55 | ] 56 | } 57 | ] 58 | } 59 | ``` 60 | **Into this:** 61 | 62 | 63 | 64 | ### Features 65 | 66 | - Pagination 67 | - Sortable columns 68 | - Navigation for nested data structures 69 | - Easily theme-able 70 | 71 | 72 | ## Licencing 73 | Please note that the licencing of the Vue Start project and the licencing of the code generated by the Vue Start project is different. 74 | 75 | ### Generated Code 76 | 77 | 78 | 79 | The code generated by the Vue Start project is [unlicenced](https://unlicense.org). This means that you may use the generated code without restrictions in any project under any open source or comercial licence. 80 | 81 | ### Vue Start 82 | 83 | 84 | 85 | The Vue Start project itself is available under the [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html). If you would like to get a comercial licence, please send an e-mail to [gabor.angyal@codesharp.hu](mailto://gabor.angyal@codesharp.hu). 86 | -------------------------------------------------------------------------------- /Server/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # Tye 66 | .tye/ 67 | 68 | # ASP.NET Scaffolding 69 | ScaffoldingReadMe.txt 70 | 71 | # StyleCop 72 | StyleCopReport.xml 73 | 74 | # Files built by Visual Studio 75 | *_i.c 76 | *_p.c 77 | *_h.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.iobj 82 | *.pch 83 | *.pdb 84 | *.ipdb 85 | *.pgc 86 | *.pgd 87 | *.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.vspscc 97 | *.vssscc 98 | .builds 99 | *.pidb 100 | *.svclog 101 | *.scc 102 | 103 | # Chutzpah Test files 104 | _Chutzpah* 105 | 106 | # Visual C++ cache files 107 | ipch/ 108 | *.aps 109 | *.ncb 110 | *.opendb 111 | *.opensdf 112 | *.sdf 113 | *.cachefile 114 | *.VC.db 115 | *.VC.VC.opendb 116 | 117 | # Visual Studio profiler 118 | *.psess 119 | *.vsp 120 | *.vspx 121 | *.sap 122 | 123 | # Visual Studio Trace Files 124 | *.e2e 125 | 126 | # TFS 2012 Local Workspace 127 | $tf/ 128 | 129 | # Guidance Automation Toolkit 130 | *.gpState 131 | 132 | # ReSharper is a .NET coding add-in 133 | _ReSharper*/ 134 | *.[Rr]e[Ss]harper 135 | *.DotSettings.user 136 | 137 | # TeamCity is a build add-in 138 | _TeamCity* 139 | 140 | # DotCover is a Code Coverage Tool 141 | *.dotCover 142 | 143 | # AxoCover is a Code Coverage Tool 144 | .axoCover/* 145 | !.axoCover/settings.json 146 | 147 | # Coverlet is a free, cross platform Code Coverage Tool 148 | coverage*.json 149 | coverage*.xml 150 | coverage*.info 151 | 152 | # Visual Studio code coverage results 153 | *.coverage 154 | *.coveragexml 155 | 156 | # NCrunch 157 | _NCrunch_* 158 | .*crunch*.local.xml 159 | nCrunchTemp_* 160 | 161 | # MightyMoose 162 | *.mm.* 163 | AutoTest.Net/ 164 | 165 | # Web workbench (sass) 166 | .sass-cache/ 167 | 168 | # Installshield output folder 169 | [Ee]xpress/ 170 | 171 | # DocProject is a documentation generator add-in 172 | DocProject/buildhelp/ 173 | DocProject/Help/*.HxT 174 | DocProject/Help/*.HxC 175 | DocProject/Help/*.hhc 176 | DocProject/Help/*.hhk 177 | DocProject/Help/*.hhp 178 | DocProject/Help/Html2 179 | DocProject/Help/html 180 | 181 | # Click-Once directory 182 | publish/ 183 | 184 | # Publish Web Output 185 | *.[Pp]ublish.xml 186 | *.azurePubxml 187 | # Note: Comment the next line if you want to checkin your web deploy settings, 188 | # but database connection strings (with potential passwords) will be unencrypted 189 | *.pubxml 190 | *.publishproj 191 | 192 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 193 | # checkin your Azure Web App publish settings, but sensitive information contained 194 | # in these scripts will be unencrypted 195 | PublishScripts/ 196 | 197 | # NuGet Packages 198 | *.nupkg 199 | # NuGet Symbol Packages 200 | *.snupkg 201 | # The packages folder can be ignored because of Package Restore 202 | **/[Pp]ackages/* 203 | # except build/, which is used as an MSBuild target. 204 | !**/[Pp]ackages/build/ 205 | # Uncomment if necessary however generally it will be regenerated when needed 206 | #!**/[Pp]ackages/repositories.config 207 | # NuGet v3's project.json files produces more ignorable files 208 | *.nuget.props 209 | *.nuget.targets 210 | 211 | # Microsoft Azure Build Output 212 | csx/ 213 | *.build.csdef 214 | 215 | # Microsoft Azure Emulator 216 | ecf/ 217 | rcf/ 218 | 219 | # Windows Store app package directories and files 220 | AppPackages/ 221 | BundleArtifacts/ 222 | Package.StoreAssociation.xml 223 | _pkginfo.txt 224 | *.appx 225 | *.appxbundle 226 | *.appxupload 227 | 228 | # Visual Studio cache files 229 | # files ending in .cache can be ignored 230 | *.[Cc]ache 231 | # but keep track of directories ending in .cache 232 | !?*.[Cc]ache/ 233 | 234 | # Others 235 | ClientBin/ 236 | ~$* 237 | *~ 238 | *.dbmdl 239 | *.dbproj.schemaview 240 | *.jfm 241 | *.pfx 242 | *.publishsettings 243 | orleans.codegen.cs 244 | 245 | # Including strong name files can present a security risk 246 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 247 | #*.snk 248 | 249 | # Since there are multiple workflows, uncomment next line to ignore bower_components 250 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 251 | #bower_components/ 252 | 253 | # RIA/Silverlight projects 254 | Generated_Code/ 255 | 256 | # Backup & report files from converting an old project file 257 | # to a newer Visual Studio version. Backup files are not needed, 258 | # because we have git ;-) 259 | _UpgradeReport_Files/ 260 | Backup*/ 261 | UpgradeLog*.XML 262 | UpgradeLog*.htm 263 | ServiceFabricBackup/ 264 | *.rptproj.bak 265 | 266 | # SQL Server files 267 | *.mdf 268 | *.ldf 269 | *.ndf 270 | 271 | # Business Intelligence projects 272 | *.rdl.data 273 | *.bim.layout 274 | *.bim_*.settings 275 | *.rptproj.rsuser 276 | *- [Bb]ackup.rdl 277 | *- [Bb]ackup ([0-9]).rdl 278 | *- [Bb]ackup ([0-9][0-9]).rdl 279 | 280 | # Microsoft Fakes 281 | FakesAssemblies/ 282 | 283 | # GhostDoc plugin setting file 284 | *.GhostDoc.xml 285 | 286 | # Node.js Tools for Visual Studio 287 | .ntvs_analysis.dat 288 | node_modules/ 289 | 290 | # Visual Studio 6 build log 291 | *.plg 292 | 293 | # Visual Studio 6 workspace options file 294 | *.opt 295 | 296 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 297 | *.vbw 298 | 299 | # Visual Studio LightSwitch build output 300 | **/*.HTMLClient/GeneratedArtifacts 301 | **/*.DesktopClient/GeneratedArtifacts 302 | **/*.DesktopClient/ModelManifest.xml 303 | **/*.Server/GeneratedArtifacts 304 | **/*.Server/ModelManifest.xml 305 | _Pvt_Extensions 306 | 307 | # Paket dependency manager 308 | .paket/paket.exe 309 | paket-files/ 310 | 311 | # FAKE - F# Make 312 | .fake/ 313 | 314 | # CodeRush personal settings 315 | .cr/personal 316 | 317 | # Python Tools for Visual Studio (PTVS) 318 | __pycache__/ 319 | *.pyc 320 | 321 | # Cake - Uncomment if you are using it 322 | # tools/** 323 | # !tools/packages.config 324 | 325 | # Tabs Studio 326 | *.tss 327 | 328 | # Telerik's JustMock configuration file 329 | *.jmconfig 330 | 331 | # BizTalk build output 332 | *.btp.cs 333 | *.btm.cs 334 | *.odx.cs 335 | *.xsd.cs 336 | 337 | # OpenCover UI analysis results 338 | OpenCover/ 339 | 340 | # Azure Stream Analytics local run output 341 | ASALocalRun/ 342 | 343 | # MSBuild Binary and Structured Log 344 | *.binlog 345 | 346 | # NVidia Nsight GPU debugger configuration file 347 | *.nvuser 348 | 349 | # MFractors (Xamarin productivity tool) working folder 350 | .mfractor/ 351 | 352 | # Local History for Visual Studio 353 | .localhistory/ 354 | 355 | # BeatPulse healthcheck temp database 356 | healthchecksdb 357 | 358 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 359 | MigrationBackup/ 360 | 361 | # Ionide (cross platform F# VS Code tools) working folder 362 | .ionide/ 363 | 364 | # Fody - auto-generated XML schema 365 | FodyWeavers.xsd 366 | 367 | ## 368 | ## Visual studio for Mac 369 | ## 370 | 371 | 372 | # globs 373 | Makefile.in 374 | *.userprefs 375 | *.usertasks 376 | config.make 377 | config.status 378 | aclocal.m4 379 | install-sh 380 | autom4te.cache/ 381 | *.tar.gz 382 | tarballs/ 383 | test-results/ 384 | 385 | # Mac bundle stuff 386 | *.dmg 387 | *.app 388 | 389 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 390 | # General 391 | .DS_Store 392 | .AppleDouble 393 | .LSOverride 394 | 395 | # Icon must end with two \r 396 | Icon 397 | 398 | 399 | # Thumbnails 400 | ._* 401 | 402 | # Files that might appear in the root of a volume 403 | .DocumentRevisions-V100 404 | .fseventsd 405 | .Spotlight-V100 406 | .TemporaryItems 407 | .Trashes 408 | .VolumeIcon.icns 409 | .com.apple.timemachine.donotpresent 410 | 411 | # Directories potentially created on remote AFP share 412 | .AppleDB 413 | .AppleDesktop 414 | Network Trash Folder 415 | Temporary Items 416 | .apdisk 417 | 418 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 419 | # Windows thumbnail cache files 420 | Thumbs.db 421 | ehthumbs.db 422 | ehthumbs_vista.db 423 | 424 | # Dump file 425 | *.stackdump 426 | 427 | # Folder config file 428 | [Dd]esktop.ini 429 | 430 | # Recycle Bin used on file shares 431 | $RECYCLE.BIN/ 432 | 433 | # Windows Installer files 434 | *.cab 435 | *.msi 436 | *.msix 437 | *.msm 438 | *.msp 439 | 440 | # Windows shortcuts 441 | *.lnk 442 | 443 | # JetBrains Rider 444 | .idea/ 445 | *.sln.iml 446 | 447 | ## 448 | ## Visual Studio Code 449 | ## 450 | .vscode/* 451 | !.vscode/settings.json 452 | !.vscode/tasks.json 453 | !.vscode/launch.json 454 | !.vscode/extensions.json 455 | -------------------------------------------------------------------------------- /Server/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | "program": "${workspaceFolder}/bin/Debug/net6.0/VueStart.dll", 13 | "args": [], 14 | "cwd": "${workspaceFolder}", 15 | "stopAtEntry": false, 16 | "serverReadyAction": { 17 | "action": "openExternally", 18 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 19 | }, 20 | "env": { 21 | "ASPNETCORE_ENVIRONMENT": "Development" 22 | }, 23 | "sourceFileMap": { 24 | "/Views": "${workspaceFolder}/Views" 25 | } 26 | }, 27 | { 28 | "name": ".NET Core Attach", 29 | "type": "coreclr", 30 | "request": "attach" 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /Server/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/VueStart.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/VueStart.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/VueStart.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /Server/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.AspNetCore.Identity; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.Extensions.Configuration; 6 | 7 | namespace VueStart; 8 | 9 | public class ApplicationDbContext : DbContext 10 | { 11 | public DbSet InputData { get; set; } 12 | public DbSet StatisticRecords { get; set; } 13 | public DbSet ServerErrors { get; set; } 14 | public DbSet ClientErrors { get; set; } 15 | public DbSet Visitors { get; set; } 16 | public DbSet Visits { get; set; } 17 | public DbSet Users { get; set; } 18 | public DbSet ShareableLinks { get; set; } 19 | 20 | public ApplicationDbContext(DbContextOptions options) : base(options) 21 | { 22 | } 23 | 24 | protected override void OnModelCreating(ModelBuilder modelBuilder) 25 | { 26 | modelBuilder.Entity() 27 | .Property(b => b.Hash) 28 | .IsRequired(); 29 | modelBuilder.Entity() 30 | .Property(b => b.Data) 31 | .IsRequired(); 32 | modelBuilder.Entity() 33 | .Property(b => b.Token) 34 | .IsRequired(); 35 | modelBuilder.Entity() 36 | .HasMany(v => v.Visits); 37 | modelBuilder.Entity() 38 | .HasMany(v => v.StatisticRecords); 39 | modelBuilder.Entity() 40 | .HasOne(v => v.InputData); 41 | 42 | var passwordHasher = new PasswordHasher(); 43 | var admin = new User { 44 | Id = 1, 45 | Username = "admin" 46 | }; 47 | admin.PasswordHash = passwordHasher.HashPassword(admin, "password"); 48 | modelBuilder.Entity().HasData(admin); 49 | } 50 | } -------------------------------------------------------------------------------- /Server/Authorization/AllowAnonymousAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace VueStart.Authorization; 4 | 5 | [AttributeUsage(AttributeTargets.Method)] 6 | public class AllowAnonymousAttribute : Attribute 7 | { } -------------------------------------------------------------------------------- /Server/Authorization/AuthorizeAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace VueStart.Authorization; 2 | 3 | using System; 4 | using System.Linq; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.Filters; 8 | 9 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 10 | public class AuthorizeAttribute : Attribute, IAuthorizationFilter 11 | { 12 | public void OnAuthorization(AuthorizationFilterContext context) 13 | { 14 | // skip authorization if action is decorated with [AllowAnonymous] attribute 15 | var allowAnonymous = context.ActionDescriptor.EndpointMetadata.OfType().Any(); 16 | if (allowAnonymous) 17 | return; 18 | 19 | var user = (User)context.HttpContext.Items["User"]; 20 | if (user == null) 21 | { 22 | // not logged in - return 401 unauthorized 23 | context.Result = new JsonResult(new { message = "Unauthorized" }) { StatusCode = StatusCodes.Status401Unauthorized }; 24 | 25 | // set 'WWW-Authenticate' header to trigger login popup in browsers 26 | context.HttpContext.Response.Headers["WWW-Authenticate"] = "Basic realm=\"\", charset=\"UTF-8\""; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /Server/Authorization/BasicAuthMiddleware.cs: -------------------------------------------------------------------------------- 1 | namespace VueStart.Authorization; 2 | 3 | using System; 4 | using System.Net.Http.Headers; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Http; 8 | using VueStart.Services; 9 | 10 | public class BasicAuthMiddleware 11 | { 12 | private readonly RequestDelegate _next; 13 | 14 | public BasicAuthMiddleware(RequestDelegate next) 15 | { 16 | _next = next; 17 | } 18 | 19 | public async Task Invoke(HttpContext context, UserService userService) 20 | { 21 | try 22 | { 23 | var authHeader = AuthenticationHeaderValue.Parse(context.Request.Headers["Authorization"]); 24 | var credentialBytes = Convert.FromBase64String(authHeader.Parameter); 25 | var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':', 2); 26 | var username = credentials[0]; 27 | var password = credentials[1]; 28 | 29 | // authenticate credentials with user service and attach user to http context 30 | context.Items["User"] = userService.Authenticate(username, password); 31 | } 32 | catch 33 | { 34 | // do nothing if invalid auth header 35 | // user is not attached to context so request won't have access to secure routes 36 | } 37 | 38 | await _next(context); 39 | } 40 | } -------------------------------------------------------------------------------- /Server/Controllers/AdminController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Microsoft.AspNetCore.Mvc; 4 | using System.Text.Json; 5 | using VueStart.Services; 6 | using System.Text; 7 | using Microsoft.Extensions.Caching.Memory; 8 | using Microsoft.EntityFrameworkCore; 9 | using System.Collections.Generic; 10 | using VueStart.Authorization; 11 | 12 | namespace VueStart.Controllers; 13 | 14 | [Authorize] 15 | [ApiController] 16 | [Route("admin")] 17 | public class AdminController : ControllerBase 18 | { 19 | private readonly ApplicationDbContext dbContext; 20 | private readonly IMemoryCache memoryCache; 21 | private readonly GenerationService generationService; 22 | private readonly GenerationService generateService; 23 | private readonly VisitorStatisticService visitorStatisticService; 24 | private readonly UserService userService; 25 | 26 | public AdminController(ApplicationDbContext dbContext, IMemoryCache memoryCache, GenerationService generationService, GenerationService generateService, VisitorStatisticService visitorStatisticService, UserService userService) 27 | { 28 | this.dbContext = dbContext; 29 | this.memoryCache = memoryCache; 30 | this.generationService = generationService; 31 | this.generateService = generateService; 32 | this.visitorStatisticService = visitorStatisticService; 33 | this.userService = userService; 34 | } 35 | 36 | [AllowAnonymous] 37 | [HttpPost("authenticate")] 38 | public IActionResult Authenticate([FromBody]AuthenticateModel model) 39 | { 40 | var user = userService.Authenticate(model.Username, model.Password); 41 | 42 | if (user == null) 43 | return BadRequest(new { message = "Username or password is incorrect" }); 44 | 45 | return Ok(user); 46 | } 47 | 48 | [HttpPost("change-password")] 49 | public IActionResult Authenticate([FromBody]ChangePasswordModel model) 50 | { 51 | var user = userService.Authenticate(model.Username, model.Password); 52 | 53 | if (user == null) 54 | return BadRequest(new { message = "Username or password is incorrect" }); 55 | 56 | userService.SetPassword(user, model.NewPassword); 57 | 58 | return Ok(user); 59 | } 60 | 61 | 62 | 63 | [HttpGet] 64 | public IActionResult GetDashboard() 65 | { 66 | return GetFile("index.html"); 67 | } 68 | 69 | [HttpGet] 70 | [Route("{fileName}")] 71 | public IActionResult GetFile(string fileName) 72 | { 73 | List visitors = dbContext.Visitors.Include(visitor => visitor.Visits).ToList(); 74 | visitors.AddRange(visitorStatisticService.GetCachedVisitors()); 75 | if (!visitors.Any()) 76 | return NotFound(); 77 | JsonDocument doc = JsonDocument.Parse("{\"visitors\":" + JsonSerializer.Serialize(visitors, new JsonSerializerOptions{ 78 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase 79 | }) + "}"); 80 | JsonElement json = doc.RootElement; 81 | string content = ""; 82 | string contentType; 83 | var settings = new GenerateSettings { 84 | Frontend = "bootstrap", 85 | IsReadonly = true, 86 | Color = "42b983" 87 | }; 88 | var request = new GenerateRequest { 89 | Settings = settings, 90 | Data = json 91 | }; 92 | var generator = new VueStartGenerator(request, memoryCache); 93 | string artifactId = generateService.Generate(request, "Dashboard", generator.Id, true, out string appjs, out string indexhtml, true); 94 | 95 | if (string.IsNullOrWhiteSpace(fileName)) 96 | fileName = "index.html"; 97 | 98 | switch (fileName.Split('.').LastOrDefault()) 99 | { 100 | case "js": 101 | contentType = "application/javascript"; 102 | content = appjs; 103 | break; 104 | case "html": 105 | contentType = "text/html"; 106 | content = indexhtml; 107 | break; 108 | default: 109 | contentType = "text/plain"; 110 | break; 111 | } 112 | var cd = new System.Net.Mime.ContentDisposition 113 | { 114 | FileName = fileName, 115 | Inline = true 116 | }; 117 | Response.Headers.Add("Content-Disposition", cd.ToString()); 118 | Response.Headers.Add("X-Content-Type-Options", "nosniff"); 119 | return File(Encoding.UTF8.GetBytes(content), contentType); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Server/Controllers/ClientErrorsController.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Extensions.Configuration; 4 | using System.Text.Json; 5 | using System; 6 | 7 | namespace VueStart.Controllers 8 | { 9 | [ApiController] 10 | [Route("api/errors")] 11 | public class ClientErrorsController : ControllerBase 12 | { 13 | private readonly ApplicationDbContext dbContext; 14 | 15 | public ClientErrorsController(ApplicationDbContext dbContext) 16 | { 17 | this.dbContext = dbContext; 18 | } 19 | 20 | [HttpPost] 21 | [Route("")] 22 | public IActionResult LogError([FromBody] JsonElement data) { 23 | dbContext.ClientErrors.Add(new ClientError{ 24 | DateTime = DateTime.UtcNow, 25 | UserAgent = Request.Headers["User-Agent"].FirstOrDefault(), 26 | Data = data.ToString() 27 | }); 28 | dbContext.SaveChanges(); 29 | return Ok(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Server/Controllers/DownloadController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Microsoft.AspNetCore.Mvc; 4 | using System.IO; 5 | using System.Text.Json; 6 | using System.IO.Compression; 7 | using VueStart.Services; 8 | using VueStart.Data; 9 | using System.Threading.Channels; 10 | 11 | namespace VueStart.Controllers 12 | { 13 | [ApiController] 14 | [Route("api/download")] 15 | public class DownloadController : ControllerBase 16 | { 17 | private readonly GenerationService generateService; 18 | private readonly Channel eventChannel; 19 | 20 | public DownloadController(GenerationService generateService, Channel eventChannel) 21 | { 22 | this.generateService = generateService; 23 | this.eventChannel = eventChannel; 24 | } 25 | 26 | private static string ToUpperFirst(string type) 27 | { 28 | return Char.ToUpper(type.First()) + type.Substring(1); 29 | } 30 | 31 | [HttpPost] 32 | public IActionResult DownloadEditor([FromBody] GenerateRequest request) 33 | { 34 | var layout = request.Settings.IsReadonly; 35 | var json = request.Data; 36 | try { 37 | var frontend = request.Settings.Frontend.ToFrontendType(); 38 | if (frontend == Frontend.None) 39 | return NotFound(); 40 | eventChannel.Writer.WriteAsync(new EventData(Request.HttpContext, request, ActionType.Download)); 41 | var memoryStream = CreateZipStream(request, "DataTable"); 42 | return File(memoryStream, "application/zip", $"{layout}.zip"); 43 | } catch (FormatException e) { 44 | return BadRequest(new { error = e.Message }); 45 | } 46 | } 47 | 48 | private MemoryStream CreateZipStream(GenerateRequest request, string title) 49 | { 50 | var memoryStream = new MemoryStream(); 51 | generateService.Generate(request, title, "", true, out string appjs, out string indexhtml); 52 | using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) 53 | { 54 | AddEntry(archive, appjs, "app.js"); 55 | AddEntry(archive, indexhtml, "index.html"); 56 | } 57 | memoryStream.Position = 0; 58 | return memoryStream; 59 | } 60 | 61 | private static void AddEntry(ZipArchive archive, string content, string fileName) 62 | { 63 | var entry = archive.CreateEntry(fileName); 64 | 65 | using (var entryStream = entry.Open()) 66 | using (var streamWriter = new StreamWriter(entryStream)) 67 | { 68 | streamWriter.Write(content); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Server/Controllers/FilesController.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.AspNetCore.Mvc; 3 | using BootGen.Core; 4 | using System.IO; 5 | using Microsoft.Extensions.Caching.Memory; 6 | using System.Text; 7 | 8 | namespace VueStart.Controllers 9 | { 10 | [ApiController] 11 | [Route("api/files")] 12 | public class FilesController : ControllerBase 13 | { 14 | private readonly IMemoryCache memoryCache; 15 | public FilesController(IMemoryCache memoryCache) 16 | { 17 | this.memoryCache = memoryCache; 18 | } 19 | 20 | [HttpGet] 21 | [Route("{id}/{filename}")] 22 | public IActionResult ServeFile([FromRoute] string id, [FromRoute] string filename, [FromQuery] bool display) { 23 | if (string.IsNullOrWhiteSpace(filename)) 24 | filename = "index.html"; 25 | string key = $"{id}/{filename}"; 26 | if (display) 27 | key += "_display"; 28 | string content; 29 | if (!memoryCache.TryGetValue(key, out content)) 30 | return NotFound(); 31 | 32 | string contentType; 33 | 34 | switch (filename.Split('.').LastOrDefault()) 35 | { 36 | case "js": 37 | contentType = "application/javascript"; 38 | break; 39 | case "html": 40 | contentType = "text/html"; 41 | break; 42 | default: 43 | contentType = "text/plain"; 44 | break; 45 | } 46 | var cd = new System.Net.Mime.ContentDisposition 47 | { 48 | FileName = filename, 49 | Inline = true 50 | }; 51 | Response.Headers.Add("Content-Disposition", cd.ToString()); 52 | Response.Headers.Add("X-Content-Type-Options", "nosniff"); 53 | return File(Encoding.UTF8.GetBytes(content), contentType); 54 | } 55 | 56 | private static VirtualDisk Load(string path) 57 | { 58 | var templates = new VirtualDisk(); 59 | foreach (var file in Directory.EnumerateFiles(path)) 60 | { 61 | templates.Files.Add(new VirtualFile 62 | { 63 | Name = Path.GetFileName(file), 64 | Path = "", 65 | Content = System.IO.File.ReadAllText(file) 66 | }); 67 | } 68 | 69 | return templates; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Server/Controllers/GenerationController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Microsoft.AspNetCore.Mvc; 4 | using System.IO; 5 | using System.Text.Json; 6 | using Microsoft.Extensions.Caching.Memory; 7 | using System.IO.Compression; 8 | using VueStart.Services; 9 | using VueStart.Data; 10 | using BootGen.Core; 11 | using System.Threading.Channels; 12 | 13 | namespace VueStart.Controllers 14 | { 15 | [ApiController] 16 | [Route("api/generate")] 17 | public class GenerationController : ControllerBase 18 | { 19 | private readonly GenerationService generationService; 20 | private readonly Channel eventChannel; 21 | 22 | public GenerationController(GenerationService generateService, Channel eventChannel) 23 | { 24 | this.generationService = generateService; 25 | this.eventChannel = eventChannel; 26 | } 27 | 28 | [HttpPost] 29 | public IActionResult Generate([FromBody] GenerateRequest request) 30 | { 31 | var eventData = new EventData(Request.HttpContext, request, ActionType.Generate); 32 | if (request.Data.ValueKind != JsonValueKind.Object) { 33 | eventData.Error = true; 34 | eventChannel.Writer.WriteAsync(eventData); 35 | return BadRequest(new { error = "The root element must be an object!", fixable = true }); 36 | } 37 | foreach (var property in request.Data.EnumerateObject()) { 38 | if (property.Value.ValueKind != JsonValueKind.Object && property.Value.ValueKind != JsonValueKind.Array) { 39 | eventData.Error = true; 40 | eventChannel.Writer.WriteAsync(eventData); 41 | return BadRequest(new { error = "Properties of the root element must be an objects or arrays!", fixable = false }); 42 | } 43 | } 44 | try { 45 | eventChannel.Writer.WriteAsync(eventData); 46 | var result = generationService.GenerateToCache(request, "DataTable"); 47 | return Ok(result); 48 | } catch (FormatException e) { 49 | return BadRequest(new { error = e.Message, fixable = false }); 50 | } catch (NamingException e) { 51 | return BadRequest(new { error = e.Message, fixable = true }); 52 | } 53 | } 54 | 55 | [HttpPost] 56 | [Route("fix")] 57 | public IActionResult Fix([FromBody] JsonElement json) 58 | { 59 | return Ok(generationService.Fix(json)); 60 | } 61 | private static string ToUpperFirst(string type) 62 | { 63 | return Char.ToUpper(type.First()) + type.Substring(1); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Server/Controllers/ShareController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text.Json; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace VueStart.Controllers; 7 | 8 | [ApiController] 9 | [Route("api/share")] 10 | public class ShareController : ControllerBase 11 | { 12 | private readonly ApplicationDbContext dbContext; 13 | 14 | public ShareController(ApplicationDbContext dbContext) 15 | { 16 | this.dbContext = dbContext; 17 | } 18 | private static int StringHash(string text) 19 | { 20 | unchecked 21 | { 22 | int hash = 23; 23 | foreach (char c in text) 24 | { 25 | hash = hash * 31 + c; 26 | } 27 | return hash; 28 | } 29 | } 30 | 31 | [HttpPost] 32 | public IActionResult Save([FromBody] GenerateRequest request) 33 | { 34 | string requestAsString = JsonSerializer.Serialize(request, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); 35 | int hash = StringHash(requestAsString); 36 | var link = dbContext.ShareableLinks.FirstOrDefault(r => r.Hash == hash); 37 | if(link != null) { 38 | return Ok(new { hash = hash }); 39 | } 40 | var generateRequest = JsonSerializer.Deserialize(requestAsString); 41 | dbContext.ShareableLinks.Add(new ShareableLink { Hash = hash, GenerateRequest = generateRequest, FirstUse = DateTime.UtcNow, LastUse = DateTime.UtcNow, Count = 0}); 42 | dbContext.SaveChanges(); 43 | return Ok(new { hash = hash }); 44 | } 45 | 46 | [HttpGet] 47 | [Route("{hash}")] 48 | public IActionResult Load([FromRoute] int hash) 49 | { 50 | var link = dbContext.ShareableLinks.FirstOrDefault(r => r.Hash == hash); 51 | if (link == null) 52 | return NotFound(); 53 | link.LastUse = DateTime.UtcNow; 54 | link.Count += 1; 55 | dbContext.SaveChanges(); 56 | return Ok(link); 57 | } 58 | } -------------------------------------------------------------------------------- /Server/Data/ActionType.cs: -------------------------------------------------------------------------------- 1 | namespace VueStart.Data 2 | { 3 | public enum ActionType { 4 | Generate, 5 | Download 6 | } 7 | } -------------------------------------------------------------------------------- /Server/Data/Frontend.cs: -------------------------------------------------------------------------------- 1 | namespace VueStart.Data 2 | { 3 | public enum Frontend { 4 | None, 5 | Bootstrap, 6 | Tailwind, 7 | Vanilla 8 | } 9 | 10 | public static class FrontendTypeExtensions { 11 | public static Frontend ToFrontendType(this string type) 12 | { 13 | switch (type) 14 | { 15 | case "bootstrap": 16 | return Frontend.Bootstrap; 17 | case "tailwind": 18 | return Frontend.Tailwind; 19 | case "vanilla": 20 | return Frontend.Vanilla; 21 | default: 22 | return Frontend.None; 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /Server/Entities/ClientError.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace VueStart 4 | { 5 | public class ClientError 6 | { 7 | public int Id { get; set; } 8 | public DateTime DateTime { get; init; } 9 | public string UserAgent { get; set; } 10 | public string Data { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /Server/Entities/InputData.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text.Json; 5 | 6 | namespace VueStart 7 | { 8 | 9 | public class InputData 10 | { 11 | public int Id { get; set; } 12 | public int Hash { get; set; } 13 | public JsonElement Data { get; set; } 14 | public DateTime FirstUse { get; set; } 15 | public DateTime LastUse { get; set; } 16 | public bool Error { get; set; } 17 | public List StatisticRecords { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /Server/Entities/ServerError.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace VueStart 4 | { 5 | public class ServerError 6 | { 7 | public int Id { get; set; } 8 | public DateTime DateTime { get; init; } 9 | public string Message { get; init; } 10 | public string StackTrace { get; init; } 11 | public string File { get; init; } 12 | public int Line { get; init; } 13 | public string Source { get; init; } 14 | public int HResult { get; init; } 15 | public string Data { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /Server/Entities/ShareableLink.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | 4 | namespace VueStart 5 | { 6 | public class ShareableLink { 7 | public int Id { get; set; } 8 | public int Hash { get; set; } 9 | public JsonElement GenerateRequest { get; set; } 10 | public DateTime FirstUse { get; set; } 11 | public DateTime LastUse { get; set; } 12 | public int Count { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /Server/Entities/StatisticRecord.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace VueStart 7 | { 8 | 9 | public class StatisticRecord 10 | { 11 | public int Id { get; set; } 12 | public int InputDataId { get; set; } 13 | 14 | [JsonIgnore] 15 | public InputData InputData { get; set; } 16 | public bool Readonly { get; set; } 17 | public bool Download { get; set; } 18 | public int BootstrapCount { get; set; } 19 | public int TailwindCount { get; set; } 20 | public int VanillaCount { get; set; } 21 | 22 | internal bool IsSameKind(StatisticRecord record) 23 | { 24 | return Readonly == record.Readonly && Download == record.Download; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Server/Entities/User.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Text.Json.Serialization; 3 | 4 | public class User 5 | { 6 | public int Id { get; set; } 7 | public string Username { get; set; } 8 | 9 | [JsonIgnore] 10 | public string PasswordHash { get; set; } 11 | } -------------------------------------------------------------------------------- /Server/Entities/Visit.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace VueStart 4 | { 5 | public class Visit { 6 | public int Id { get; set; } 7 | public DateTime Start { get; init; } 8 | public DateTime End { get; set; } 9 | public int Count { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /Server/Entities/Visitor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace VueStart 6 | { 7 | public class Visitor { 8 | 9 | public int Id { get; set; } 10 | public string Token { get; init; } 11 | public string Citation { get; init; } 12 | public DateTime FirstVisit { get; init; } 13 | public string Country { get; set; } 14 | public string Region { get; set; } 15 | public string City { get; set; } 16 | [JsonIgnore] 17 | public string UserAgent { get; set; } 18 | public string OSFamily { get; set; } 19 | public string OSMajor { get; set; } 20 | public string OSMinor { get; set; } 21 | public string DeviceFamily { get; set; } 22 | public string DeviceBrand { get; set; } 23 | public string BrowserFamily { get; set; } 24 | public string BrowserMajor { get; set; } 25 | public string BrowserMinor { get; set; } 26 | public string DeviceModel { get; set; } 27 | public List Visits { get; set; } 28 | } 29 | } -------------------------------------------------------------------------------- /Server/Migrations/20220527143241_ShareableLinkSettings.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace VueStart.Migrations 6 | { 7 | public partial class ShareableLinkSettings : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.DropColumn( 12 | name: "Color", 13 | table: "ShareableLinks"); 14 | 15 | migrationBuilder.DropColumn( 16 | name: "Editable", 17 | table: "ShareableLinks"); 18 | 19 | migrationBuilder.DropColumn( 20 | name: "FrontendType", 21 | table: "ShareableLinks"); 22 | 23 | migrationBuilder.RenameColumn( 24 | name: "Json", 25 | table: "ShareableLinks", 26 | newName: "GenerateRequest"); 27 | 28 | migrationBuilder.UpdateData( 29 | table: "Users", 30 | keyColumn: "Id", 31 | keyValue: 1, 32 | column: "PasswordHash", 33 | value: "AQAAAAEAACcQAAAAEIyo9Z9EEDv7Dmyv5tqUNQ6pgWN6HV2kzycBoFanFRBjNxClWbdgQe2BSCq5ldV4Mw=="); 34 | } 35 | 36 | protected override void Down(MigrationBuilder migrationBuilder) 37 | { 38 | migrationBuilder.RenameColumn( 39 | name: "GenerateRequest", 40 | table: "ShareableLinks", 41 | newName: "Json"); 42 | 43 | migrationBuilder.AddColumn( 44 | name: "Color", 45 | table: "ShareableLinks", 46 | type: "text", 47 | nullable: true); 48 | 49 | migrationBuilder.AddColumn( 50 | name: "Editable", 51 | table: "ShareableLinks", 52 | type: "boolean", 53 | nullable: false, 54 | defaultValue: false); 55 | 56 | migrationBuilder.AddColumn( 57 | name: "FrontendType", 58 | table: "ShareableLinks", 59 | type: "text", 60 | nullable: true); 61 | 62 | migrationBuilder.UpdateData( 63 | table: "Users", 64 | keyColumn: "Id", 65 | keyValue: 1, 66 | column: "PasswordHash", 67 | value: "AQAAAAEAACcQAAAAEP2ySbRDxPLVAzU13JfKBPpxhk2Vjh0LzEe29VP+MuGnKdzeD8BuGR5Dd1oo7gFzzA=="); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Server/Models/AuthenticateModel.cs: -------------------------------------------------------------------------------- 1 | namespace VueStart; 2 | 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | public class AuthenticateModel 6 | { 7 | [Required] 8 | public string Username { get; set; } 9 | 10 | [Required] 11 | public string Password { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /Server/Models/ChangePasswordModel.cs: -------------------------------------------------------------------------------- 1 | namespace VueStart; 2 | 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | public class ChangePasswordModel 6 | { 7 | [Required] 8 | public string Username { get; set; } 9 | 10 | [Required] 11 | public string Password { get; set; } 12 | 13 | [Required] 14 | public string NewPassword { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /Server/Models/EventData.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.AspNetCore.Http; 3 | using VueStart.Data; 4 | 5 | namespace VueStart; 6 | 7 | public class EventData 8 | { 9 | public EventData(HttpContext context, GenerateRequest request, ActionType actionType, bool error = false) 10 | { 11 | 12 | UaString = context.Request.Headers["User-Agent"].FirstOrDefault(); 13 | IdToken = context.Request.Headers["idtoken"].FirstOrDefault(); 14 | Citation = context.Request.Headers["citation"].FirstOrDefault(); 15 | RemoteIpAddress = context.Connection.RemoteIpAddress.ToString(); 16 | Request = request; 17 | ActionType = actionType; 18 | Error = error; 19 | } 20 | 21 | public string UaString { get; set; } 22 | public string IdToken { get; set; } 23 | public string Citation { get; set; } 24 | public string RemoteIpAddress { get; set; } 25 | public GenerateRequest Request { get; set; } 26 | public ActionType ActionType { get; set; } 27 | public bool Error { get; set; } 28 | } 29 | -------------------------------------------------------------------------------- /Server/Models/GenerateRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | namespace VueStart; 4 | public class GenerateRequest 5 | { 6 | public GenerateSettings Settings { get; set; } 7 | public JsonElement Data { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /Server/Models/GenerateSettings.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace VueStart; 4 | 5 | public class GenerateSettings 6 | { 7 | public string Frontend { get; set; } 8 | public bool IsReadonly { get; set; } 9 | public string Color { get; set; } 10 | public List ClassSettings { get; set; } 11 | } -------------------------------------------------------------------------------- /Server/Models/VisitorData.cs: -------------------------------------------------------------------------------- 1 | namespace VueStart; 2 | public struct VisitorData 3 | { 4 | public Visitor Visitor { get; init; } 5 | public string Ip { get; init; } 6 | } -------------------------------------------------------------------------------- /Server/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace VueStart; 11 | 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) => 20 | Host.CreateDefaultBuilder(args) 21 | .ConfigureWebHostDefaults(webBuilder => 22 | { 23 | webBuilder.UseStartup(); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /Server/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:63628", 8 | "sslPort": 44356 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "swagger", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "VueStart": { 21 | "commandName": "Project", 22 | "dotnetRunMessages": "true", 23 | "launchBrowser": true, 24 | "launchUrl": "swagger", 25 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Server/Services/ErrorHandlerService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace VueStart 5 | { 6 | class ErrorHandlerService 7 | { 8 | private readonly ApplicationDbContext dbContext; 9 | 10 | public ErrorHandlerService(ApplicationDbContext dbContext) 11 | { 12 | this.dbContext = dbContext; 13 | } 14 | 15 | public void OnException(Exception e, string data) 16 | { 17 | ServerError error = e.InnerException == null ? ExceptionToError(e) : ExceptionToError(e.InnerException); 18 | error.Data = data; 19 | dbContext.ServerErrors.Add(error); 20 | dbContext.SaveChanges(); 21 | } 22 | 23 | private static ServerError ExceptionToError(Exception e) 24 | { 25 | var st = new StackTrace(e, true); 26 | var frame = st.GetFrame(0); 27 | return new ServerError { 28 | DateTime = DateTime.UtcNow, 29 | Message = e.Message, 30 | StackTrace = e.StackTrace, 31 | File = frame.GetFileName(), 32 | Line = frame.GetFileLineNumber(), 33 | Source = e.Source, 34 | HResult = e.HResult 35 | }; 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /Server/Services/GenerationService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text.Json; 5 | using BootGen.Core; 6 | using Microsoft.Extensions.Caching.Memory; 7 | using Newtonsoft.Json; 8 | using Newtonsoft.Json.Linq; 9 | using System.Drawing; 10 | using System.Linq; 11 | 12 | namespace VueStart.Services; 13 | public class GenerationService 14 | { 15 | private readonly IMemoryCache memoryCache; 16 | 17 | public GenerationService(IMemoryCache memoryCache) 18 | { 19 | this.memoryCache = memoryCache; 20 | } 21 | public string Generate(GenerateRequest request, string title, string generatedId, bool forDownload, out string appjs, out string indexhtml, bool isAdmin = false) 22 | { 23 | var generator = new VueStartGenerator(request, memoryCache); 24 | Generate(request, title, generatedId, forDownload, out appjs, out indexhtml, generator, isAdmin); 25 | return generator.Id; 26 | } 27 | 28 | private static void Generate(GenerateRequest request, string title, string generatedId, bool forDownload, out string appjs, out string indexhtml, VueStartGenerator generator, bool isAdmin = false) 29 | { 30 | string layout = request.Settings.IsReadonly ? "table" : "table-editable"; 31 | string templateFileName = $"{request.Settings.Frontend}-{layout}.sbn"; 32 | var jsParameters = new Dictionary { 33 | {"classes", generator.DataModel.CommonClasses} 34 | }; 35 | if (forDownload) 36 | jsParameters.Add("input", request.Data.ToString()); 37 | else 38 | jsParameters.Add("generated_id", $"{generatedId}"); 39 | 40 | appjs = generator.Render(templateFileName, jsParameters); 41 | var indexParameters = new Dictionary { 42 | {"title", $"{title}"} 43 | }; 44 | if (isAdmin) 45 | indexParameters.Add("base_url", "/admin/"); 46 | else if (!forDownload) { 47 | indexParameters.Add("base_url", $"/api/files/{generator.Id}/"); 48 | } 49 | indexParameters.Add("color", request.Settings.Color); 50 | if (Brightness(ColorTranslator.FromHtml($"#{request.Settings.Color}")) > 170) 51 | { 52 | indexParameters.Add("text_color", "2c3e50"); 53 | } 54 | else 55 | { 56 | indexParameters.Add("text_color", "ffffff"); 57 | } 58 | indexParameters.Add("is_readonly", request.Settings.IsReadonly); 59 | indexhtml = generator.Render($"{request.Settings.Frontend}-index.sbn", indexParameters); 60 | } 61 | 62 | private static int Brightness(Color c) 63 | { 64 | return (int)Math.Sqrt( 65 | c.R * c.R * .241 + 66 | c.G * c.G * .691 + 67 | c.B * c.B * .068); 68 | } 69 | 70 | public JsonElement Fix(JsonElement json) 71 | { 72 | if (json.ValueKind == JsonValueKind.Array) { 73 | return JsonDocument.Parse($"{{\"items\": {json} }}").RootElement; 74 | } 75 | var jObject = JsonConvert.DeserializeObject(json.ToString(), new JsonSerializerSettings 76 | { 77 | DateFormatString = "yyyy-MM-ddTHH:mm", 78 | }); 79 | try 80 | { 81 | var dataModel = new DataModel 82 | { 83 | TypeToString = TypeScriptGenerator.ToTypeScriptType 84 | }; 85 | dataModel.LoadRootObject("App", jObject); 86 | } 87 | catch (NamingException e) 88 | { 89 | string jsonString; 90 | if (e.IsArray) 91 | jsonString = jObject.RenamingArrays(e.ActualName, e.SuggestedName).ToString(); 92 | else 93 | jsonString = jObject.RenamingObjects(e.ActualName, e.SuggestedName).ToString(); 94 | return JsonDocument.Parse(jsonString).RootElement; 95 | } 96 | return json; 97 | } 98 | 99 | public GenerationResult GenerateToCache(GenerateRequest request, string title) 100 | { 101 | var generator = new VueStartGenerator(request, memoryCache); 102 | Generate(request, title, generator.Id, false, out var appjs, out var indexhtml, generator); 103 | memoryCache.Set($"{generator.Id}/app.js", Minify(appjs), TimeSpan.FromMinutes(30)); 104 | memoryCache.Set($"{generator.Id}/index.html", Minify(indexhtml), TimeSpan.FromMinutes(30)); 105 | Generate(request, title, generator.Id, true, out var pAppjs, out var pIndexhtml, generator); 106 | memoryCache.Set($"{generator.Id}/app.js_display", pAppjs, TimeSpan.FromMinutes(30)); 107 | memoryCache.Set($"{generator.Id}/index.html_display", pIndexhtml, TimeSpan.FromMinutes(30)); 108 | var result = new GenerationResult { 109 | Warnings = new List() 110 | }; 111 | var warningData = generator.DataModel.Warnings; 112 | foreach (var key in warningData.Keys) { 113 | switch (key) { 114 | case WarningType.EmptyType: 115 | { 116 | HashSet names = warningData[WarningType.EmptyType]; 117 | if (names.Count == 1) 118 | result.Warnings.Add($"Empty types are not supported, and are omitted. The type \"{names.First()}\" has no properties."); 119 | else 120 | result.Warnings.Add("Empty types are not supported, and are omitted. The following types have no properties: " + names.Aggregate((a, b) => $"{a}, {b}")); 121 | } 122 | break; 123 | case WarningType.NestedArray: 124 | { 125 | HashSet names = warningData[WarningType.NestedArray]; 126 | if (names.Count == 1) 127 | result.Warnings.Add($"Nested arrays are not supported. The property \"{names.First()}\" is omitted."); 128 | else 129 | result.Warnings.Add("Nested arrays are not supported. The following properties are omitted: " + names.Aggregate((a, b) => $"{a}, {b}")); 130 | } 131 | break; 132 | case WarningType.PrimitiveArrayElement: 133 | { 134 | HashSet names = warningData[WarningType.PrimitiveArrayElement]; 135 | if (names.Count == 1) 136 | result.Warnings.Add($"Arrays with primitive elements are not supported. The property \"{names.First()}\" is omitted."); 137 | else 138 | result.Warnings.Add("Arrays with primitive elements are not supported. The following properties are omitted: " + names.Aggregate((a, b) => $"{a}, {b}")); 139 | } 140 | break; 141 | case WarningType.PrimitiveRoot: 142 | { 143 | HashSet names = warningData[WarningType.PrimitiveRoot]; 144 | if (names.Count == 1) 145 | result.Warnings.Add($"Root elements must be arrays or objects. The property \"{names.First()}\" is omitted."); 146 | else 147 | result.Warnings.Add("Root elements must be arrays or objects. The following properties are omitted: " + names.Aggregate((a, b) => $"{a}, {b}")); 148 | } 149 | break; 150 | } 151 | } 152 | result.Id = generator.Id; 153 | result.Settings = generator.DataModel.GetSettings().Select(ClassSettings.FromBootGenClassSettings).ToList(); 154 | return result; 155 | } 156 | 157 | private string Minify(string value) 158 | { 159 | #if DEBUG 160 | return value; 161 | #else 162 | value = value.Replace("\n", " "); 163 | value = value.Replace("\r", " "); 164 | value = value.Replace("\t", " "); 165 | int length; 166 | do { 167 | length = value.Length; 168 | value = value.Replace(" ", " "); 169 | } while(value.Length != length); 170 | 171 | return value; 172 | #endif 173 | } 174 | } 175 | 176 | public class GenerationResult 177 | { 178 | public string Id { get; set; } 179 | public List Warnings { get; set; } 180 | public List Settings { get; set; } 181 | } 182 | 183 | struct TemplateCacheKey 184 | { 185 | public string Path { get; init; } 186 | } 187 | 188 | class VueStartGenerator 189 | { 190 | public DataModel DataModel { get; } 191 | public string Id { get; } 192 | private readonly TypeScriptGenerator generator; 193 | 194 | private IMemoryCache memoryCache; 195 | public VueStartGenerator(GenerateRequest request, IMemoryCache memoryCache) 196 | { 197 | this.memoryCache = memoryCache; 198 | DataModel = new DataModel 199 | { 200 | TypeToString = TypeScriptGenerator.ToTypeScriptType, 201 | GenerateIds = false 202 | }; 203 | var jObject = JsonConvert.DeserializeObject(request.Data.ToString(), new JsonSerializerSettings 204 | { 205 | DateFormatString = "yyyy-MM-ddTHH:mm", 206 | }); 207 | DataModel.LoadRootObject("App", jObject, request.Settings.ClassSettings.Select(s => s.ToBootGenClassSettings()).ToList()); 208 | Id = Guid.NewGuid().ToString(); 209 | generator = new TypeScriptGenerator(null); 210 | generator.Templates = Load("templates"); 211 | this.memoryCache = memoryCache; 212 | } 213 | 214 | private VirtualDisk Load(string path) 215 | { 216 | return memoryCache.GetOrCreate(new TemplateCacheKey { Path = path }, entry => 217 | { 218 | var templates = new VirtualDisk(); 219 | foreach (var file in Directory.EnumerateFiles(path)) 220 | { 221 | templates.Files.Add(new VirtualFile 222 | { 223 | Name = Path.GetFileName(file), 224 | Path = "", 225 | Content = System.IO.File.ReadAllText(file) 226 | }); 227 | } 228 | return templates; 229 | }); 230 | } 231 | 232 | internal string Render(string templateFileName, Dictionary parameters) 233 | { 234 | return generator.Render(templateFileName, parameters); 235 | } 236 | } -------------------------------------------------------------------------------- /Server/Services/GeoLocationService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Microsoft.Extensions.Configuration; 8 | using Newtonsoft.Json.Linq; 9 | 10 | namespace VueStart.Services; 11 | 12 | public class GeoLocationService 13 | { 14 | private readonly IConfiguration configuration; 15 | 16 | public GeoLocationService(IConfiguration configuration) 17 | { 18 | this.configuration = configuration; 19 | } 20 | 21 | public async Task SetGeoLocation(List data) 22 | { 23 | var ipInfotoken = configuration.GetValue("IpInfoToken"); 24 | if (string.IsNullOrWhiteSpace(ipInfotoken)) 25 | return; 26 | 27 | using var client = new HttpClient(); 28 | string ipListString = data.Select(d => $"\"{d.Ip}\"").Aggregate((a, b) => $"{a}, {b}"); 29 | string stringData = $"[{ipListString}]"; 30 | var content = new StringContent(stringData, Encoding.UTF8, "application/json"); 31 | 32 | using var response = await client.PostAsync($"https://ipinfo.io/batch?token={ipInfotoken}", content); 33 | using var reader = new StreamReader(response.Content.ReadAsStream()); 34 | 35 | var jsonString = reader.ReadToEnd(); 36 | var jObject = JObject.Parse(jsonString); 37 | foreach (var item in data) { 38 | var obj = jObject.GetValue(item.Ip) as JObject; 39 | if (obj != null) { 40 | SetLocation(item.Visitor, obj); 41 | } 42 | } 43 | } 44 | 45 | private static void SetLocation(Visitor visitor, JObject jObject) 46 | { 47 | visitor.Country = jObject.GetValue("country")?.ToString(); 48 | visitor.Region = jObject.GetValue("region")?.ToString(); 49 | visitor.City = jObject.GetValue("city")?.ToString(); 50 | } 51 | } -------------------------------------------------------------------------------- /Server/Services/InputStatisticService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.Extensions.Logging; 5 | using VueStart.Data; 6 | 7 | namespace VueStart.Services; 8 | 9 | public class InputStatisticService 10 | { 11 | private readonly ILogger logger; 12 | 13 | private List InputData { get; } = new List(); 14 | 15 | public InputStatisticService(ILogger logger) 16 | { 17 | this.logger = logger; 18 | } 19 | 20 | private static int StringHash(string text) 21 | { 22 | unchecked 23 | { 24 | int hash = 23; 25 | foreach (char c in text) 26 | { 27 | hash = hash * 31 + c; 28 | } 29 | return hash; 30 | } 31 | } 32 | 33 | 34 | public void StoreStatisticRecord(GenerateRequest request, ActionType actionType, bool error) 35 | { 36 | int hash = StringHash(request.Data.ToString()); 37 | var inputData = InputData.FirstOrDefault(r => r.Hash == hash); 38 | var record = new StatisticRecord { 39 | Download = actionType == ActionType.Download, 40 | Readonly = request.Settings.IsReadonly 41 | }; 42 | if (inputData == null) 43 | { 44 | inputData = new InputData { 45 | Hash = hash, 46 | Data = request.Data, 47 | FirstUse = DateTime.UtcNow, 48 | LastUse = DateTime.UtcNow, 49 | Error = error, 50 | StatisticRecords = new List() { record } 51 | }; 52 | InputData.Add(inputData); 53 | } else { 54 | var existingRecord = inputData.StatisticRecords.FirstOrDefault(r => r.IsSameKind(record)); 55 | if (existingRecord != null) 56 | { 57 | record = existingRecord; 58 | } else { 59 | inputData.StatisticRecords.Add(record); 60 | } 61 | } 62 | UpdateRecord(record, request.Settings.Frontend.ToFrontendType()); 63 | } 64 | 65 | private void UpdateRecord(StatisticRecord record, Frontend cssType) 66 | { 67 | switch (cssType) 68 | { 69 | case Frontend.Bootstrap: 70 | record.BootstrapCount += 1; 71 | break; 72 | case Frontend.Tailwind: 73 | record.TailwindCount += 1; 74 | break; 75 | case Frontend.Vanilla: 76 | record.VanillaCount += 1; 77 | break; 78 | } 79 | } 80 | 81 | public void SaveRecords(ApplicationDbContext dbContext) 82 | { 83 | logger.Log(LogLevel.Information, $"Saving {InputData.Count} input records."); 84 | foreach (var inputData in InputData) 85 | { 86 | var existingInputData = dbContext.InputData.FirstOrDefault(r => r.Hash == inputData.Hash); 87 | if (existingInputData != null) 88 | { 89 | dbContext.Entry(existingInputData).Collection(i => i.StatisticRecords).Load(); 90 | existingInputData.LastUse = DateTime.UtcNow; 91 | foreach(var record in inputData.StatisticRecords) { 92 | var existingRecord = existingInputData.StatisticRecords.FirstOrDefault(r => r.IsSameKind(record)); 93 | if (existingRecord != null) { 94 | existingRecord.BootstrapCount += record.BootstrapCount; 95 | existingRecord.TailwindCount += record.TailwindCount; 96 | existingRecord.VanillaCount += record.VanillaCount; 97 | } else { 98 | existingInputData.StatisticRecords.Add(record); 99 | } 100 | } 101 | } 102 | else 103 | { 104 | dbContext.InputData.Add(inputData); 105 | } 106 | } 107 | InputData.Clear(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Server/Services/StatisticsService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Channels; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace VueStart.Services; 10 | 11 | public class StatisticsService : BackgroundService 12 | { 13 | private readonly VisitorStatisticService visitorStatisticService; 14 | private readonly InputStatisticService inputStatisticService; 15 | private readonly IServiceProvider serviceProvider; 16 | private readonly Channel eventChannel; 17 | private readonly ILogger logger; 18 | private int currentDay = -1; 19 | private int currentPeriod = -1; 20 | public StatisticsService(VisitorStatisticService visitorStatisticService, InputStatisticService inputStatisticService, IServiceProvider serviceProvider, Channel eventChannel, ILogger logger, IHostApplicationLifetime applicationLifetime) 21 | { 22 | this.visitorStatisticService = visitorStatisticService; 23 | this.inputStatisticService = inputStatisticService; 24 | this.serviceProvider = serviceProvider; 25 | this.eventChannel = eventChannel; 26 | this.logger = logger; 27 | applicationLifetime.ApplicationStopping.Register(() => { 28 | logger.Log(LogLevel.Information, "Application stopping detected."); 29 | SaveData().Wait(); 30 | }); 31 | } 32 | 33 | private async Task OnEvent(EventData eventData) 34 | { 35 | try { 36 | var now = DateTime.UtcNow; 37 | #if DEBUG 38 | var periodLengthInMinutes = 1; 39 | #else 40 | var periodLengthInMinutes = 15; 41 | #endif 42 | int day = (now - new DateTime(2021, 1, 1)).Days; 43 | int period = (int)now.TimeOfDay.TotalMinutes / periodLengthInMinutes; 44 | if (currentDay != -1 && (day != currentDay || period != currentPeriod)) 45 | { 46 | await SaveData(); 47 | } 48 | currentDay = day; 49 | currentPeriod = period; 50 | visitorStatisticService.StoreVisit(eventData); 51 | inputStatisticService.StoreStatisticRecord(eventData.Request, eventData.ActionType, eventData.Error); 52 | } catch (Exception e) { 53 | using IServiceScope scope = serviceProvider.CreateScope(); 54 | var errorHandlingService = scope.ServiceProvider.GetRequiredService(); 55 | errorHandlingService.OnException(e, null); 56 | logger.Log(LogLevel.Error, e, e.Message); 57 | } 58 | } 59 | 60 | private async Task SaveData() 61 | { 62 | using IServiceScope scope = serviceProvider.CreateScope(); 63 | 64 | var dbContext = scope.ServiceProvider.GetRequiredService(); 65 | try { 66 | await visitorStatisticService.SaveVisitors(dbContext); 67 | inputStatisticService.SaveRecords(dbContext); 68 | dbContext.SaveChanges(); 69 | } catch (Exception e) { 70 | var errorHandlingService = scope.ServiceProvider.GetRequiredService(); 71 | errorHandlingService.OnException(e, null); 72 | logger.Log(LogLevel.Error, e, e.Message); 73 | } 74 | } 75 | 76 | 77 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 78 | { 79 | await foreach(var e in eventChannel.Reader.ReadAllAsync(stoppingToken)) { 80 | logger.Log(LogLevel.Information, "Event data read."); 81 | await OnEvent(e); 82 | } 83 | logger.Log(LogLevel.Information, "Event log finished."); 84 | await SaveData(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Server/Services/UserService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Identity; 6 | 7 | namespace VueStart.Services; 8 | 9 | public class UserService 10 | { 11 | private readonly ApplicationDbContext dbContext; 12 | 13 | public UserService(ApplicationDbContext dbContext){ 14 | this.dbContext = dbContext; 15 | } 16 | 17 | public User Authenticate(string username, string password) 18 | { 19 | var user = dbContext.Users.FirstOrDefault(u => u.Username == username); 20 | if (user == null) 21 | return null; 22 | var passwordHasher = new PasswordHasher(); 23 | var result = passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password); 24 | 25 | switch (result) 26 | { 27 | case PasswordVerificationResult.Failed: 28 | return null; 29 | case PasswordVerificationResult.SuccessRehashNeeded: 30 | user.PasswordHash = passwordHasher.HashPassword(user, password); 31 | dbContext.SaveChanges(); 32 | break; 33 | } 34 | 35 | return user; 36 | } 37 | 38 | internal void SetPassword(User user, string newPassword) 39 | { 40 | var passwordHasher = new PasswordHasher(); 41 | user.PasswordHash = passwordHasher.HashPassword(user, newPassword); 42 | dbContext.SaveChanges(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Server/Services/VisitorStatisticService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.Logging; 7 | using UAParser; 8 | 9 | namespace VueStart.Services; 10 | 11 | public class VisitorStatisticService 12 | { 13 | private Dictionary Visitors { get; } = new Dictionary(); 14 | private readonly GeoLocationService geoLocationService; 15 | private readonly ILogger logger; 16 | 17 | public VisitorStatisticService(GeoLocationService geoLocationService, ILogger logger) 18 | { 19 | this.geoLocationService = geoLocationService; 20 | this.logger = logger; 21 | } 22 | 23 | public void StoreVisit(EventData eventData) 24 | { 25 | if (Visitors.TryGetValue(eventData.IdToken, out var data)) 26 | { 27 | var visit = data.Visitor.Visits.First(); 28 | visit.Count += 1; 29 | visit.End = DateTime.UtcNow; 30 | } 31 | else 32 | { 33 | var visitor = CreateVisitor(eventData.UaString, eventData.IdToken, eventData.Citation); 34 | Visitors.Add(eventData.IdToken, new VisitorData { 35 | Visitor = visitor, 36 | Ip = eventData.RemoteIpAddress 37 | }); 38 | 39 | visitor.Visits = new List { 40 | new Visit { 41 | Start = DateTime.UtcNow, 42 | End = DateTime.UtcNow, 43 | Count = 1 44 | } 45 | }; 46 | } 47 | } 48 | 49 | public async Task SaveVisitors(ApplicationDbContext dbContext) 50 | { 51 | var toLocate = new List(); 52 | logger.Log(LogLevel.Information, $"Saving {Visitors.Count} visitors."); 53 | foreach (var item in Visitors) 54 | { 55 | var visitor = item.Value.Visitor; 56 | var earlierVisitor = dbContext.Visitors.FirstOrDefault(v => v.Token == visitor.Token); 57 | if (earlierVisitor != null) 58 | { 59 | dbContext.Entry(earlierVisitor).Collection(v => v.Visits).Load(); 60 | earlierVisitor.Visits.AddRange(visitor.Visits); 61 | } 62 | else 63 | { 64 | toLocate.Add(item.Value); 65 | } 66 | } 67 | if (toLocate.Any()) { 68 | await geoLocationService.SetGeoLocation(toLocate); 69 | foreach (var item in toLocate) { 70 | dbContext.Visitors.Add(item.Visitor); 71 | } 72 | } 73 | Visitors.Clear(); 74 | } 75 | 76 | internal IEnumerable GetCachedVisitors() 77 | { 78 | return Visitors.Values.Select(d => d.Visitor); 79 | } 80 | 81 | private Visitor CreateVisitor(string uaString, string token, string citation) 82 | { 83 | var uaParser = Parser.GetDefault(); 84 | ClientInfo c = uaParser.Parse(uaString); 85 | var visitor = new Visitor 86 | { 87 | Token = token, 88 | Citation = citation, 89 | FirstVisit = DateTime.UtcNow, 90 | UserAgent = uaString, 91 | OSFamily = c.OS.Family, 92 | OSMajor = c.OS.Major, 93 | OSMinor = c.OS.Minor, 94 | DeviceBrand = c.Device.Brand, 95 | DeviceFamily = c.Device.Family, 96 | DeviceModel = c.Device.Model, 97 | BrowserFamily = c.UA.Family, 98 | BrowserMajor = c.UA.Major, 99 | BrowserMinor = c.UA.Minor 100 | }; 101 | logger.Log(LogLevel.Information, "New visitor"); 102 | return visitor; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Server/Settings/ClassSettings.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace VueStart; 5 | public class ClassSettings 6 | { 7 | public string Name { get; set; } 8 | public List PropertySettings { get; set; } 9 | 10 | public static ClassSettings FromBootGenClassSettings(BootGen.Core.ClassSettings classSettings) 11 | { 12 | return new ClassSettings 13 | { 14 | Name = classSettings.Name, 15 | PropertySettings = classSettings.PropertySettings.Select(VueStart.PropertySettings.FromBootGenPropertySettings).ToList() 16 | }; 17 | } 18 | 19 | public BootGen.Core.ClassSettings ToBootGenClassSettings() 20 | { 21 | return new BootGen.Core.ClassSettings 22 | { 23 | Name = Name, 24 | PropertySettings = PropertySettings.Select(s => s.ToBootGenPropertySettings()).ToList() 25 | }; 26 | } 27 | } -------------------------------------------------------------------------------- /Server/Settings/PropertySettings.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace VueStart; 3 | 4 | public struct PropertySettings 5 | { 6 | public string Name { get; set; } 7 | public string VisibleName { get; set; } 8 | public bool? IsReadOnly { get; set; } 9 | public bool IsHidden { get; set; } 10 | public bool? ShowAsImage { get; set; } 11 | 12 | public static PropertySettings FromBootGenPropertySettings(BootGen.Core.PropertySettings propertySettings) 13 | { 14 | return new PropertySettings 15 | { 16 | Name = propertySettings.Name, 17 | VisibleName = propertySettings.VisibleName, 18 | IsReadOnly = propertySettings.IsReadOnly, 19 | IsHidden = propertySettings.IsHidden, 20 | ShowAsImage = propertySettings.ShowAsImage 21 | }; 22 | } 23 | 24 | public BootGen.Core.PropertySettings ToBootGenPropertySettings() 25 | { 26 | return new BootGen.Core.PropertySettings 27 | { 28 | Name = Name, 29 | VisibleName = VisibleName, 30 | IsReadOnly = IsReadOnly, 31 | IsHidden = IsHidden, 32 | ShowAsImage = ShowAsImage 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Server/Startup.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Diagnostics; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.AspNetCore.HttpOverrides; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Hosting; 9 | using VueStart.Services; 10 | using Microsoft.AspNetCore.Http; 11 | using VueStart.Authorization; 12 | using System.Threading.Channels; 13 | using Microsoft.EntityFrameworkCore; 14 | 15 | namespace VueStart 16 | { 17 | public class Startup 18 | { 19 | public Startup(IConfiguration configuration) 20 | { 21 | Configuration = configuration; 22 | } 23 | 24 | public IConfiguration Configuration { get; } 25 | 26 | // This method gets called by the runtime. Use this method to add services to the container. 27 | public void ConfigureServices(IServiceCollection services) 28 | { 29 | services.AddControllers(); 30 | services.AddMemoryCache(); 31 | services.AddHostedService(); 32 | services.AddSingleton(Channel.CreateUnbounded()); 33 | services.AddSingleton(); 34 | services.AddSingleton(); 35 | services.AddSingleton(); 36 | services.AddScoped(); 37 | services.AddScoped(); 38 | services.AddScoped(); 39 | services.AddDbContext(options => options.UseNpgsql(Configuration.GetConnectionString("PostgreSQL"))); 40 | } 41 | 42 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 43 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 44 | { 45 | if (env.IsDevelopment()) 46 | { 47 | app.UseDeveloperExceptionPage(); 48 | } 49 | 50 | app.UseForwardedHeaders(new ForwardedHeadersOptions 51 | { 52 | ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto 53 | }); 54 | app.Use((context, next) => 55 | { 56 | context.Request.EnableBuffering(); 57 | return next(); 58 | }); 59 | app.UseExceptionHandler(builder => { 60 | builder.Run(async context => { 61 | using var scope = app.ApplicationServices.CreateScope(); 62 | var service = scope.ServiceProvider.GetRequiredService(); 63 | var handler = context.Features.Get(); 64 | var exception = handler?.Error; 65 | context.Request.Body.Seek(0, SeekOrigin.Begin); 66 | if (exception != null) { 67 | service.OnException(exception, await new StreamReader(context.Request.Body).ReadToEndAsync()); 68 | } 69 | }); 70 | }); 71 | 72 | //app.UseHttpsRedirection(); 73 | 74 | app.UseRouting(); 75 | 76 | app.UseMiddleware(); 77 | app.UseCors(); 78 | 79 | app.UseEndpoints(endpoints => 80 | { 81 | endpoints.MapControllers(); 82 | }); 83 | 84 | 85 | if (env.IsDevelopment()) 86 | { 87 | app.UseSpa(spa => 88 | { 89 | spa.UseProxyToSpaDevelopmentServer($"http://localhost:8080"); 90 | }); 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Server/VueStart.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | runtime; build; native; contentfiles; analyzers; buildtransitive 12 | all 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | PreserveNewest 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Server/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "ConnectionStrings": { 10 | "PostgreSQL": "User ID =postgres;Password=secret;Server=localhost;Port=5432;Database=vuestart;Integrated Security=true;Pooling=true;" 11 | }, 12 | "Urls": "http://localhost:5000" 13 | } 14 | -------------------------------------------------------------------------------- /Server/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /Server/templates/bootstrap-index.sbn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ title }} 8 | {{~ if base_url ~}} 9 | 10 | {{~ end ~}} 11 | 12 | 13 | 78 | 79 | 80 | 83 |
84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /Server/templates/tailwind-index.sbn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 22 | {{ title }} 23 | {{~ if base_url ~}} 24 | 25 | {{~ end ~}} 26 | 41 | 42 | 43 | 46 |
47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Server/templates/vanilla-index.sbn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ title }} 9 | {{~ if base_url ~}} 10 | 11 | {{~ end ~}} 12 | 228 | 229 | 230 | 233 |
234 | 235 | 236 | 237 | 238 | -------------------------------------------------------------------------------- /vs_demo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/vs_demo.webp --------------------------------------------------------------------------------