diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3729ff0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/.github/workflows/dockerservice.yml b/.github/workflows/dockerservice.yml new file mode 100644 index 0000000..70d8e0b --- /dev/null +++ b/.github/workflows/dockerservice.yml @@ -0,0 +1,36 @@ +name: docker_service + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - + name: Check out the repo + uses: actions/checkout@v2 + - + name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - + name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Docker Build & Push to Docker Hub For Service + uses: docker/build-push-action@v2 + with: + context: . + file: ./Inotify/Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ secrets.DOCKERHUB_TAG }}:service + + - name: 'Report Suecss' + run: curl ${{ secrets.INOTIFY }}/Inotify_service/dockerBuildComplated! diff --git a/.github/workflows/dockervue.yml b/.github/workflows/dockervue.yml new file mode 100644 index 0000000..7aa29db --- /dev/null +++ b/.github/workflows/dockervue.yml @@ -0,0 +1,56 @@ +name: docker_vue + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v2 + + - name: Cache node modules NPM + uses: actions/cache@v2 + env: + cache-name: cache-node-modules-NPM + with: + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('./Inotify.Vue/package.json') }} + restore-keys: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('./Inotify.Vue/package.json') }} + + - name: Cache node modules + uses: actions/cache@v2 + env: + cache-name: cache-node-modules + with: + path: ./Inotify.Vue/node_modules + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('./Inotify.Vue/package.json') }} + restore-keys: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('./Inotify.Vue/package.json') }} + + - name: InstallNode and BuildVue + run: | + cd ./Inotify.Vue + npm install + npm run build:prod + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Docker Build & Push to Docker Hub For Inotify.Vue + uses: docker/build-push-action@v2 + with: + context: ./Inotify.Vue + file: ./Inotify.Vue/Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ secrets.DOCKERHUB_TAG }}:vue + + - name: 'Report Suecss' + run: curl ${{ secrets.INOTIFY }}/InotifyVue/dockerBuildComplated! diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 6704566..0000000 --- a/.gitignore +++ /dev/null @@ -1,104 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env -.env.test - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# Next.js build output -.next - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and *not* Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port diff --git a/Inotify.Vue/.editorconfig b/Inotify.Vue/.editorconfig new file mode 100644 index 0000000..ea6e20f --- /dev/null +++ b/Inotify.Vue/.editorconfig @@ -0,0 +1,14 @@ +# http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/Inotify.Vue/.env.development b/Inotify.Vue/.env.development new file mode 100644 index 0000000..128fbf0 --- /dev/null +++ b/Inotify.Vue/.env.development @@ -0,0 +1,5 @@ +# just a flag +ENV = 'development' + +# base api +VUE_APP_BASE_API = '/api' diff --git a/Inotify.Vue/.env.production b/Inotify.Vue/.env.production new file mode 100644 index 0000000..07d391e --- /dev/null +++ b/Inotify.Vue/.env.production @@ -0,0 +1,6 @@ +# just a flag +ENV = 'production' + +# base api +VUE_APP_BASE_API = '/api' + diff --git a/Inotify.Vue/.env.staging b/Inotify.Vue/.env.staging new file mode 100644 index 0000000..d69f663 --- /dev/null +++ b/Inotify.Vue/.env.staging @@ -0,0 +1,8 @@ +NODE_ENV = production + +# just a flag +ENV = 'staging' + +# base api +VUE_APP_BASE_API = '/api' + diff --git a/Inotify.Vue/.eslintignore b/Inotify.Vue/.eslintignore new file mode 100644 index 0000000..b8e7312 --- /dev/null +++ b/Inotify.Vue/.eslintignore @@ -0,0 +1,5 @@ +build/*.js +src/assets +public +dist +*.vue diff --git a/Inotify.Vue/.eslintrc.js b/Inotify.Vue/.eslintrc.js new file mode 100644 index 0000000..7b5fd09 --- /dev/null +++ b/Inotify.Vue/.eslintrc.js @@ -0,0 +1,198 @@ +module.exports = { + root: true, + parserOptions: { + parser: 'babel-eslint', + sourceType: 'module' + }, + env: { + browser: true, + node: true, + es6: true, + }, + extends: ['plugin:vue/recommended', 'eslint:recommended'], + + // add your custom rules here + //it is base on https://github.com/vuejs/eslint-config-vue + rules: { + "vue/max-attributes-per-line": [2, { + "singleline": 10, + "multiline": { + "max": 1, + "allowFirstLine": false + } + }], + "vue/singleline-html-element-content-newline": "off", + "vue/multiline-html-element-content-newline":"off", + "vue/name-property-casing": ["error", "PascalCase"], + "vue/no-v-html": "off", + 'accessor-pairs': 2, + 'arrow-spacing': [2, { + 'before': true, + 'after': true + }], + 'block-spacing': [2, 'always'], + 'brace-style': [2, '1tbs', { + 'allowSingleLine': true + }], + 'camelcase': [0, { + 'properties': 'always' + }], + 'comma-dangle': [2, 'never'], + 'comma-spacing': [2, { + 'before': false, + 'after': true + }], + 'comma-style': [2, 'last'], + 'constructor-super': 2, + 'curly': [2, 'multi-line'], + 'dot-location': [2, 'property'], + 'eol-last': 2, + 'eqeqeq': ["error", "always", {"null": "ignore"}], + 'generator-star-spacing': [2, { + 'before': true, + 'after': true + }], + 'handle-callback-err': [2, '^(err|error)$'], + 'indent': [2, 2, { + 'SwitchCase': 1 + }], + 'jsx-quotes': [2, 'prefer-single'], + 'key-spacing': [2, { + 'beforeColon': false, + 'afterColon': true + }], + 'keyword-spacing': [2, { + 'before': true, + 'after': true + }], + 'new-cap': [2, { + 'newIsCap': true, + 'capIsNew': false + }], + 'new-parens': 2, + 'no-array-constructor': 2, + 'no-caller': 2, + 'no-console': 'off', + 'no-class-assign': 2, + 'no-cond-assign': 2, + 'no-const-assign': 2, + 'no-control-regex': 0, + 'no-delete-var': 2, + 'no-dupe-args': 2, + 'no-dupe-class-members': 2, + 'no-dupe-keys': 2, + 'no-duplicate-case': 2, + 'no-empty-character-class': 2, + 'no-empty-pattern': 2, + 'no-eval': 2, + 'no-ex-assign': 2, + 'no-extend-native': 2, + 'no-extra-bind': 2, + 'no-extra-boolean-cast': 2, + 'no-extra-parens': [2, 'functions'], + 'no-fallthrough': 2, + 'no-floating-decimal': 2, + 'no-func-assign': 2, + 'no-implied-eval': 2, + 'no-inner-declarations': [2, 'functions'], + 'no-invalid-regexp': 2, + 'no-irregular-whitespace': 2, + 'no-iterator': 2, + 'no-label-var': 2, + 'no-labels': [2, { + 'allowLoop': false, + 'allowSwitch': false + }], + 'no-lone-blocks': 2, + 'no-mixed-spaces-and-tabs': 2, + 'no-multi-spaces': 2, + 'no-multi-str': 2, + 'no-multiple-empty-lines': [2, { + 'max': 1 + }], + 'no-native-reassign': 2, + 'no-negated-in-lhs': 2, + 'no-new-object': 2, + 'no-new-require': 2, + 'no-new-symbol': 2, + 'no-new-wrappers': 2, + 'no-obj-calls': 2, + 'no-octal': 2, + 'no-octal-escape': 2, + 'no-path-concat': 2, + 'no-proto': 2, + 'no-redeclare': 2, + 'no-regex-spaces': 2, + 'no-return-assign': [2, 'except-parens'], + 'no-self-assign': 2, + 'no-self-compare': 2, + 'no-sequences': 2, + 'no-shadow-restricted-names': 2, + 'no-spaced-func': 2, + 'no-sparse-arrays': 2, + 'no-this-before-super': 2, + 'no-throw-literal': 2, + 'no-trailing-spaces': 2, + 'no-undef': 2, + 'no-undef-init': 2, + 'no-unexpected-multiline': 2, + 'no-unmodified-loop-condition': 2, + 'no-unneeded-ternary': [2, { + 'defaultAssignment': false + }], + 'no-unreachable': 2, + 'no-unsafe-finally': 2, + 'no-unused-vars': [2, { + 'vars': 'all', + 'args': 'none' + }], + 'no-useless-call': 2, + 'no-useless-computed-key': 2, + 'no-useless-constructor': 2, + 'no-useless-escape': 0, + 'no-whitespace-before-property': 2, + 'no-with': 2, + 'one-var': [2, { + 'initialized': 'never' + }], + 'operator-linebreak': [2, 'after', { + 'overrides': { + '?': 'before', + ':': 'before' + } + }], + 'padded-blocks': [2, 'never'], + 'quotes': [2, 'single', { + 'avoidEscape': true, + 'allowTemplateLiterals': true + }], + 'semi': [2, 'never'], + 'semi-spacing': [2, { + 'before': false, + 'after': true + }], + 'space-before-blocks': [2, 'always'], + 'space-before-function-paren': 0, + 'space-in-parens': [2, 'never'], + 'space-infix-ops': 2, + 'space-unary-ops': [2, { + 'words': true, + 'nonwords': false + }], + 'spaced-comment': [2, 'always', { + 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] + }], + 'template-curly-spacing': [2, 'never'], + 'use-isnan': 2, + 'valid-typeof': 2, + 'wrap-iife': [2, 'any'], + 'yield-star-spacing': [2, 'both'], + 'yoda': [2, 'never'], + 'prefer-const': 2, + 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, + 'object-curly-spacing': [2, 'always', { + objectsInObjects: false + }], + 'array-bracket-spacing': [2, 'never'] + } +} diff --git a/Inotify.Vue/.gitignore b/Inotify.Vue/.gitignore new file mode 100644 index 0000000..9ad28d2 --- /dev/null +++ b/Inotify.Vue/.gitignore @@ -0,0 +1,16 @@ +.DS_Store +node_modules/ +dist/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +package-lock.json +tests/**/coverage/ + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln diff --git a/Inotify.Vue/.prettierrc b/Inotify.Vue/.prettierrc new file mode 100644 index 0000000..494a2f8 --- /dev/null +++ b/Inotify.Vue/.prettierrc @@ -0,0 +1,6 @@ +{ + "printWidth": 300, + "tabWidth": 2, + "singleQuote": true, + "semi": false +} \ No newline at end of file diff --git a/Inotify.Vue/.travis.yml b/Inotify.Vue/.travis.yml new file mode 100644 index 0000000..f4be7a0 --- /dev/null +++ b/Inotify.Vue/.travis.yml @@ -0,0 +1,5 @@ +language: node_js +node_js: 10 +script: npm run test +notifications: + email: false diff --git a/Inotify.Vue/Dockerfile b/Inotify.Vue/Dockerfile new file mode 100644 index 0000000..6b1750a --- /dev/null +++ b/Inotify.Vue/Dockerfile @@ -0,0 +1,6 @@ +FROM nginx +RUN mkdir /usr/share/nginx/dist +RUN rm -rf /etc/nginx/nginx.conf +COPY ./nginx.conf /etc/nginx/nginx.conf +COPY ./dist /usr/share/nginx/dist +EXPOSE 9099 \ No newline at end of file diff --git a/Inotify.Vue/LICENSE b/Inotify.Vue/LICENSE new file mode 100644 index 0000000..6151575 --- /dev/null +++ b/Inotify.Vue/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017-present PanJiaChen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Inotify.Vue/babel.config.js b/Inotify.Vue/babel.config.js new file mode 100644 index 0000000..fea4ca7 --- /dev/null +++ b/Inotify.Vue/babel.config.js @@ -0,0 +1,16 @@ +module.exports = { + presets: [ + // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app + '@vue/cli-plugin-babel/preset' + ], + 'env': { + 'development': { + // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require(). + // This plugin can significantly increase the speed of hot updates, when you have a large number of pages. + // https://panjiachen.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html + 'plugins': ['dynamic-import-node'], + 'sourceMaps':true, + 'retainLines':true + } + } +} diff --git a/Inotify.Vue/build/index.js b/Inotify.Vue/build/index.js new file mode 100644 index 0000000..0c57de2 --- /dev/null +++ b/Inotify.Vue/build/index.js @@ -0,0 +1,35 @@ +const { run } = require('runjs') +const chalk = require('chalk') +const config = require('../vue.config.js') +const rawArgv = process.argv.slice(2) +const args = rawArgv.join(' ') + +if (process.env.npm_config_preview || rawArgv.includes('--preview')) { + const report = rawArgv.includes('--report') + + run(`vue-cli-service build ${args}`) + + const port = 9526 + const publicPath = config.publicPath + + var connect = require('connect') + var serveStatic = require('serve-static') + const app = connect() + + app.use( + publicPath, + serveStatic('./dist', { + index: ['index.html', '/'] + }) + ) + + app.listen(port, function () { + console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`)) + if (report) { + console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`)) + } + + }) +} else { + run(`vue-cli-service build ${args}`) +} diff --git a/Inotify.Vue/jest.config.js b/Inotify.Vue/jest.config.js new file mode 100644 index 0000000..143cdc8 --- /dev/null +++ b/Inotify.Vue/jest.config.js @@ -0,0 +1,24 @@ +module.exports = { + moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], + transform: { + '^.+\\.vue$': 'vue-jest', + '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': + 'jest-transform-stub', + '^.+\\.jsx?$': 'babel-jest' + }, + moduleNameMapper: { + '^@/(.*)$': '/src/$1' + }, + snapshotSerializers: ['jest-serializer-vue'], + testMatch: [ + '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' + ], + collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], + coverageDirectory: '/tests/unit/coverage', + // 'collectCoverage': true, + 'coverageReporters': [ + 'lcov', + 'text-summary' + ], + testURL: 'http://localhost/' +} diff --git a/Inotify.Vue/jsconfig.json b/Inotify.Vue/jsconfig.json new file mode 100644 index 0000000..ed079e2 --- /dev/null +++ b/Inotify.Vue/jsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "baseUrl": "./", + "paths": { + "@/*": ["src/*"] + } + }, + "exclude": ["node_modules", "dist"] +} diff --git a/Inotify.Vue/mock/index.js b/Inotify.Vue/mock/index.js new file mode 100644 index 0000000..32d013c --- /dev/null +++ b/Inotify.Vue/mock/index.js @@ -0,0 +1,59 @@ +const Mock = require('mockjs') +const { param2Obj } = require('./utils') + +const user = require('./user') +const table = require('./table') +const setting = require('./setting') + +const mocks = [ + ...user, + ...table, + ...setting +] + +// for front mock +// please use it cautiously, it will redefine XMLHttpRequest, +// which will cause many of your third-party libraries to be invalidated(like progress event). +function mockXHR() { + // mock patch + // https://github.com/nuysoft/Mock/issues/300 + Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send + Mock.XHR.prototype.send = function() { + if (this.custom.xhr) { + this.custom.xhr.withCredentials = this.withCredentials || false + + if (this.responseType) { + this.custom.xhr.responseType = this.responseType + } + } + this.proxy_send(...arguments) + } + + function XHR2ExpressReqWrap(respond) { + return function(options) { + let result = null + if (respond instanceof Function) { + const { body, type, url } = options + // https://expressjs.com/en/4x/api.html#req + result = respond({ + method: type, + body: JSON.parse(body), + query: param2Obj(url) + }) + } else { + result = respond + } + return Mock.mock(result) + } + } + + for (const i of mocks) { + Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response)) + } +} + +module.exports = { + mocks, + mockXHR +} + diff --git a/Inotify.Vue/mock/mock-server.js b/Inotify.Vue/mock/mock-server.js new file mode 100644 index 0000000..8941ec0 --- /dev/null +++ b/Inotify.Vue/mock/mock-server.js @@ -0,0 +1,81 @@ +const chokidar = require('chokidar') +const bodyParser = require('body-parser') +const chalk = require('chalk') +const path = require('path') +const Mock = require('mockjs') + +const mockDir = path.join(process.cwd(), 'mock') + +function registerRoutes(app) { + let mockLastIndex + const { mocks } = require('./index.js') + const mocksForServer = mocks.map(route => { + return responseFake(route.url, route.type, route.response) + }) + for (const mock of mocksForServer) { + app[mock.type](mock.url, mock.response) + mockLastIndex = app._router.stack.length + } + const mockRoutesLength = Object.keys(mocksForServer).length + return { + mockRoutesLength: mockRoutesLength, + mockStartIndex: mockLastIndex - mockRoutesLength + } +} + +function unregisterRoutes() { + Object.keys(require.cache).forEach(i => { + if (i.includes(mockDir)) { + delete require.cache[require.resolve(i)] + } + }) +} + +// for mock server +const responseFake = (url, type, respond) => { + return { + url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`), + type: type || 'get', + response(req, res) { + console.log('request invoke:' + req.path) + res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond)) + } + } +} + +module.exports = app => { + // parse app.body + // https://expressjs.com/en/4x/api.html#req.body + app.use(bodyParser.json()) + app.use(bodyParser.urlencoded({ + extended: true + })) + + const mockRoutes = registerRoutes(app) + var mockRoutesLength = mockRoutes.mockRoutesLength + var mockStartIndex = mockRoutes.mockStartIndex + + // watch files, hot reload mock server + chokidar.watch(mockDir, { + ignored: /mock-server/, + ignoreInitial: true + }).on('all', (event, path) => { + if (event === 'change' || event === 'add') { + try { + // remove mock routes stack + app._router.stack.splice(mockStartIndex, mockRoutesLength) + + // clear routes cache + unregisterRoutes() + + const mockRoutes = registerRoutes(app) + mockRoutesLength = mockRoutes.mockRoutesLength + mockStartIndex = mockRoutes.mockStartIndex + + console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`)) + } catch (error) { + console.log(chalk.redBright(error)) + } + } + }) +} diff --git a/Inotify.Vue/mock/setting.js b/Inotify.Vue/mock/setting.js new file mode 100644 index 0000000..859740d --- /dev/null +++ b/Inotify.Vue/mock/setting.js @@ -0,0 +1,189 @@ +const Mock = require('mockjs') + +const data = Mock.mock({ + 'items|30': [ + { + id: '@id', + title: '@sentence(10, 20)', + 'status|1': ['published', 'draft', 'deleted'], + author: 'name', + display_time: '@datetime', + pageviews: '@integer(300, 5000)' + } + ] +}) +const sendTemeplateData = [ + { + name: '邮件推送', + key: 'EA2B43F7-956C-4C01-B583-0C943ABB36C3', + values: [ + { + name: 'FromAddress', + description: '\u53D1\u4EF6\u5730\u5740', + default: 'abc@qq.com', + type: 0, + order: 0 + }, + { + name: 'FromPasssWord', + description: '\u53D1\u4EF6\u5BC6\u7801', + default: '123456789', + type: 0, + order: 1 + }, + { + name: 'FromServer', + description: '\u53D1\u4EF6SMTP', + default: 'stmp.qq.com', + type: 0, + order: 2 + }, + { name: 'EnableSSL', description: 'SSL', default: 'true|false', type: 1, order: 3 }, + { + name: 'ToAddress', + description: '\u6536\u4EF6\u7BB1', + default: 'abcd@qq.com', + type: 0, + order: 4 + } + ] + }, + { + key: '409A30D5-ABE8-4A28-BADD-D04B9908D763', + name: '企业微信', + values: [ + { + name: 'Corpid', + description: '\u4F01\u4E1AID', + default: 'Corpid', + type: 0, + order: 0 + }, + { + name: 'Corpsecret', + description: '\u5BC6\u94A5', + default: 'Corpsecret', + type: 0, + order: 1 + }, + { + name: 'AgentID', + description: '\u5E94\u7528ID', + default: 'AgentID', + type: 0, + order: 2 + } + ] + } +] +const sendAuths = { + code: 200, + data: [ + { + key: '409A30D5-ABE8-4A28-BADD-D04B9908D763', + type: '微信推送', + name: '测试', + isActive: true, + auth: '{\u0022Corpid\u0022:\u0022ww4199b1ecd7dcecba\u0022,\u0022Corpsecret\u0022:\u0022kZUQf52AMYAMsxPGXEiQsHISLwjHhHAnyPXYKLPdoo4\u0022,\u0022AgentID\u0022:\u00221000002\u0022}', + values: [ + { name: 'Corpid', description: '企业ID', default: 'Corpid', type: 1, order: 0, value: 'ww4199b1ecd7dcecba' }, + { name: 'Corpsecret', description: '密钥', default: 'Corpsecret', type: 1, order: 1, value: 'kZUQf52AMYAMsxPGXEiQsHISLwjHhHAnyPXYKLPdoo4' }, + { name: 'AgentID', description: '应用ID', default: 'AgentID', type: 1, order: 2, value: '1000002' } + ] + } + ] +} + +const sendKeyData = '3015679CB0DC462C89F2E37779540894' + +module.exports = [ + // user login + { + url: '/setting/getSendTemplates', + type: 'get', + response: config => { + const items = sendTemeplateData + return { + code: 200, + data: sendTemeplateData + } + } + }, + { + url: '/setting/getSendAuths', + type: 'get', + response: config => { + return sendAuths + } + }, + { + url: '/setting/getSendKey', + type: 'get', + response: config => { + return { + code: 200, + data: sendKeyData + } + } + }, + { + url: '/setting/reSendKey', + type: 'get', + response: config => { + return { + code: 200, + data: true + } + } + }, + { + url: '/send', + type: 'get', + response: config => { + return { + code: 200, + data: true + } + } + }, + { + url: '/setting/deleteSendAuth', + type: 'get', + response: config => { + return { + code: 200, + data: true + } + } + }, + { + url: '/setting/ActiveSendAuth', + type: 'get', + response: config => { + return { + code: 200, + data: true + } + } + }, + { + url: '/setting/addSendAuth', + type: 'post', + response: config => { + return { + code: 200, + data: true + } + } + }, + { + url: '/setting/modifySendAuth', + type: 'post', + response: config => { + return { + code: 200, + data: true + } + } + } +] diff --git a/Inotify.Vue/mock/table.js b/Inotify.Vue/mock/table.js new file mode 100644 index 0000000..6f12eac --- /dev/null +++ b/Inotify.Vue/mock/table.js @@ -0,0 +1,29 @@ +const Mock = require('mockjs') + +const data = Mock.mock({ + 'items|30': [{ + id: '@id', + title: '@sentence(10, 20)', + 'status|1': ['published', 'draft', 'deleted'], + author: 'name', + display_time: '@datetime', + pageviews: '@integer(300, 5000)' + }] +}) + +module.exports = [ + { + url: '/vue-admin-template/table/list', + type: 'get', + response: config => { + const items = data.items + return { + code: 200, + data: { + total: items.length, + items: items + } + } + } + } +] diff --git a/Inotify.Vue/mock/user.js b/Inotify.Vue/mock/user.js new file mode 100644 index 0000000..972849f --- /dev/null +++ b/Inotify.Vue/mock/user.js @@ -0,0 +1,104 @@ + +const tokens = { + admin: { + token: 'admin-token' + }, + editor: { + token: 'editor-token' + } +} + +const users = { + 'admin-token': { + roles: ['admin'], + introduction: 'I am a super administrator', + avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', + name: 'Super Admin' + }, + 'editor-token': { + roles: ['editor'], + introduction: 'I am an editor', + avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', + name: 'Normal Editor' + } +} + +module.exports = [ + // user login + { + url: '/oauth/login', + type: 'post', + response: config => { + const { username } = config.body + const token = tokens[username] + + // mock error + if (!token) { + return { + code: 60204, + message: 'Account and password are incorrect.' + } + } + + return { + code: 200, + data: token + } + } + }, + { + url: '/oauth/githublogin', + type: 'post', + response: config => { + const { username } = config.body + const token = tokens[username] + + // mock error + if (!token) { + return { + code: 60204, + message: '登陆失败.' + } + } + + return { + code: 200, + data: token + } + } + }, + // get user info + { + url: '/oauth/info', + type: 'get', + response: config => { + const { token } = config.query + const info = users[token] + + // mock error + if (!info) { + return { + code: 50008, + message: 'Login failed, unable to get user details.' + } + } + + return { + code: 200, + data: info + } + } + }, + + // user logout + { + url: '/oauth/logout', + type: 'post', + response: _ => { + return { + code: 200, + data: 'success' + } + } + } +] diff --git a/Inotify.Vue/mock/utils.js b/Inotify.Vue/mock/utils.js new file mode 100644 index 0000000..95cc27d --- /dev/null +++ b/Inotify.Vue/mock/utils.js @@ -0,0 +1,25 @@ +/** + * @param {string} url + * @returns {Object} + */ +function param2Obj(url) { + const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') + if (!search) { + return {} + } + const obj = {} + const searchArr = search.split('&') + searchArr.forEach(v => { + const index = v.indexOf('=') + if (index !== -1) { + const name = v.substring(0, index) + const val = v.substring(index + 1, v.length) + obj[name] = val + } + }) + return obj +} + +module.exports = { + param2Obj +} diff --git a/Inotify.Vue/nginx.conf b/Inotify.Vue/nginx.conf new file mode 100644 index 0000000..31a4cfb --- /dev/null +++ b/Inotify.Vue/nginx.conf @@ -0,0 +1,20 @@ +# fangt add for web server +# 2018-10-27 +worker_processes 1; + +events { + worker_connections 1024; +} +http { + include mime.types; + sendfile on; + keepalive_timeout 65; + default_type application/octet-stream; + server { + listen 80; + server_name localhost; + location / { + root /usr/share/nginx/dist; + } + } +} \ No newline at end of file diff --git a/Inotify.Vue/package.json b/Inotify.Vue/package.json new file mode 100644 index 0000000..8959592 --- /dev/null +++ b/Inotify.Vue/package.json @@ -0,0 +1,65 @@ +{ + "name": "Inotify-vue", + "version": "0.0.1", + "description": "WebControl for Inotify", + "author": "", + "scripts": { + "dev": "vue-cli-service serve", + "build:prod": "vue-cli-service build", + "build:stage": "vue-cli-service build --mode staging", + "preview": "node build/index.js --preview", + "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml", + "lint": "eslint --ext .js,.vue src", + "test:unit": "jest --clearCache && vue-cli-service test:unit", + "test:ci": "npm run lint && npm run test:unit" + }, + "dependencies": { + "axios": "0.18.1", + "core-js": "3.6.5", + "echarts": "^4.9.0", + "element-plus": "^1.0.2-beta.32", + "element-ui": "2.13.2", + "js-cookie": "2.2.0", + "moment": "^2.29.1", + "normalize.css": "7.0.0", + "nprogress": "0.2.0", + "path-to-regexp": "2.4.0", + "vue": "2.6.10", + "vue-router": "3.0.6", + "vuex": "3.1.0" + }, + "devDependencies": { + "@vue/cli-plugin-babel": "4.4.4", + "@vue/cli-plugin-eslint": "4.4.4", + "@vue/cli-plugin-unit-jest": "4.4.4", + "@vue/cli-service": "4.4.4", + "@vue/test-utils": "1.0.0-beta.29", + "autoprefixer": "9.5.1", + "babel-eslint": "10.1.0", + "babel-jest": "23.6.0", + "babel-plugin-dynamic-import-node": "2.3.3", + "chalk": "2.4.2", + "connect": "3.6.6", + "eslint": "6.7.2", + "eslint-plugin-vue": "6.2.2", + "html-webpack-plugin": "3.2.0", + "mockjs": "1.0.1-beta3", + "runjs": "4.3.2", + "sass": "1.26.8", + "sass-loader": "8.0.2", + "script-ext-html-webpack-plugin": "2.1.3", + "serve-static": "1.13.2", + "svg-sprite-loader": "4.1.3", + "svgo": "1.2.2", + "vue-template-compiler": "2.6.10" + }, + "browserslist": [ + "> 1%", + "last 2 versions" + ], + "engines": { + "node": ">=8.9", + "npm": ">= 3.0.0" + }, + "license": "MIT" +} diff --git a/Inotify.Vue/postcss.config.js b/Inotify.Vue/postcss.config.js new file mode 100644 index 0000000..10473ef --- /dev/null +++ b/Inotify.Vue/postcss.config.js @@ -0,0 +1,8 @@ +// https://github.com/michael-ciniawsky/postcss-load-config + +module.exports = { + 'plugins': { + // to edit target browsers: use "browserslist" field in package.json + 'autoprefixer': {} + } +} diff --git a/Inotify.Vue/public/favicon.ico b/Inotify.Vue/public/favicon.ico new file mode 100644 index 0000000..46e0084 Binary files /dev/null and b/Inotify.Vue/public/favicon.ico differ diff --git a/Inotify.Vue/public/index.html b/Inotify.Vue/public/index.html new file mode 100644 index 0000000..fa2be91 --- /dev/null +++ b/Inotify.Vue/public/index.html @@ -0,0 +1,17 @@ + + + + + + + + <%= webpackConfig.name %> + + + +
+ + + diff --git a/Inotify.Vue/src/App.vue b/Inotify.Vue/src/App.vue new file mode 100644 index 0000000..5a9a33c --- /dev/null +++ b/Inotify.Vue/src/App.vue @@ -0,0 +1,11 @@ + + + diff --git a/Inotify.Vue/src/api/setting.js b/Inotify.Vue/src/api/setting.js new file mode 100644 index 0000000..515a315 --- /dev/null +++ b/Inotify.Vue/src/api/setting.js @@ -0,0 +1,96 @@ +import request from '@/utils/request' + +export function getSendTemplates(data) { + return request({ + url: '/setting/getSendTemplates', + method: 'get' + }) +} + +export function getSendAuths(authInfo) { + return request({ + url: '/setting/getSendAuths', + method: 'get', + params: { authInfo } + }) +} + +export function getSendKey(authInfo) { + return request({ + url: '/setting/getSendKey', + method: 'get', + params: { authInfo } + }) +} + +export function reSendKey(authInfo) { + return request({ + url: '/setting/reSendKey', + method: 'get', + params: { authInfo } + }) +} + +export function sendMessage(message) { + return request({ + url: '/send', + method: 'get', + params: message + }) +} + +export function deleteAuthInfo(sendAuthId) { + return request({ + url: '/setting/deleteSendAuth', + method: 'post', + params: { sendAuthId: sendAuthId } + }) +} + +export function activeAuthInfo(sendAuthId, state) { + return request({ + url: '/setting/activeSendAuth', + method: 'post', + params: { sendAuthId: sendAuthId, state: state } + }) +} + +export function addAuthInfo(template) { + return request({ + url: '/setting/addSendAuth', + method: 'post', + data: template + }) +} + +export function modifySendAuth(template) { + return request({ + url: '/setting/modifySendAuth', + method: 'post', + data: template + }) +} + +export function deepClone(data) { + let d + if (typeof data === 'object') { + if (data == null) { + d = null + } else { + if (data.constructor === Array) { + d = [] + for (const i in data) { + d.push(deepClone(data[i])) + } + } else { + d = {} + for (const i in data) { + d[i] = deepClone(data[i]) + } + } + } + } else { + d = data + } + return d +} diff --git a/Inotify.Vue/src/api/systemsetting.js b/Inotify.Vue/src/api/systemsetting.js new file mode 100644 index 0000000..619cde2 --- /dev/null +++ b/Inotify.Vue/src/api/systemsetting.js @@ -0,0 +1,79 @@ +import request from '@/utils/request' + +export function getGlobal() { + return request({ + url: '/settingsys/GetGlobal', + method: 'get' + }) +} + +export function setGlobal(data) { + return request({ + url: '/settingsys/setGlobal', + method: 'post', + params: data + }) +} + +export function getJWT() { + return request({ + url: '/settingsys/getJWT', + method: 'get' + }) +} + +export function setJWT(data) { + return request({ + url: '/settingsys/setJWT', + method: 'post', + data: data + }) +} + +export function getGithubEnable() { + return request({ + url: '/settingsys/getGithubEnable', + method: 'get' + }) +} + +export function getUsers(query, page, pagesize) { + return request({ + url: '/settingsys/getUsers', + method: 'get', + params: { query: query, page: page, pagesize: pagesize } + }) +} + +export function activeUser(userName, active) { + return request({ + url: '/settingsys/activeUser', + method: 'post', + params: { userName: userName, active: active } + }) +} + +export function deleteUser(userName) { + return request({ + url: '/settingsys/deleteUser', + method: 'post', + params: { userName: userName } + }) +} + +export function getSendTypeInfos(start, end) { + return request({ + url: '/settingsys/GetSendTypeInfos', + method: 'get', + params: { start: start, end: end } + }) +} + +export function getSendInfos(start, end) { + return request({ + url: '/settingsys/GetSendInfos', + method: 'get', + params: { start: start, end: end } + }) +} + diff --git a/Inotify.Vue/src/api/table.js b/Inotify.Vue/src/api/table.js new file mode 100644 index 0000000..2752f52 --- /dev/null +++ b/Inotify.Vue/src/api/table.js @@ -0,0 +1,9 @@ +import request from '@/utils/request' + +export function getList(params) { + return request({ + url: '/vue-admin-template/table/list', + method: 'get', + params + }) +} diff --git a/Inotify.Vue/src/api/user.js b/Inotify.Vue/src/api/user.js new file mode 100644 index 0000000..f6d3aec --- /dev/null +++ b/Inotify.Vue/src/api/user.js @@ -0,0 +1,51 @@ +import request from '@/utils/request' + +export function login(data) { + return request({ + url: '/oauth/login', + method: 'post', + params: { + username: data.username, + password: data.password + } + }) +} + +export function githubLogin() { + return request({ + url: '/oauth/githublogin', + method: 'get' + }) +} + +export function githubEnable() { + return request({ + url: '/oauth/GithubEnable', + method: 'get' + }) +} + +export function resetPassword(password) { + return request({ + url: '/oauth/resetPassword', + method: 'post', + params: { + password: password + } + }) +} + +export function getInfo(token) { + return request({ + url: '/oauth/info', + method: 'get', + params: { token } + }) +} + +export function logout() { + return request({ + url: '/oauth/logout', + method: 'post' + }) +} diff --git a/Inotify.Vue/src/assets/404_images/404.png b/Inotify.Vue/src/assets/404_images/404.png new file mode 100644 index 0000000..3d8e230 Binary files /dev/null and b/Inotify.Vue/src/assets/404_images/404.png differ diff --git a/Inotify.Vue/src/assets/404_images/404_cloud.png b/Inotify.Vue/src/assets/404_images/404_cloud.png new file mode 100644 index 0000000..c6281d0 Binary files /dev/null and b/Inotify.Vue/src/assets/404_images/404_cloud.png differ diff --git a/Inotify.Vue/src/components/Breadcrumb/index.vue b/Inotify.Vue/src/components/Breadcrumb/index.vue new file mode 100644 index 0000000..46a9932 --- /dev/null +++ b/Inotify.Vue/src/components/Breadcrumb/index.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/Inotify.Vue/src/components/Hamburger/index.vue b/Inotify.Vue/src/components/Hamburger/index.vue new file mode 100644 index 0000000..368b002 --- /dev/null +++ b/Inotify.Vue/src/components/Hamburger/index.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/Inotify.Vue/src/components/SvgIcon/index.vue b/Inotify.Vue/src/components/SvgIcon/index.vue new file mode 100644 index 0000000..b07ded2 --- /dev/null +++ b/Inotify.Vue/src/components/SvgIcon/index.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/Inotify.Vue/src/icons/index.js b/Inotify.Vue/src/icons/index.js new file mode 100644 index 0000000..2c6b309 --- /dev/null +++ b/Inotify.Vue/src/icons/index.js @@ -0,0 +1,9 @@ +import Vue from 'vue' +import SvgIcon from '@/components/SvgIcon'// svg component + +// register globally +Vue.component('svg-icon', SvgIcon) + +const req = require.context('./svg', false, /\.svg$/) +const requireAll = requireContext => requireContext.keys().map(requireContext) +requireAll(req) diff --git a/Inotify.Vue/src/icons/svg/dashboard.svg b/Inotify.Vue/src/icons/svg/dashboard.svg new file mode 100644 index 0000000..5317d37 --- /dev/null +++ b/Inotify.Vue/src/icons/svg/dashboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Inotify.Vue/src/icons/svg/example.svg b/Inotify.Vue/src/icons/svg/example.svg new file mode 100644 index 0000000..46f42b5 --- /dev/null +++ b/Inotify.Vue/src/icons/svg/example.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Inotify.Vue/src/icons/svg/eye-open.svg b/Inotify.Vue/src/icons/svg/eye-open.svg new file mode 100644 index 0000000..88dcc98 --- /dev/null +++ b/Inotify.Vue/src/icons/svg/eye-open.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Inotify.Vue/src/icons/svg/eye.svg b/Inotify.Vue/src/icons/svg/eye.svg new file mode 100644 index 0000000..16ed2d8 --- /dev/null +++ b/Inotify.Vue/src/icons/svg/eye.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Inotify.Vue/src/icons/svg/form.svg b/Inotify.Vue/src/icons/svg/form.svg new file mode 100644 index 0000000..dcbaa18 --- /dev/null +++ b/Inotify.Vue/src/icons/svg/form.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Inotify.Vue/src/icons/svg/github.svg b/Inotify.Vue/src/icons/svg/github.svg new file mode 100644 index 0000000..032dd8b --- /dev/null +++ b/Inotify.Vue/src/icons/svg/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Inotify.Vue/src/icons/svg/link.svg b/Inotify.Vue/src/icons/svg/link.svg new file mode 100644 index 0000000..48197ba --- /dev/null +++ b/Inotify.Vue/src/icons/svg/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Inotify.Vue/src/icons/svg/nested.svg b/Inotify.Vue/src/icons/svg/nested.svg new file mode 100644 index 0000000..06713a8 --- /dev/null +++ b/Inotify.Vue/src/icons/svg/nested.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Inotify.Vue/src/icons/svg/password.svg b/Inotify.Vue/src/icons/svg/password.svg new file mode 100644 index 0000000..e291d85 --- /dev/null +++ b/Inotify.Vue/src/icons/svg/password.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Inotify.Vue/src/icons/svg/table.svg b/Inotify.Vue/src/icons/svg/table.svg new file mode 100644 index 0000000..0e3dc9d --- /dev/null +++ b/Inotify.Vue/src/icons/svg/table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Inotify.Vue/src/icons/svg/tree.svg b/Inotify.Vue/src/icons/svg/tree.svg new file mode 100644 index 0000000..dd4b7dd --- /dev/null +++ b/Inotify.Vue/src/icons/svg/tree.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Inotify.Vue/src/icons/svg/user.svg b/Inotify.Vue/src/icons/svg/user.svg new file mode 100644 index 0000000..0ba0716 --- /dev/null +++ b/Inotify.Vue/src/icons/svg/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Inotify.Vue/src/icons/svgo.yml b/Inotify.Vue/src/icons/svgo.yml new file mode 100644 index 0000000..d11906a --- /dev/null +++ b/Inotify.Vue/src/icons/svgo.yml @@ -0,0 +1,22 @@ +# replace default config + +# multipass: true +# full: true + +plugins: + + # - name + # + # or: + # - name: false + # - name: true + # + # or: + # - name: + # param1: 1 + # param2: 2 + +- removeAttrs: + attrs: + - 'fill' + - 'fill-rule' diff --git a/Inotify.Vue/src/layout/components/AppMain.vue b/Inotify.Vue/src/layout/components/AppMain.vue new file mode 100644 index 0000000..f6a3286 --- /dev/null +++ b/Inotify.Vue/src/layout/components/AppMain.vue @@ -0,0 +1,40 @@ + + + + + + + diff --git a/Inotify.Vue/src/layout/components/Navbar.vue b/Inotify.Vue/src/layout/components/Navbar.vue new file mode 100644 index 0000000..38d9b76 --- /dev/null +++ b/Inotify.Vue/src/layout/components/Navbar.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/Inotify.Vue/src/layout/components/Sidebar/FixiOSBug.js b/Inotify.Vue/src/layout/components/Sidebar/FixiOSBug.js new file mode 100644 index 0000000..bc14856 --- /dev/null +++ b/Inotify.Vue/src/layout/components/Sidebar/FixiOSBug.js @@ -0,0 +1,26 @@ +export default { + computed: { + device() { + return this.$store.state.app.device + } + }, + mounted() { + // In order to fix the click on menu on the ios device will trigger the mouseleave bug + // https://github.com/PanJiaChen/vue-element-admin/issues/1135 + this.fixBugIniOS() + }, + methods: { + fixBugIniOS() { + const $subMenu = this.$refs.subMenu + if ($subMenu) { + const handleMouseleave = $subMenu.handleMouseleave + $subMenu.handleMouseleave = (e) => { + if (this.device === 'mobile') { + return + } + handleMouseleave(e) + } + } + } + } +} diff --git a/Inotify.Vue/src/layout/components/Sidebar/Item.vue b/Inotify.Vue/src/layout/components/Sidebar/Item.vue new file mode 100644 index 0000000..aa1f5da --- /dev/null +++ b/Inotify.Vue/src/layout/components/Sidebar/Item.vue @@ -0,0 +1,41 @@ + + + diff --git a/Inotify.Vue/src/layout/components/Sidebar/Link.vue b/Inotify.Vue/src/layout/components/Sidebar/Link.vue new file mode 100644 index 0000000..530b3d5 --- /dev/null +++ b/Inotify.Vue/src/layout/components/Sidebar/Link.vue @@ -0,0 +1,43 @@ + + + diff --git a/Inotify.Vue/src/layout/components/Sidebar/Logo.vue b/Inotify.Vue/src/layout/components/Sidebar/Logo.vue new file mode 100644 index 0000000..040fab6 --- /dev/null +++ b/Inotify.Vue/src/layout/components/Sidebar/Logo.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/Inotify.Vue/src/layout/components/Sidebar/SidebarItem.vue b/Inotify.Vue/src/layout/components/Sidebar/SidebarItem.vue new file mode 100644 index 0000000..392308f --- /dev/null +++ b/Inotify.Vue/src/layout/components/Sidebar/SidebarItem.vue @@ -0,0 +1,107 @@ + + + diff --git a/Inotify.Vue/src/layout/components/Sidebar/index.vue b/Inotify.Vue/src/layout/components/Sidebar/index.vue new file mode 100644 index 0000000..da39034 --- /dev/null +++ b/Inotify.Vue/src/layout/components/Sidebar/index.vue @@ -0,0 +1,56 @@ + + + diff --git a/Inotify.Vue/src/layout/components/index.js b/Inotify.Vue/src/layout/components/index.js new file mode 100644 index 0000000..97ee3cd --- /dev/null +++ b/Inotify.Vue/src/layout/components/index.js @@ -0,0 +1,3 @@ +export { default as Navbar } from './Navbar' +export { default as Sidebar } from './Sidebar' +export { default as AppMain } from './AppMain' diff --git a/Inotify.Vue/src/layout/index.vue b/Inotify.Vue/src/layout/index.vue new file mode 100644 index 0000000..db22a7b --- /dev/null +++ b/Inotify.Vue/src/layout/index.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/Inotify.Vue/src/layout/mixin/ResizeHandler.js b/Inotify.Vue/src/layout/mixin/ResizeHandler.js new file mode 100644 index 0000000..e8d0df8 --- /dev/null +++ b/Inotify.Vue/src/layout/mixin/ResizeHandler.js @@ -0,0 +1,45 @@ +import store from '@/store' + +const { body } = document +const WIDTH = 992 // refer to Bootstrap's responsive design + +export default { + watch: { + $route(route) { + if (this.device === 'mobile' && this.sidebar.opened) { + store.dispatch('app/closeSideBar', { withoutAnimation: false }) + } + } + }, + beforeMount() { + window.addEventListener('resize', this.$_resizeHandler) + }, + beforeDestroy() { + window.removeEventListener('resize', this.$_resizeHandler) + }, + mounted() { + const isMobile = this.$_isMobile() + if (isMobile) { + store.dispatch('app/toggleDevice', 'mobile') + store.dispatch('app/closeSideBar', { withoutAnimation: true }) + } + }, + methods: { + // use $_ for mixins properties + // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential + $_isMobile() { + const rect = body.getBoundingClientRect() + return rect.width - 1 < WIDTH + }, + $_resizeHandler() { + if (!document.hidden) { + const isMobile = this.$_isMobile() + store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') + + if (isMobile) { + store.dispatch('app/closeSideBar', { withoutAnimation: true }) + } + } + } + } +} diff --git a/Inotify.Vue/src/main.js b/Inotify.Vue/src/main.js new file mode 100644 index 0000000..4e1a5fd --- /dev/null +++ b/Inotify.Vue/src/main.js @@ -0,0 +1,42 @@ +import Vue from 'vue' +import 'normalize.css/normalize.css' // A modern alternative to CSS resets +import ElementUI from 'element-ui' + +import 'element-ui/lib/theme-chalk/index.css' +import locale from 'element-ui/lib/locale/lang/zh-CN' // lang i18n + +import '@/styles/index.scss' // global css + +import App from './App' +import store from './store' +import router from './router' + +import '@/icons' // icon +import '@/permission' // permission control + +/** + * If you don't want to use mock-server + * you want to use MockJs for mock api + * you can execute: mockXHR() + * + * Currently MockJs will be used in the production environment, + * please remove it before going online ! ! ! + */ +// if (process.env.NODE_ENV === 'production') { +// const { mockXHR } = require('../mock') +// mockXHR() +// } + +// set ElementUI lang to EN +Vue.use(ElementUI, { locale, size: 'small' }) +// 如果想要中文版 element-ui,按如下方式声明 +// Vue.use(ElementUI) + +Vue.config.productionTip = false + +new Vue({ + el: '#app', + router, + store, + render: h => h(App) +}) diff --git a/Inotify.Vue/src/permission.js b/Inotify.Vue/src/permission.js new file mode 100644 index 0000000..8b8b82b --- /dev/null +++ b/Inotify.Vue/src/permission.js @@ -0,0 +1,69 @@ +import router from './router' +import store from './store' +import { Message } from 'element-ui' +import NProgress from 'nprogress' // progress bar +import 'nprogress/nprogress.css' // progress bar style +import { getToken } from '@/utils/auth' // get token from cookie +import getPageTitle from '@/utils/get-page-title' + +NProgress.configure({ showSpinner: false }) +// NProgress Configuration +const whiteList = ['/login'] + +// no redirect whitelist +router.beforeEach(async (to, from, next) => { + // start progress bar + NProgress.start() + + // set page title + document.title = getPageTitle(to.meta.title) + + // determine whether the user has logged in + const hasToken = getToken() + if (hasToken) { + if (to.path === '/login') { + // if is logged in, redirect to the home page + next({ path: '/' }) + NProgress.done() + } else { + const hasGetUserInfo = store.getters.name + const role = store.getters.role + if (hasGetUserInfo) { + if (!to.meta.roles || to.meta.roles.includes(role)) { + next() + } else { + Message.error('未授权访问') + NProgress.done() + } + } else { + try { + // get user info + await store.dispatch('user/getInfo') + next() + } catch (error) { + // remove token and go to login page to re-login + await store.dispatch('user/resetToken') + Message.error(error || 'Has Error') + next(`/login?redirect=${to.path}`) + NProgress.done() + } + } + } + } else { + /* has no token*/ + + if (whiteList.indexOf(to.path) !== -1) { + // in the free login whitelist, go directly + next() + } else { + // other pages that do not have permission to access are redirected to the login page. + next(`/login?redirect=${to.path}`) + NProgress.done() + } + } +}) + +router.afterEach(() => { + // finish progress bar + NProgress.done() +}) diff --git a/Inotify.Vue/src/router/index.js b/Inotify.Vue/src/router/index.js new file mode 100644 index 0000000..46e8fd8 --- /dev/null +++ b/Inotify.Vue/src/router/index.js @@ -0,0 +1,154 @@ +import Vue from 'vue' +import Router from 'vue-router' + +Vue.use(Router) + +/* Layout */ +import Layout from '@/layout' + +/** + * Note: sub-menu only appear when route children.length >= 1 + * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html + * + * hidden: true if set true, item will not show in the sidebar(default is false) + * alwaysShow: true if set true, will always show the root menu + * if not set alwaysShow, when item has more than one children route, + * it will becomes nested mode, otherwise not show the root menu + * redirect: noRedirect if set noRedirect will no redirect in the breadcrumb + * name:'router-name' the name is used by (must set!!!) + * meta : { + roles: ['admin','editor'] control the page roles (you can set multiple roles) + title: 'title' the name show in sidebar and breadcrumb (recommend set) + icon: 'svg-name'/'el-icon-x' the icon show in the sidebar + breadcrumb: false if set false, the item will hidden in breadcrumb(default is true) + activeMenu: '/example/list' if set path, the sidebar will highlight the path you set + } + */ + +/** + * constantRoutes + * a base page that does not have permission requirements + * all roles can be accessed + */ +export const constantRoutes = [ + { + path: '/login', + component: () => import('@/views/login/index'), + hidden: true, + props: true + }, + { + path: '/404', + component: () => import('@/views/404'), + hidden: true + }, + { + path: '/', + component: Layout, + redirect: '/manager', + children: [ + { + path: 'manager', + name: 'Manager', + component: () => import('@/views/manager/settingpro/sendkey/index'), + meta: { roles: ['user', 'system'], title: '后台管理', icon: 'el-icon-s-home' } + } + ] + }, + { + path: '/settingpro', + component: Layout, + redirect: '/settingpro/sendmethods', + name: 'settingpro', + meta: { roles: ['user', 'system'], title: '个人设置', icon: 'el-icon-user' }, + children: [ + { + path: 'sendkey', + name: 'sendkey', + component: () => import('@/views/manager/settingpro/sendkey/index'), + meta: { roles: ['user', 'system'], title: '消息验证', icon: 'el-icon-s-comment' } + }, + { + path: 'sendmethods', + name: 'sendmethods', + component: () => import('@/views/manager/settingpro/sendauths/index'), + meta: { roles: ['user', 'system'], title: '消息通道', icon: 'el-icon-s-promotion' } + }, + { + path: 'oauthsetting', + name: 'oauthsetting', + component: () => import('@/views/manager/settingpro/oauthsetting/index'), + meta: { roles: ['user', 'system'], title: '重置密码', icon: 'el-icon-s-custom' } + } + ] + }, + { + path: '/settingsys', + component: Layout, + redirect: '/settingsys/systeminfo', + name: 'settingsys', + meta: { roles: ['system'], title: '系统设置', icon: 'el-icon-setting' }, + children: [ + { + path: 'systeminfo', + name: 'systeminfo', + component: () => import('@/views/manager/settingsys/systeminfo/index'), + meta: { roles: ['system'], title: '系统状态', icon: 'el-icon-s-data' } + }, + { + path: 'systemusers', + name: 'systemusers', + component: () => import('@/views/manager/settingsys/usermanager/index'), + meta: { roles: ['system'], title: '用户管理', icon: 'el-icon-s-check' } + }, + { + path: 'systemjwt', + name: 'systemjwt', + component: () => import('@/views/manager/settingsys/jwt/index'), + meta: { roles: ['system'], title: 'JWT验证', icon: 'el-icon-s-platform' } + }, + { + path: 'systemglobal', + name: 'systemglobal', + component: () => import('@/views/manager/settingsys/systemglobal/index'), + meta: { roles: ['system'], title: '全局参数', icon: 'el-icon-s-tools' } + } + ] + }, + { + path: 'external-link', + component: Layout, + children: [ + { + path: 'https://github.com/xpnas/Inotify', + meta: { title: '源码', icon: 'el-icon-share' } + } + ] + }, + + // 404 page must be placed at the end !!! + { path: '*', redirect: '/404', hidden: true } +] + +const createRouter = () => + new Router({ + // mode: 'history', // require service support + scrollBehavior: () => ({ y: 0 }), + routes: constantRoutes + }) + +const router = createRouter() + +// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465 +export function resetRouter() { + const newRouter = createRouter() + router.matcher = newRouter.matcher // reset router + router.push({ + name: '/login', + params: { + token: '' + } + }) +} + +export default router diff --git a/Inotify.Vue/src/settings.js b/Inotify.Vue/src/settings.js new file mode 100644 index 0000000..89a81b4 --- /dev/null +++ b/Inotify.Vue/src/settings.js @@ -0,0 +1,17 @@ +module.exports = { + + title: 'Inotify', + + /** + * @type {boolean} true | false + * @description Whether fix the header + */ + fixedHeader: false, + + /** + * @type {boolean} true | false + * @description Whether show the logo in sidebar + */ + sidebarLogo: false +} + diff --git a/Inotify.Vue/src/store/getters.js b/Inotify.Vue/src/store/getters.js new file mode 100644 index 0000000..7e96b44 --- /dev/null +++ b/Inotify.Vue/src/store/getters.js @@ -0,0 +1,9 @@ +const getters = { + sidebar: state => state.app.sidebar, + device: state => state.app.device, + token: state => state.user.token, + avatar: state => state.user.avatar, + name: state => state.user.name, + role: state => state.user.role +} +export default getters diff --git a/Inotify.Vue/src/store/index.js b/Inotify.Vue/src/store/index.js new file mode 100644 index 0000000..6be466a --- /dev/null +++ b/Inotify.Vue/src/store/index.js @@ -0,0 +1,19 @@ +import Vue from 'vue' +import Vuex from 'vuex' +import getters from './getters' +import app from './modules/app' +import settings from './modules/settings' +import user from './modules/user' + +Vue.use(Vuex) + +const store = new Vuex.Store({ + modules: { + app, + settings, + user + }, + getters +}) + +export default store diff --git a/Inotify.Vue/src/store/modules/app.js b/Inotify.Vue/src/store/modules/app.js new file mode 100644 index 0000000..7ea7e33 --- /dev/null +++ b/Inotify.Vue/src/store/modules/app.js @@ -0,0 +1,48 @@ +import Cookies from 'js-cookie' + +const state = { + sidebar: { + opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, + withoutAnimation: false + }, + device: 'desktop' +} + +const mutations = { + TOGGLE_SIDEBAR: state => { + state.sidebar.opened = !state.sidebar.opened + state.sidebar.withoutAnimation = false + if (state.sidebar.opened) { + Cookies.set('sidebarStatus', 1) + } else { + Cookies.set('sidebarStatus', 0) + } + }, + CLOSE_SIDEBAR: (state, withoutAnimation) => { + Cookies.set('sidebarStatus', 0) + state.sidebar.opened = false + state.sidebar.withoutAnimation = withoutAnimation + }, + TOGGLE_DEVICE: (state, device) => { + state.device = device + } +} + +const actions = { + toggleSideBar({ commit }) { + commit('TOGGLE_SIDEBAR') + }, + closeSideBar({ commit }, { withoutAnimation }) { + commit('CLOSE_SIDEBAR', withoutAnimation) + }, + toggleDevice({ commit }, device) { + commit('TOGGLE_DEVICE', device) + } +} + +export default { + namespaced: true, + state, + mutations, + actions +} diff --git a/Inotify.Vue/src/store/modules/settings.js b/Inotify.Vue/src/store/modules/settings.js new file mode 100644 index 0000000..b3f33f8 --- /dev/null +++ b/Inotify.Vue/src/store/modules/settings.js @@ -0,0 +1,32 @@ +import defaultSettings from '@/settings' + +const { showSettings, fixedHeader, sidebarLogo } = defaultSettings + +const state = { + showSettings: showSettings, + fixedHeader: fixedHeader, + sidebarLogo: sidebarLogo +} + +const mutations = { + CHANGE_SETTING: (state, { key, value }) => { + // eslint-disable-next-line no-prototype-builtins + if (state.hasOwnProperty(key)) { + state[key] = value + } + } +} + +const actions = { + changeSetting({ commit }, data) { + commit('CHANGE_SETTING', data) + } +} + +export default { + namespaced: true, + state, + mutations, + actions +} + diff --git a/Inotify.Vue/src/store/modules/user.js b/Inotify.Vue/src/store/modules/user.js new file mode 100644 index 0000000..0b6f225 --- /dev/null +++ b/Inotify.Vue/src/store/modules/user.js @@ -0,0 +1,125 @@ +import { login, logout, getInfo } from '@/api/user' +import { getToken, setToken, removeToken } from '@/utils/auth' +import { resetRouter } from '@/router' + +const getDefaultState = () => { + return { + token: getToken(), + name: '', + role: '', + avatar: '' + } +} + +const state = getDefaultState() + +const mutations = { + RESET_STATE: state => { + Object.assign(state, getDefaultState()) + }, + SET_TOKEN: (state, token) => { + state.token = token + }, + SET_ROLE: (state, role) => { + state.role = role + }, + SET_NAME: (state, name) => { + state.name = name + }, + SET_AVATAR: (state, avatar) => { + state.avatar = avatar + } +} + +const actions = { + // user login + login({ commit }, userInfo) { + const { username, password } = userInfo + return new Promise((resolve, reject) => { + login({ username: username.trim(), password: password }) + .then(response => { + const { data } = response + commit('SET_TOKEN', data.token) + commit('SET_ROLE', data.role) + setToken(data.token) + resolve() + }) + .catch(error => { + reject(error) + }) + }) + }, + thirdlogin({ commit }, token) { + commit('SET_TOKEN', token) + setToken(token) + return new Promise((resolve, reject) => { + getInfo(token) + .then(response => { + const { data } = response + if (!data) { + return reject('Verification failed, please Login again.') + } + const { name, role, avatar } = data + commit('SET_NAME', name) + commit('SET_ROLE', role) + commit('SET_AVATAR', avatar) + resolve(data) + }) + .catch(error => { + reject(error) + }) + }) + }, + // get user info + getInfo({ commit, state }) { + return new Promise((resolve, reject) => { + getInfo(state.token) + .then(response => { + const { data } = response + if (!data) { + return reject('Verification failed, please Login again.') + } + const { name, role, avatar } = data + commit('SET_NAME', name) + commit('SET_ROLE', role) + commit('SET_AVATAR', avatar) + resolve(data) + }) + .catch(error => { + reject(error) + }) + }) + }, + + // user logout + logout({ commit, state }) { + return new Promise((resolve, reject) => { + logout(state.token) + .then(() => { + removeToken() // must remove token first + resetRouter() + commit('RESET_STATE') + resolve() + }) + .catch(error => { + reject(error) + }) + }) + }, + + // remove token + resetToken({ commit }) { + return new Promise(resolve => { + removeToken() // must remove token first + commit('RESET_STATE') + resolve() + }) + } +} + +export default { + namespaced: true, + state, + mutations, + actions +} diff --git a/Inotify.Vue/src/styles/element-ui.scss b/Inotify.Vue/src/styles/element-ui.scss new file mode 100644 index 0000000..0062411 --- /dev/null +++ b/Inotify.Vue/src/styles/element-ui.scss @@ -0,0 +1,49 @@ +// cover some element-ui styles + +.el-breadcrumb__inner, +.el-breadcrumb__inner a { + font-weight: 400 !important; +} + +.el-upload { + input[type="file"] { + display: none !important; + } +} + +.el-upload__input { + display: none; +} + + +// to fixed https://github.com/ElemeFE/element/issues/2461 +.el-dialog { + transform: none; + left: 0; + position: relative; + margin: 0 auto; +} + +// refine element ui upload +.upload-container { + .el-upload { + width: 100%; + + .el-upload-dragger { + width: 100%; + height: 200px; + } + } +} + +// dropdown +.el-dropdown-menu { + a { + display: block + } +} + +// to fix el-date-picker css style +.el-range-separator { + box-sizing: content-box; +} diff --git a/Inotify.Vue/src/styles/index.scss b/Inotify.Vue/src/styles/index.scss new file mode 100644 index 0000000..3b4da51 --- /dev/null +++ b/Inotify.Vue/src/styles/index.scss @@ -0,0 +1,65 @@ +@import './variables.scss'; +@import './mixin.scss'; +@import './transition.scss'; +@import './element-ui.scss'; +@import './sidebar.scss'; + +body { + height: 100%; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; +} + +label { + font-weight: 700; +} + +html { + height: 100%; + box-sizing: border-box; +} + +#app { + height: 100%; +} + +*, +*:before, +*:after { + box-sizing: inherit; +} + +a:focus, +a:active { + outline: none; +} + +a, +a:focus, +a:hover { + cursor: pointer; + color: inherit; + text-decoration: none; +} + +div:focus { + outline: none; +} + +.clearfix { + &:after { + visibility: hidden; + display: block; + font-size: 0; + content: " "; + clear: both; + height: 0; + } +} + +// main-container global css +.app-container { + padding: 20px; +} diff --git a/Inotify.Vue/src/styles/mixin.scss b/Inotify.Vue/src/styles/mixin.scss new file mode 100644 index 0000000..36b74bb --- /dev/null +++ b/Inotify.Vue/src/styles/mixin.scss @@ -0,0 +1,28 @@ +@mixin clearfix { + &:after { + content: ""; + display: table; + clear: both; + } +} + +@mixin scrollBar { + &::-webkit-scrollbar-track-piece { + background: #d3dce6; + } + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-thumb { + background: #99a9bf; + border-radius: 20px; + } +} + +@mixin relative { + position: relative; + width: 100%; + height: 100%; +} diff --git a/Inotify.Vue/src/styles/sidebar.scss b/Inotify.Vue/src/styles/sidebar.scss new file mode 100644 index 0000000..94760cc --- /dev/null +++ b/Inotify.Vue/src/styles/sidebar.scss @@ -0,0 +1,226 @@ +#app { + + .main-container { + min-height: 100%; + transition: margin-left .28s; + margin-left: $sideBarWidth; + position: relative; + } + + .sidebar-container { + transition: width 0.28s; + width: $sideBarWidth !important; + background-color: $menuBg; + height: 100%; + position: fixed; + font-size: 0px; + top: 0; + bottom: 0; + left: 0; + z-index: 1001; + overflow: hidden; + + // reset element-ui css + .horizontal-collapse-transition { + transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; + } + + .scrollbar-wrapper { + overflow-x: hidden !important; + } + + .el-scrollbar__bar.is-vertical { + right: 0px; + } + + .el-scrollbar { + height: 100%; + } + + &.has-logo { + .el-scrollbar { + height: calc(100% - 50px); + } + } + + .is-horizontal { + display: none; + } + + a { + display: inline-block; + width: 100%; + overflow: hidden; + } + + .svg-icon { + margin-right: 16px; + } + + .sub-el-icon { + margin-right: 12px; + margin-left: -2px; + } + + .el-menu { + border: none; + height: 100%; + width: 100% !important; + } + + // menu hover + .submenu-title-noDropdown, + .el-submenu__title { + &:hover { + background-color: $menuHover !important; + } + } + + .is-active>.el-submenu__title { + color: $subMenuActiveText !important; + } + + & .nest-menu .el-submenu>.el-submenu__title, + & .el-submenu .el-menu-item { + min-width: $sideBarWidth !important; + background-color: $subMenuBg !important; + + &:hover { + background-color: $subMenuHover !important; + } + } + } + + .hideSidebar { + .sidebar-container { + width: 54px !important; + } + + .main-container { + margin-left: 54px; + } + + .submenu-title-noDropdown { + padding: 0 !important; + position: relative; + + .el-tooltip { + padding: 0 !important; + + .svg-icon { + margin-left: 20px; + } + + .sub-el-icon { + margin-left: 19px; + } + } + } + + .el-submenu { + overflow: hidden; + + &>.el-submenu__title { + padding: 0 !important; + + .svg-icon { + margin-left: 20px; + } + + .sub-el-icon { + margin-left: 19px; + } + + .el-submenu__icon-arrow { + display: none; + } + } + } + + .el-menu--collapse { + .el-submenu { + &>.el-submenu__title { + &>span { + height: 0; + width: 0; + overflow: hidden; + visibility: hidden; + display: inline-block; + } + } + } + } + } + + .el-menu--collapse .el-menu .el-submenu { + min-width: $sideBarWidth !important; + } + + // mobile responsive + .mobile { + .main-container { + margin-left: 0px; + } + + .sidebar-container { + transition: transform .28s; + width: $sideBarWidth !important; + } + + &.hideSidebar { + .sidebar-container { + pointer-events: none; + transition-duration: 0.3s; + transform: translate3d(-$sideBarWidth, 0, 0); + } + } + } + + .withoutAnimation { + + .main-container, + .sidebar-container { + transition: none; + } + } +} + +// when menu collapsed +.el-menu--vertical { + &>.el-menu { + .svg-icon { + margin-right: 16px; + } + .sub-el-icon { + margin-right: 12px; + margin-left: -2px; + } + } + + .nest-menu .el-submenu>.el-submenu__title, + .el-menu-item { + &:hover { + // you can use $subMenuHover + background-color: $menuHover !important; + } + } + + // the scroll bar appears when the subMenu is too long + >.el-menu--popup { + max-height: 100vh; + overflow-y: auto; + + &::-webkit-scrollbar-track-piece { + background: #d3dce6; + } + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-thumb { + background: #99a9bf; + border-radius: 20px; + } + } +} diff --git a/Inotify.Vue/src/styles/transition.scss b/Inotify.Vue/src/styles/transition.scss new file mode 100644 index 0000000..4cb27cc --- /dev/null +++ b/Inotify.Vue/src/styles/transition.scss @@ -0,0 +1,48 @@ +// global transition css + +/* fade */ +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.28s; +} + +.fade-enter, +.fade-leave-active { + opacity: 0; +} + +/* fade-transform */ +.fade-transform-leave-active, +.fade-transform-enter-active { + transition: all .5s; +} + +.fade-transform-enter { + opacity: 0; + transform: translateX(-30px); +} + +.fade-transform-leave-to { + opacity: 0; + transform: translateX(30px); +} + +/* breadcrumb transition */ +.breadcrumb-enter-active, +.breadcrumb-leave-active { + transition: all .5s; +} + +.breadcrumb-enter, +.breadcrumb-leave-active { + opacity: 0; + transform: translateX(20px); +} + +.breadcrumb-move { + transition: all .5s; +} + +.breadcrumb-leave-active { + position: absolute; +} diff --git a/Inotify.Vue/src/styles/variables.scss b/Inotify.Vue/src/styles/variables.scss new file mode 100644 index 0000000..be55772 --- /dev/null +++ b/Inotify.Vue/src/styles/variables.scss @@ -0,0 +1,25 @@ +// sidebar +$menuText:#bfcbd9; +$menuActiveText:#409EFF; +$subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951 + +$menuBg:#304156; +$menuHover:#263445; + +$subMenuBg:#1f2d3d; +$subMenuHover:#001528; + +$sideBarWidth: 210px; + +// the :export directive is the magic sauce for webpack +// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass +:export { + menuText: $menuText; + menuActiveText: $menuActiveText; + subMenuActiveText: $subMenuActiveText; + menuBg: $menuBg; + menuHover: $menuHover; + subMenuBg: $subMenuBg; + subMenuHover: $subMenuHover; + sideBarWidth: $sideBarWidth; +} diff --git a/Inotify.Vue/src/utils/auth.js b/Inotify.Vue/src/utils/auth.js new file mode 100644 index 0000000..302ebb2 --- /dev/null +++ b/Inotify.Vue/src/utils/auth.js @@ -0,0 +1,15 @@ +import Cookies from 'js-cookie' + +const TokenKey = 'JWTToken' + +export function getToken() { + return Cookies.get(TokenKey) +} + +export function setToken(token) { + return Cookies.set(TokenKey, token) +} + +export function removeToken() { + return Cookies.remove(TokenKey) +} diff --git a/Inotify.Vue/src/utils/get-page-title.js b/Inotify.Vue/src/utils/get-page-title.js new file mode 100644 index 0000000..a6de99d --- /dev/null +++ b/Inotify.Vue/src/utils/get-page-title.js @@ -0,0 +1,10 @@ +import defaultSettings from '@/settings' + +const title = defaultSettings.title || 'Vue Admin Template' + +export default function getPageTitle(pageTitle) { + if (pageTitle) { + return `${pageTitle} - ${title}` + } + return `${title}` +} diff --git a/Inotify.Vue/src/utils/index.js b/Inotify.Vue/src/utils/index.js new file mode 100644 index 0000000..4830c04 --- /dev/null +++ b/Inotify.Vue/src/utils/index.js @@ -0,0 +1,117 @@ +/** + * Created by PanJiaChen on 16/11/18. + */ + +/** + * Parse the time to string + * @param {(Object|string|number)} time + * @param {string} cFormat + * @returns {string | null} + */ +export function parseTime(time, cFormat) { + if (arguments.length === 0 || !time) { + return null + } + const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' + let date + if (typeof time === 'object') { + date = time + } else { + if ((typeof time === 'string')) { + if ((/^[0-9]+$/.test(time))) { + // support "1548221490638" + time = parseInt(time) + } else { + // support safari + // https://stackoverflow.com/questions/4310953/invalid-date-in-safari + time = time.replace(new RegExp(/-/gm), '/') + } + } + + if ((typeof time === 'number') && (time.toString().length === 10)) { + time = time * 1000 + } + date = new Date(time) + } + const formatObj = { + y: date.getFullYear(), + m: date.getMonth() + 1, + d: date.getDate(), + h: date.getHours(), + i: date.getMinutes(), + s: date.getSeconds(), + a: date.getDay() + } + const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => { + const value = formatObj[key] + // Note: getDay() returns 0 on Sunday + if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] } + return value.toString().padStart(2, '0') + }) + return time_str +} + +/** + * @param {number} time + * @param {string} option + * @returns {string} + */ +export function formatTime(time, option) { + if (('' + time).length === 10) { + time = parseInt(time) * 1000 + } else { + time = +time + } + const d = new Date(time) + const now = Date.now() + + const diff = (now - d) / 1000 + + if (diff < 30) { + return '刚刚' + } else if (diff < 3600) { + // less 1 hour + return Math.ceil(diff / 60) + '分钟前' + } else if (diff < 3600 * 24) { + return Math.ceil(diff / 3600) + '小时前' + } else if (diff < 3600 * 24 * 2) { + return '1天前' + } + if (option) { + return parseTime(time, option) + } else { + return ( + d.getMonth() + + 1 + + '月' + + d.getDate() + + '日' + + d.getHours() + + '时' + + d.getMinutes() + + '分' + ) + } +} + +/** + * @param {string} url + * @returns {Object} + */ +export function param2Obj(url) { + const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') + if (!search) { + return {} + } + const obj = {} + const searchArr = search.split('&') + searchArr.forEach(v => { + const index = v.indexOf('=') + if (index !== -1) { + const name = v.substring(0, index) + const val = v.substring(index + 1, v.length) + obj[name] = val + } + }) + return obj +} diff --git a/Inotify.Vue/src/utils/request.js b/Inotify.Vue/src/utils/request.js new file mode 100644 index 0000000..a944f33 --- /dev/null +++ b/Inotify.Vue/src/utils/request.js @@ -0,0 +1,85 @@ +import axios from 'axios' +import { MessageBox, Message } from 'element-ui' +import store from '@/store' +import { getToken } from '@/utils/auth' + +// create an axios instance +const service = axios.create({ + baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url + // withCredentials: true, // send cookies when cross-domain requests + timeout: 5000 // request timeout +}) + +// request interceptor +service.interceptors.request.use( + config => { + // do something before request is sent + if (store.getters.token) { + // let each request carry token + // ['X-Token'] is a custom headers key + // please modify it according to the actual situation + config.headers.Authorization = `bearer ${getToken()}` + } + return config + }, + error => { + // do something with request error + console.log(error) // for debug + return Promise.reject(error) + } +) + +// response interceptor +service.interceptors.response.use( + /** + * If you want to get http information such as headers or status + * Please return response => response + */ + + /** + * Determine the request status by custom code + * Here is just an example + * You can also judge the status by HTTP Status Code + */ + response => { + const res = response.data + + if (res.code === 403) { + MessageBox.confirm('您已登出, 需要重新登陆', '确认登出', { + confirmButtonText: '重新登陆', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + store.dispatch('user/resetToken').then(() => { + location.reload() + }) + }) + } else if (res.code === 405) { + Message({ + message: res.message || '无权访问', + type: 'error', + duration: 5 * 1000 + }) + } else if (res.code !== 200) { + Message({ + message: res.message || '系统错误,请联系管理员', + type: 'error', + duration: 5 * 1000 + }) + return Promise.reject(new Error(res.message || 'Error')) + } else { + return res + } + }, + error => { + console.log('err' + error) // for debug + Message({ + message: error.message, + type: 'error', + duration: 5 * 1000 + }) + return Promise.reject(error) + } +) + +export default service diff --git a/Inotify.Vue/src/utils/validate.js b/Inotify.Vue/src/utils/validate.js new file mode 100644 index 0000000..a6d41ba --- /dev/null +++ b/Inotify.Vue/src/utils/validate.js @@ -0,0 +1,19 @@ +/** + * Created by PanJiaChen on 16/11/18. + */ + +/** + * @param {string} path + * @returns {Boolean} + */ +export function isExternal(path) { + return /^(https?:|mailto:|tel:)/.test(path) +} + +/** + * @param {string} str + * @returns {Boolean} + */ +export function validUsername(str) { + return str.trim().length >= 0 +} diff --git a/Inotify.Vue/src/views/404.vue b/Inotify.Vue/src/views/404.vue new file mode 100644 index 0000000..1791f55 --- /dev/null +++ b/Inotify.Vue/src/views/404.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/Inotify.Vue/src/views/login/index.vue b/Inotify.Vue/src/views/login/index.vue new file mode 100644 index 0000000..e69081e --- /dev/null +++ b/Inotify.Vue/src/views/login/index.vue @@ -0,0 +1,256 @@ + + + + + diff --git a/Inotify.Vue/src/views/manager/index.vue b/Inotify.Vue/src/views/manager/index.vue new file mode 100644 index 0000000..1f6254e --- /dev/null +++ b/Inotify.Vue/src/views/manager/index.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/Inotify.Vue/src/views/manager/settingpro/oauthsetting/index.vue b/Inotify.Vue/src/views/manager/settingpro/oauthsetting/index.vue new file mode 100644 index 0000000..9b49b45 --- /dev/null +++ b/Inotify.Vue/src/views/manager/settingpro/oauthsetting/index.vue @@ -0,0 +1,100 @@ + + + diff --git a/Inotify.Vue/src/views/manager/settingpro/sendauths/index.vue b/Inotify.Vue/src/views/manager/settingpro/sendauths/index.vue new file mode 100644 index 0000000..36577b6 --- /dev/null +++ b/Inotify.Vue/src/views/manager/settingpro/sendauths/index.vue @@ -0,0 +1,236 @@ + + + + diff --git a/Inotify.Vue/src/views/manager/settingpro/sendkey/index.vue b/Inotify.Vue/src/views/manager/settingpro/sendkey/index.vue new file mode 100644 index 0000000..2f54288 --- /dev/null +++ b/Inotify.Vue/src/views/manager/settingpro/sendkey/index.vue @@ -0,0 +1,155 @@ + + + + + diff --git a/Inotify.Vue/src/views/manager/settingsys/jwt/index.vue b/Inotify.Vue/src/views/manager/settingsys/jwt/index.vue new file mode 100644 index 0000000..eaf00d8 --- /dev/null +++ b/Inotify.Vue/src/views/manager/settingsys/jwt/index.vue @@ -0,0 +1,113 @@ + + + diff --git a/Inotify.Vue/src/views/manager/settingsys/systemglobal/index.vue b/Inotify.Vue/src/views/manager/settingsys/systemglobal/index.vue new file mode 100644 index 0000000..b9d301e --- /dev/null +++ b/Inotify.Vue/src/views/manager/settingsys/systemglobal/index.vue @@ -0,0 +1,109 @@ + + + diff --git a/Inotify.Vue/src/views/manager/settingsys/systeminfo/index.vue b/Inotify.Vue/src/views/manager/settingsys/systeminfo/index.vue new file mode 100644 index 0000000..1c922c0 --- /dev/null +++ b/Inotify.Vue/src/views/manager/settingsys/systeminfo/index.vue @@ -0,0 +1,98 @@ + + + diff --git a/Inotify.Vue/src/views/manager/settingsys/usermanager/index.vue b/Inotify.Vue/src/views/manager/settingsys/usermanager/index.vue new file mode 100644 index 0000000..839f373 --- /dev/null +++ b/Inotify.Vue/src/views/manager/settingsys/usermanager/index.vue @@ -0,0 +1,135 @@ + + + diff --git a/Inotify.Vue/tests/unit/.eslintrc.js b/Inotify.Vue/tests/unit/.eslintrc.js new file mode 100644 index 0000000..958d51b --- /dev/null +++ b/Inotify.Vue/tests/unit/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + env: { + jest: true + } +} diff --git a/Inotify.Vue/tests/unit/components/Breadcrumb.spec.js b/Inotify.Vue/tests/unit/components/Breadcrumb.spec.js new file mode 100644 index 0000000..1d94c8f --- /dev/null +++ b/Inotify.Vue/tests/unit/components/Breadcrumb.spec.js @@ -0,0 +1,98 @@ +import { mount, createLocalVue } from '@vue/test-utils' +import VueRouter from 'vue-router' +import ElementUI from 'element-ui' +import Breadcrumb from '@/components/Breadcrumb/index.vue' + +const localVue = createLocalVue() +localVue.use(VueRouter) +localVue.use(ElementUI) + +const routes = [ + { + path: '/', + name: 'home', + children: [{ + path: 'dashboard', + name: 'dashboard' + }] + }, + { + path: '/menu', + name: 'menu', + children: [{ + path: 'menu1', + name: 'menu1', + meta: { title: 'menu1' }, + children: [{ + path: 'menu1-1', + name: 'menu1-1', + meta: { title: 'menu1-1' } + }, + { + path: 'menu1-2', + name: 'menu1-2', + redirect: 'noredirect', + meta: { title: 'menu1-2' }, + children: [{ + path: 'menu1-2-1', + name: 'menu1-2-1', + meta: { title: 'menu1-2-1' } + }, + { + path: 'menu1-2-2', + name: 'menu1-2-2' + }] + }] + }] + }] + +const router = new VueRouter({ + routes +}) + +describe('Breadcrumb.vue', () => { + const wrapper = mount(Breadcrumb, { + localVue, + router + }) + it('dashboard', () => { + router.push('/dashboard') + const len = wrapper.findAll('.el-breadcrumb__inner').length + expect(len).toBe(1) + }) + it('normal route', () => { + router.push('/menu/menu1') + const len = wrapper.findAll('.el-breadcrumb__inner').length + expect(len).toBe(2) + }) + it('nested route', () => { + router.push('/menu/menu1/menu1-2/menu1-2-1') + const len = wrapper.findAll('.el-breadcrumb__inner').length + expect(len).toBe(4) + }) + it('no meta.title', () => { + router.push('/menu/menu1/menu1-2/menu1-2-2') + const len = wrapper.findAll('.el-breadcrumb__inner').length + expect(len).toBe(3) + }) + // it('click link', () => { + // router.push('/menu/menu1/menu1-2/menu1-2-2') + // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') + // const second = breadcrumbArray.at(1) + // console.log(breadcrumbArray) + // const href = second.find('a').attributes().href + // expect(href).toBe('#/menu/menu1') + // }) + // it('noRedirect', () => { + // router.push('/menu/menu1/menu1-2/menu1-2-1') + // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') + // const redirectBreadcrumb = breadcrumbArray.at(2) + // expect(redirectBreadcrumb.contains('a')).toBe(false) + // }) + it('last breadcrumb', () => { + router.push('/menu/menu1/menu1-2/menu1-2-1') + const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') + const redirectBreadcrumb = breadcrumbArray.at(3) + expect(redirectBreadcrumb.contains('a')).toBe(false) + }) +}) diff --git a/Inotify.Vue/tests/unit/components/Hamburger.spec.js b/Inotify.Vue/tests/unit/components/Hamburger.spec.js new file mode 100644 index 0000000..01ea303 --- /dev/null +++ b/Inotify.Vue/tests/unit/components/Hamburger.spec.js @@ -0,0 +1,18 @@ +import { shallowMount } from '@vue/test-utils' +import Hamburger from '@/components/Hamburger/index.vue' +describe('Hamburger.vue', () => { + it('toggle click', () => { + const wrapper = shallowMount(Hamburger) + const mockFn = jest.fn() + wrapper.vm.$on('toggleClick', mockFn) + wrapper.find('.hamburger').trigger('click') + expect(mockFn).toBeCalled() + }) + it('prop isActive', () => { + const wrapper = shallowMount(Hamburger) + wrapper.setProps({ isActive: true }) + expect(wrapper.contains('.is-active')).toBe(true) + wrapper.setProps({ isActive: false }) + expect(wrapper.contains('.is-active')).toBe(false) + }) +}) diff --git a/Inotify.Vue/tests/unit/components/SvgIcon.spec.js b/Inotify.Vue/tests/unit/components/SvgIcon.spec.js new file mode 100644 index 0000000..31467a9 --- /dev/null +++ b/Inotify.Vue/tests/unit/components/SvgIcon.spec.js @@ -0,0 +1,22 @@ +import { shallowMount } from '@vue/test-utils' +import SvgIcon from '@/components/SvgIcon/index.vue' +describe('SvgIcon.vue', () => { + it('iconClass', () => { + const wrapper = shallowMount(SvgIcon, { + propsData: { + iconClass: 'test' + } + }) + expect(wrapper.find('use').attributes().href).toBe('#icon-test') + }) + it('className', () => { + const wrapper = shallowMount(SvgIcon, { + propsData: { + iconClass: 'test' + } + }) + expect(wrapper.classes().length).toBe(1) + wrapper.setProps({ className: 'test' }) + expect(wrapper.classes().includes('test')).toBe(true) + }) +}) diff --git a/Inotify.Vue/tests/unit/utils/formatTime.spec.js b/Inotify.Vue/tests/unit/utils/formatTime.spec.js new file mode 100644 index 0000000..24e165b --- /dev/null +++ b/Inotify.Vue/tests/unit/utils/formatTime.spec.js @@ -0,0 +1,30 @@ +import { formatTime } from '@/utils/index.js' + +describe('Utils:formatTime', () => { + const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" + const retrofit = 5 * 1000 + + it('ten digits timestamp', () => { + expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分') + }) + it('test now', () => { + expect(formatTime(+new Date() - 1)).toBe('刚刚') + }) + it('less two minute', () => { + expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前') + }) + it('less two hour', () => { + expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前') + }) + it('less one day', () => { + expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前') + }) + it('more than one day', () => { + expect(formatTime(d)).toBe('7月13日17时54分') + }) + it('format', () => { + expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') + expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') + expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') + }) +}) diff --git a/Inotify.Vue/tests/unit/utils/param2Obj.spec.js b/Inotify.Vue/tests/unit/utils/param2Obj.spec.js new file mode 100644 index 0000000..e106ed8 --- /dev/null +++ b/Inotify.Vue/tests/unit/utils/param2Obj.spec.js @@ -0,0 +1,14 @@ +import { param2Obj } from '@/utils/index.js' +describe('Utils:param2Obj', () => { + const url = 'https://github.com/PanJiaChen/vue-element-admin?name=bill&age=29&sex=1&field=dGVzdA==&key=%E6%B5%8B%E8%AF%95' + + it('param2Obj test', () => { + expect(param2Obj(url)).toEqual({ + name: 'bill', + age: '29', + sex: '1', + field: window.btoa('test'), + key: '测试' + }) + }) +}) diff --git a/Inotify.Vue/tests/unit/utils/parseTime.spec.js b/Inotify.Vue/tests/unit/utils/parseTime.spec.js new file mode 100644 index 0000000..56045af --- /dev/null +++ b/Inotify.Vue/tests/unit/utils/parseTime.spec.js @@ -0,0 +1,35 @@ +import { parseTime } from '@/utils/index.js' + +describe('Utils:parseTime', () => { + const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" + it('timestamp', () => { + expect(parseTime(d)).toBe('2018-07-13 17:54:01') + }) + it('timestamp string', () => { + expect(parseTime((d + ''))).toBe('2018-07-13 17:54:01') + }) + it('ten digits timestamp', () => { + expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01') + }) + it('new Date', () => { + expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01') + }) + it('format', () => { + expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') + expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') + expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') + }) + it('get the day of the week', () => { + expect(parseTime(d, '{a}')).toBe('五') // 星期五 + }) + it('get the day of the week', () => { + expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日 + }) + it('empty argument', () => { + expect(parseTime()).toBeNull() + }) + + it('null', () => { + expect(parseTime(null)).toBeNull() + }) +}) diff --git a/Inotify.Vue/tests/unit/utils/validate.spec.js b/Inotify.Vue/tests/unit/utils/validate.spec.js new file mode 100644 index 0000000..f774905 --- /dev/null +++ b/Inotify.Vue/tests/unit/utils/validate.spec.js @@ -0,0 +1,17 @@ +import { validUsername, isExternal } from '@/utils/validate.js' + +describe('Utils:validate', () => { + it('validUsername', () => { + expect(validUsername('admin')).toBe(true) + expect(validUsername('editor')).toBe(true) + expect(validUsername('xxxx')).toBe(false) + }) + it('isExternal', () => { + expect(isExternal('https://github.com/PanJiaChen/vue-element-admin')).toBe(true) + expect(isExternal('http://github.com/PanJiaChen/vue-element-admin')).toBe(true) + expect(isExternal('github.com/PanJiaChen/vue-element-admin')).toBe(false) + expect(isExternal('/dashboard')).toBe(false) + expect(isExternal('./dashboard')).toBe(false) + expect(isExternal('dashboard')).toBe(false) + }) +}) diff --git a/Inotify.Vue/vue.config.js b/Inotify.Vue/vue.config.js new file mode 100644 index 0000000..c3086e9 --- /dev/null +++ b/Inotify.Vue/vue.config.js @@ -0,0 +1,133 @@ +'use strict' +const path = require('path') +const defaultSettings = require('./src/settings.js') + +function resolve(dir) { + return path.join(__dirname, dir) +} + +const name = defaultSettings.title || 'Inotify' // page title + +// If your port is set to 80, +// use administrator privileges to execute the command line. +// For example, Mac: sudo npm run +// You can change the port by the following methods: +// port = 9528 npm run dev OR npm run dev --port = 9528 +const port = process.env.port || process.env.npm_config_port || 9528 // dev port + +// All configuration item explanations can be find in https://cli.vuejs.org/config/ +module.exports = { + /** + * You will need to set publicPath if you plan to deploy your site under a sub path, + * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/, + * then publicPath should be set to "/bar/". + * In most cases please use '/' !!! + * Detail: https://cli.vuejs.org/config/#publicpath + */ + publicPath: '/', + outputDir: 'dist', + assetsDir: 'static', + lintOnSave: process.env.NODE_ENV === 'development', + productionSourceMap: false, + devServer: { + port: port, + open: true, + overlay: { + warnings: false, + errors: true + }, + before: require('./mock/mock-server.js') + }, + configureWebpack: { + // provide the app's title in webpack's name field, so that + // it can be accessed in index.html to inject the correct title. + name: name, + resolve: { + alias: { + '@': resolve('src') + } + }, + devtool: 'source-map', + performance: { + hints: 'warning', // 枚举 + hints: 'error', // 性能提示中抛出错误 + hints: false, // 关闭性能提示 + maxAssetSize: 200000, // 整数类型(以字节为单位) + maxEntrypointSize: 400000, // 整数类型(以字节为单位) + assetFilter: function(assetFilename) { + // 提供资源文件名的断言函数 + return assetFilename.endsWith('.css') || assetFilename.endsWith('.js') + } + } + }, + chainWebpack(config) { + // it can improve the speed of the first screen, it is recommended to turn on preload + config.plugin('preload').tap(() => [ + { + rel: 'preload', + // to ignore runtime.js + // https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171 + fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/], + include: 'initial' + } + ]) + + // when there are many pages, it will cause too many meaningless requests + config.plugins.delete('prefetch') + + // set svg-sprite-loader + config.module + .rule('svg') + .exclude.add(resolve('src/icons')) + .end() + config.module + .rule('icons') + .test(/\.svg$/) + .include.add(resolve('src/icons')) + .end() + .use('svg-sprite-loader') + .loader('svg-sprite-loader') + .options({ + symbolId: 'icon-[name]' + }) + .end() + + config.when(process.env.NODE_ENV !== 'development', config => { + config + .plugin('ScriptExtHtmlWebpackPlugin') + .after('html') + .use('script-ext-html-webpack-plugin', [ + { + // `runtime` must same as runtimeChunk name. default is `runtime` + inline: /runtime\..*\.js$/ + } + ]) + .end() + config.optimization.splitChunks({ + chunks: 'all', + cacheGroups: { + libs: { + name: 'chunk-libs', + test: /[\\/]node_modules[\\/]/, + priority: 10, + chunks: 'initial' // only package third parties that are initially dependent + }, + elementUI: { + name: 'chunk-elementUI', // split elementUI into a single package + priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app + test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm + }, + commons: { + name: 'chunk-commons', + test: resolve('src/components'), // can customize your rules + minChunks: 3, // minimum common number + priority: 5, + reuseExistingChunk: true + } + } + }) + // https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk + config.optimization.runtimeChunk('single') + }) + } +} diff --git a/Inotify/.editorconfig b/Inotify/.editorconfig new file mode 100644 index 0000000..af9eccc --- /dev/null +++ b/Inotify/.editorconfig @@ -0,0 +1,19 @@ +[*.cs] + +# CS8618: 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑声明为可以为 null。 +dotnet_diagnostic.CS8618.severity = none + +# CS8603: 可能的 null 引用返回。 +dotnet_diagnostic.CS8603.severity = none + +# CS8625: 无法将 null 文本转换为不可为 null 的引用类型。 +dotnet_diagnostic.CS8625.severity = none + +# CS8604: 可能的 null 引用参数。 +dotnet_diagnostic.CS8604.severity = none + +# CS8602: 解引用可能出现空引用。 +dotnet_diagnostic.CS8602.severity = none + +# CS8600: 将 null 文本或可能的 null 值转换为不可为 null 类型。 +dotnet_diagnostic.CS8600.severity = none diff --git a/Inotify/.gitignore b/Inotify/.gitignore new file mode 100644 index 0000000..14faed6 --- /dev/null +++ b/Inotify/.gitignore @@ -0,0 +1,80 @@ +# Build and Object Folders +bin/ +obj/ + +# Nuget packages directory +packages/ + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Rr]elease/ +x64/ +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.log +*.vspscc +*.vssscc +.builds + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper* + +# NCrunch +*.ncrunch* +.*crunch*.local.xml + +# Installshield output folder +[Ee]xpress + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help +UpgradeLog*.XML + +# Lightswitch +_Pvt_Extensions +GeneratedArtifacts +*.xap +ModelManifest.xml + +#Backup file +*.bak + +#zzzili +v15/ diff --git a/Inotify/Common/Extensions.cs b/Inotify/Common/Extensions.cs new file mode 100644 index 0000000..ff7debc --- /dev/null +++ b/Inotify/Common/Extensions.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace Inotify.Common +{ + public static class Extensions + { + /// + /// MD5加密字符串(32位大写) + /// + /// 源字符串 + /// 加密后的字符串 + public static string ToMd5(this string source) + { + MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider(); + byte[] bytes = Encoding.UTF8.GetBytes(source); + string result = BitConverter.ToString(md5.ComputeHash(bytes)); + return result.Replace("-", ""); + } + + } +} diff --git a/Inotify/Controllers/BaseController.cs b/Inotify/Controllers/BaseController.cs new file mode 100644 index 0000000..752c9a3 --- /dev/null +++ b/Inotify/Controllers/BaseController.cs @@ -0,0 +1,106 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Inotify.Controllers +{ + public static class Roles + { + static Roles() + { + User = "user"; + System = "system"; + + } + public static string User { get; set; } + + public static string System { get; set; } + + public static string SystemOrUser { get; set; } + + } + + public static class Policys + { + + public const string Users = "users"; + + public const string Systems = "systems"; + + public const string SystemOrUsers = "SystemOrUsers"; + + public const string All = "all"; + } + + + public class BaseController : ControllerBase + { + public string UserName + { + get + { + var principal = HttpContext.User; + if (principal != null) + { + return principal.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Name)?.Value; + } + return null; + } + } + + public string Token + { + get + { + var principal = HttpContext.User; + if (principal != null) + { + return principal.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Sid)?.Value; + } + return null; + } + } + + protected JsonResult OK(object? obj = null) + { + return new JsonResult(new + { + code = 200, + data = obj ?? "sucess" + }); + } + + protected JsonResult Fail() + { + return new JsonResult(new + { + code = 404, + data = "fail" + }); + } + + protected JsonResult Fail(int code) + { + return new JsonResult(new + { + code, + data = "fail" + }); + } + + protected JsonResult Fail(int code, string message) + { + return new JsonResult(new + { + code, + message + }); + } + + + + } +} diff --git a/Inotify/Controllers/OAuthControlor.cs b/Inotify/Controllers/OAuthControlor.cs new file mode 100644 index 0000000..129737e --- /dev/null +++ b/Inotify/Controllers/OAuthControlor.cs @@ -0,0 +1,238 @@ +using Inotify.Common; +using Inotify.Data; +using Inotify.Data.Models; +using Inotify.Sends; +using Inotify.ThridOauth.Common; +using Inotify.ThridOauth.IService; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json.Linq; +using NPoco; +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; + +namespace Inotify.Controllers +{ + [ApiController] + [Route("api/oauth")] + public class OAuthController : BaseController + { + private readonly IGitHubLogin m_gitHubLogin; + + private readonly IConfiguration m_configuration; + + public OAuthController(IGitHubLogin gitHubLogin, IConfiguration configuration) + { + m_gitHubLogin = gitHubLogin; + m_configuration = configuration; + } + + [HttpPost, Route("Login")] + public JsonResult Login(string username, string password) + { + var md5 = password.ToMd5(); + + var userInfo = DBManager.Instance.DBase.Query().FirstOrDefault(e => e.UserName == username); + if (userInfo != null) + { + if (!userInfo.Active) + return Fail(401, "用户被禁用"); + if (userInfo.Password == password.ToMd5()) + { + var token = GenToken(username); + var role = GetRole(username); + return OK(new { name = username, role, token }); + } + else + { + return Fail(401, "用户密码错误"); + } + } + return Fail(401, "用户名不存在"); + } + + [HttpGet, Route("GithubEnable")] + public JsonResult GithubEnable() + { + return OK(SendCacheStore.GetSystemValue("githubEnable") == "true"); + } + + [HttpGet, Route("GithubLogin")] + public JsonResult GitHubLogin(string? code) + { + if (SendCacheStore.GetSystemValue("githubEnable") != "true") + { + return Fail(401, "未启用GITHUB登陆"); + } + + if (UserName != null && Token != null) + { + var direct = string.Format("/#login?token={0}", Token); + HttpContext.Response.Redirect(direct, true); + return OK(); + } + else + { + if (string.IsNullOrEmpty(code)) + { + return OK(m_gitHubLogin.GetOauthUrl()); + } + else + { + var res = m_gitHubLogin.Authorize(); + if (res != null && res.Result != null && res.Code == Code.Success) + { + string? githubUserName = null; + string? avtar = null; + string email = ""; + if (res.Result.TryGetValue("login", out JToken? jToken)) + githubUserName = jToken.ToString(); + + if (res.Result.TryGetValue("avatar_url", out jToken)) + avtar = jToken.ToString(); + + if (res.Result.TryGetValue("email", out jToken)) + email = jToken.ToString(); + + if (githubUserName != null && avtar != null) + { + SendUserInfo user; + if (DBManager.Instance.IsUser(githubUserName)) + { + user = DBManager.Instance.DBase.Query().FirstOrDefault(e => e.UserName == githubUserName); + user.Avatar = avtar; + DBManager.Instance.DBase.Update(user); + if (!user.Active) + return Fail(401, "用户被禁用"); + } + else + { + user = new SendUserInfo() + { + UserName = githubUserName, + Avatar = avtar, + Password = "123456".ToMd5(), + Email = email, + Active = true, + Token = Guid.NewGuid().ToString("N").ToUpper(), + CreateTime = DateTime.Now + }; + DBManager.Instance.DBase.Insert(user); + } + + var token = GenToken(user.UserName); + var direct = string.Format("/#login?token={0}", token); + HttpContext.Response.Redirect(direct, true); + return OK(user); + } + } + } + } + return Fail(401, "Github登陆失败"); + } + + [HttpPost, Route("ResetPassword")] + public JsonResult ResetPassword(string password) + { + if (UserName != null) + { + var userInfo = DBManager.Instance.DBase.Query().FirstOrDefault(e => e.UserName == UserName); + + if (userInfo != null) + { + var md5 = password.ToMd5(); + userInfo.Password = md5; + DBManager.Instance.DBase.Update(userInfo); + return OK(); + } + } + return Fail(401); + } + + [HttpGet, Route("Info"), Authorize(Policys.All)] + public JsonResult Info(string? token) + { + if (string.IsNullOrWhiteSpace(token)) + { + throw new ArgumentException($"“{nameof(token)}”不能为 Null 或空白", nameof(token)); + } + + if (UserName != null) + { + var user = DBManager.Instance.DBase.Query().FirstOrDefault(e => e.UserName == UserName); + if (user != null) + { + var role = GetRole(user.UserName); + return OK(new { name = user.UserName, role, avatar = user.Avatar }); + } + } + + return Fail(401, "登陆失败"); + } + + [HttpPost, Route("Logout"), Authorize(Policys.All)] + public JsonResult Logout() + { + if (UserName != null) + { + HttpContext.SignOutAsync(); + return OK("登出成功"); + } + return Fail(401, "您还未登录"); + } + + private string GenToken(string userName) + { + string role = GetRole(userName); + + var claims = new[] + { + new Claim(ClaimTypes.Name, userName), + new Claim(ClaimTypes.Role, role), + new Claim(JwtRegisteredClaimNames.Exp, $"{new DateTimeOffset(DateTime.Now.AddMinutes(Convert.ToInt32(m_configuration.GetSection("JWT")["Expires"]))).ToUnixTimeSeconds()}"), + new Claim(JwtRegisteredClaimNames.Nbf, $"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") + }; + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(DBManager.Instance.JWT.IssuerSigningKey)); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var securityToken = new JwtSecurityToken( + issuer: DBManager.Instance.JWT.Issuer, + audience: DBManager.Instance.JWT.Audience, + claims: claims, + expires: DateTime.Now.AddMinutes(Convert.ToInt32(DBManager.Instance.JWT.Expiration)), + signingCredentials: creds); + + var token = new JwtSecurityTokenHandler().WriteToken(securityToken); + + return token; + } + + private string GetRole(string userName) + { + string role = "user"; + var admins = SendCacheStore.GetSystemValue("administrators"); + if (admins != null) + { + var adminNames = admins.Split(","); + if (adminNames.Contains(userName)) + { + role = "system"; + } + } + + return role; + } + } +} diff --git a/Inotify/Controllers/SendControlor.cs b/Inotify/Controllers/SendControlor.cs new file mode 100644 index 0000000..46f2aae --- /dev/null +++ b/Inotify/Controllers/SendControlor.cs @@ -0,0 +1,31 @@ +using Inotify.Data; +using Inotify.Sends; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; +using System; + +namespace Inotify.Controllers +{ + [ApiController] + [Route("api")] + public class SendController : BaseController + { + [HttpGet, Route("send")] + public JsonResult Send(string token, string title, string? data) + { + if (DBManager.Instance.IsSendKey(token)) + { + var message = new SendMessage() + { + Token = token, + Title = title, + Data = data, + }; + if (SendTaskManager.Instance.SendMessage(message)) + return OK(); + } + return Fail(); + } + } +} diff --git a/Inotify/Controllers/SettingControlor.cs b/Inotify/Controllers/SettingControlor.cs new file mode 100644 index 0000000..b97ee9e --- /dev/null +++ b/Inotify/Controllers/SettingControlor.cs @@ -0,0 +1,159 @@ +using Inotify.Data; +using Inotify.Data.Models; +using Inotify.Sends; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Inotify.Controllers +{ + [ApiController] + [Route("api/setting")] + public class SettingControlor : BaseController + { + [HttpGet, Authorize(Policys.SystemOrUsers)] + public JsonResult Index() + { + return OK(); + } + + [HttpGet, Route("GetSendTemplates"), Authorize(Policys.SystemOrUsers)] + public JsonResult GetSendTemplates() + { + var sendTemplates = SendTaskManager.Instance.GetInputTemeplates().Values; + return OK(sendTemplates); + } + + [HttpGet, Route("GetSendAuths"), Authorize(Policys.SystemOrUsers)] + public JsonResult GetSendAuths() + { + var userInfo = DBManager.Instance.GetUser(UserName); + if (userInfo != null) + { + var sendAuthInfos = DBManager.Instance.DBase.Query().Where(e => e.UserId == userInfo.Id).ToArray(); + var userSendTemplates = new List(); + foreach (var sendAuthInfo in sendAuthInfos) + { + var sendTemplate = SendTaskManager.Instance.GetInputTemplate(sendAuthInfo.SendMethodTemplate); + if (sendTemplate != null) + { + sendTemplate.Name = sendAuthInfo.Name; + sendTemplate.AuthData = sendAuthInfo.AuthData; + sendTemplate.SendAuthId = sendAuthInfo.Id; + sendTemplate.IsActive = sendAuthInfo.Id == userInfo.SendAuthId; + sendTemplate.AuthToTemplate(sendAuthInfo.AuthData); + userSendTemplates.Add(sendTemplate); + } + } + + return OK(userSendTemplates); + } + return Fail(); + } + + [HttpPost, Route("ActiveSendAuth"), Authorize(Policys.SystemOrUsers)] + public JsonResult ActiveSendAuth(int sendAuthId, bool state) + { + var userInfo = DBManager.Instance.GetUser(UserName); + if (userInfo != null) + { + var authInfo = DBManager.Instance.DBase.Query().FirstOrDefault(e => e.Id == sendAuthId && e.UserId == userInfo.Id); + if (authInfo != null) + { + userInfo.SendAuthId = state ? sendAuthId : -1; + DBManager.Instance.DBase.Update(userInfo); + return OK(userInfo); + } + } + return Fail(); + } + + [HttpPost, Route("DeleteSendAuth"), Authorize(Policys.SystemOrUsers)] + public JsonResult DeleteSendAuth(int sendAuthId) + { + var userInfo = DBManager.Instance.GetUser(UserName); + if (userInfo != null) + { + var authInfo = DBManager.Instance.DBase.Query().FirstOrDefault(e => e.Id == sendAuthId && e.UserId == userInfo.Id); + if (authInfo != null) + { + DBManager.Instance.DBase.Delete(authInfo); + return OK(); + } + } + return Fail(); + } + + [HttpPost, Route("AddSendAuth"), Authorize(Policys.SystemOrUsers)] + public JsonResult AddSendAuth(InputTemeplate inputTemeplate) + { + var userInfo = DBManager.Instance.GetUser(UserName); + if (userInfo != null && inputTemeplate.Key != null && inputTemeplate.Name != null) + { + var authInfo = inputTemeplate.TemplateToAuth(); + var sendAuth = new SendAuthInfo() + { + UserId = userInfo.Id, + SendMethodTemplate = inputTemeplate.Key, + AuthData = authInfo, + Name = inputTemeplate.Name, + CreateTime = DateTime.Now, + ModifyTime = DateTime.Now, + }; + DBManager.Instance.DBase.Insert(sendAuth); + return OK(sendAuth); + } + return Fail(); + } + + [HttpPost, Route("ModifySendAuth"), Authorize(Policys.SystemOrUsers)] + public JsonResult ModifySendAuth(InputTemeplate inputTemeplate) + { + var userInfo = DBManager.Instance.GetUser(UserName); + if (userInfo != null) + { + var oldSendInfo = DBManager.Instance.DBase.Query().FirstOrDefault(e => e.Id == inputTemeplate.SendAuthId); + if (oldSendInfo != null && inputTemeplate.Name != null) + { + oldSendInfo.Name = inputTemeplate.Name; + oldSendInfo.AuthData = inputTemeplate.TemplateToAuth(); + oldSendInfo.ModifyTime = DateTime.Now; + DBManager.Instance.DBase.Update(oldSendInfo); + } + return OK(oldSendInfo); + } + return Fail(); + } + + [HttpGet, Route("GetSendKey"), Authorize(Policys.SystemOrUsers)] + public JsonResult GetSendKey() + { + var userInfo = DBManager.Instance.GetUser(UserName); + if (userInfo != null) + return OK(userInfo.Token); + return Fail(); + + } + + [HttpGet, Route("ReSendKey"), Authorize(Policys.SystemOrUsers)] + public JsonResult ReSendKey() + { + var userInfo = DBManager.Instance.GetUser(UserName); + if (userInfo != null) + { + userInfo.Token = Guid.NewGuid().ToString("N").ToUpper(); + DBManager.Instance.DBase.Update(userInfo); + return OK(userInfo.Token); + } + return Fail(); + } + } +} diff --git a/Inotify/Controllers/SetttingSysControlor.cs b/Inotify/Controllers/SetttingSysControlor.cs new file mode 100644 index 0000000..91f2afb --- /dev/null +++ b/Inotify/Controllers/SetttingSysControlor.cs @@ -0,0 +1,129 @@ +using Inotify.Data; +using Inotify.Data.Models; +using Inotify.Data.Models.System; +using Inotify.Sends; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using NPoco; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Inotify.Controllers +{ + [ApiController] + [Route("api/settingsys")] + public class SetttingSysControlor : BaseController + { + [HttpGet, Route("GetGlobal"), Authorize(Policys.Systems)] + public IActionResult GetGlobal() + { + var proxyenable = SendCacheStore.GetSystemValue("proxyenable"); + var githubEnable = SendCacheStore.GetSystemValue("githubEnable"); + return OK(new + { + sendthread = SendCacheStore.GetSystemValue("sendthread"), + proxy = SendCacheStore.GetSystemValue("proxy"), + proxyenable = proxyenable != "" && bool.Parse(proxyenable), + administrators = SendCacheStore.GetSystemValue("administrators"), + githubClientID = SendCacheStore.GetSystemValue("githubClientID"), + githubClientSecret = SendCacheStore.GetSystemValue("githubClientSecret"), + githubEnable = githubEnable != "" && bool.Parse(githubEnable), + }); + } + + [HttpPost, Route("SetGlobal"), Authorize(Policys.Systems)] + public IActionResult SetGlobal( + string? sendthread, + string? administrators, + string? proxy, + string? proxyenable, + string? githubClientID, + string? githubClientSecret, + string? githubEnable) + { + SendCacheStore.SetSystemValue("sendthread", sendthread); + SendCacheStore.SetSystemValue("administrators", administrators); + SendCacheStore.SetSystemValue("proxy", proxy); + SendCacheStore.SetSystemValue("proxyenable", proxyenable); + SendCacheStore.SetSystemValue("githubClientID", githubClientID); + SendCacheStore.SetSystemValue("githubClientSecret", githubClientSecret); + SendCacheStore.SetSystemValue("githubEnable", githubEnable); + + SendTaskManager.Instance.Stop(); + SendTaskManager.Instance.Run(); + + return OK(); + } + + [HttpGet, Route("GetJWT"), Authorize(Policys.Systems)] + public IActionResult GetJWT() + { + return OK(DBManager.Instance.JWT); + } + + [HttpPost, Route("SetJWT"), Authorize(Policys.Systems)] + public IActionResult SetJWT(JwtInfo jwt) + { + DBManager.Instance.JWT = jwt; + StartUpManager.Load().Restart(); + return OK(); + } + + [HttpPost, Route("DeleteUser"), Authorize(Policys.Systems)] + public IActionResult DeleteUser(string userName) + { + var userInfo = DBManager.Instance.GetUser(userName); + var userId = userInfo.Id; + if (userInfo != null) + { + DBManager.Instance.DBase.Delete(userInfo); + DBManager.Instance.DBase.DeleteMany().Where(e => e.UserId == userId); + return OK(); + } + return Fail(); + } + + [HttpPost, Route("ActiveUser"), Authorize(Policys.Systems)] + public IActionResult ActiveUser(string userName, bool active) + { + var userInfo = DBManager.Instance.GetUser(userName); + if (userInfo != null) + { + userInfo.Active = active; + DBManager.Instance.DBase.Update(userInfo, e => e.Active); + return OK(); + } + return Fail(); + } + + [HttpGet, Route("GetUsers"), Authorize(Policys.Systems)] + public IActionResult GetUsers(string? query, int page, int pageSize) + { + if (query == null) + return OK(DBManager.Instance.DBase.Query().ToPage(page, pageSize)); + else return OK(DBManager.Instance.DBase.Query().Where(e => e.UserName.Contains(query) || e.Email.Contains(query)).ToPage(page, pageSize)); + + } + + [HttpGet, Route("GetSendInfos"), Authorize(Policys.Systems)] + public IActionResult GetSendInfos(string? start, string? end) + { + var templates = SendTaskManager.Instance.GetInputTemeplates(); + var sendInfos = DBManager.Instance.DBase.Fetch(); + var sendInfoQuerys = sendInfos.Where(e => int.Parse(e.Date) >= int.Parse(start) && int.Parse(e.Date) <= int.Parse(end)).ToList(); + var sendInfoGroups = sendInfoQuerys.GroupBy(e => e.Date).Select(e => new { date = e.Key, count = e.Sum(item => item.Count) }).ToList(); + var sendTypeInfoGroups = sendInfoQuerys.GroupBy(e => e.TemplateID).Select(e => new { date = e.Key, count = e.Sum(item => item.Count) }).ToList(); + + var result = new + { + dataX = sendInfoGroups.Select(e => e.date).ToList(), + dataY = sendInfoGroups.Select(e => e.count).ToList(), + items = sendTypeInfoGroups.Select(e => new { name = templates[e.date].Name, value = e.count }) + }; + + return OK(result); + } + } +} diff --git a/Inotify/Data/DBManager.cs b/Inotify/Data/DBManager.cs new file mode 100644 index 0000000..46f92d5 --- /dev/null +++ b/Inotify/Data/DBManager.cs @@ -0,0 +1,173 @@ +using Inotify.Common; +using Inotify.Data.Models; +using Inotify.Data.Models.System; +using Inotify.Sends.Products; +using Microsoft.Data.Sqlite; +using Microsoft.IdentityModel; +using Newtonsoft.Json; +using NPoco; +using NPoco.Migrations; +using System; +using System.Data; +using System.IO; +using System.Net.Mail; +using System.Runtime.InteropServices; +using System.Security.Cryptography; + +namespace Inotify.Data +{ + public class DBManager + { + private readonly SqliteConnection m_dbConnection; + + private readonly Migrator m_migrator; + + private readonly string m_dataPath = "im_data"; + + private readonly string m_jwtPath; + + private readonly string m_dbPath; + + private JwtInfo m_JWT; + + public JwtInfo JWT + { + get { return m_JWT; } + set + { + m_JWT = value; + File.WriteAllText(m_jwtPath, JsonConvert.SerializeObject(JWT)); + } + } + + public Database DBase { get; private set; } + + private static DBManager? m_Instance; + + public static DBManager Instance + { + get + { + if (m_Instance == null) m_Instance = new DBManager(); + return m_Instance; + } + } + + private DBManager() + { + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + m_jwtPath = Path.Combine(Directory.GetCurrentDirectory(), "/" + m_dataPath + "/jwt.json"); + m_dbPath = Path.Combine(Directory.GetCurrentDirectory(), "/" + m_dataPath + "/data.db"); + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var dataPath = Path.Combine(Directory.GetCurrentDirectory(), m_dataPath); + if (!Directory.Exists(dataPath)) Directory.CreateDirectory(dataPath); + + m_jwtPath = Path.Combine(Directory.GetCurrentDirectory(), m_dataPath + "/jwt.json"); + m_dbPath = Path.Combine(Directory.GetCurrentDirectory(), m_dataPath + "/data.db"); + } + + if (!File.Exists(m_jwtPath)) + { + var bytes = new byte[128]; + var ranndom = new Random(); + ranndom.NextBytes(bytes); + var key = Convert.ToBase64String(bytes); + + JWT = new JwtInfo() + { + ClockSkew = 10, + Audience = "Inotify", + Issuer = "Inotify", + IssuerSigningKey = key, + Expiration = 36000, + }; + + File.WriteAllText(m_jwtPath, JsonConvert.SerializeObject(JWT)); + } + else + { + JWT = JsonConvert.DeserializeObject(File.ReadAllText(m_jwtPath)); + } + + + m_dbConnection = new SqliteConnection(string.Format("Data Source={0}", m_dbPath)); + if (m_dbConnection.State == ConnectionState.Closed) + m_dbConnection.Open(); + + DBase = new NPoco.Database(m_dbConnection, DatabaseType.SQLite); + m_migrator = new Migrator(DBase); + } + + public bool IsSendKey(string token) + { + return DBase.Query().Any(e => e.Token == token); + } + + public bool IsUser(string userName) + { + return DBase.Query().Any(e => e.UserName == userName); + } + + public SendUserInfo GetUser(string userName) + { + return DBase.Query().First(e => e.UserName == userName); + } + + public string GetAuth(string token, out string guid) + { + guid = string.Empty; + var upToekn = token.ToUpper(); + var userInfo = DBManager.Instance.DBase.Query().FirstOrDefault(e => e.Token == upToekn && e.Active); + if (userInfo == null) return null; + + var authInfo = DBManager.Instance.DBase.Query().FirstOrDefault(e => e.Id == userInfo.SendAuthId && e.UserId == userInfo.Id); + if (authInfo == null) + return null; + + guid = authInfo.SendMethodTemplate; + return authInfo.AuthData; + } + + + public void Run() + { + if (!m_migrator.TableExists()) + m_migrator.CreateTable(true).Execute(); + + if (!m_migrator.TableExists()) + { + m_migrator.CreateTable(true).Execute(); + + DBase.Insert(new SystemInfo() + { + key = "administrators", + Value = "admin" + }); + } + + if (!m_migrator.TableExists()) + { + m_migrator.CreateTable(true).Execute(); + SendUserInfo userInfo = new SendUserInfo() + { + Token = "112D77BAD9704FFEAECD716B5678DFBE".ToUpper(), + UserName = "admin", + Email = "admin@qq.com", + CreateTime = DateTime.Now, + Active = true, + Password = "123456".ToMd5() + }; + DBase.Insert(userInfo); + } + + if (!m_migrator.TableExists()) + { + m_migrator.CreateTable(true).Execute(); + } + } + } +} diff --git a/Inotify/Data/Models/SendAuthInfo.cs b/Inotify/Data/Models/SendAuthInfo.cs new file mode 100644 index 0000000..74dd130 --- /dev/null +++ b/Inotify/Data/Models/SendAuthInfo.cs @@ -0,0 +1,30 @@ +using System; + +namespace Inotify.Data.Models +{ + [NPoco.TableName("sendAuthInfo")] + [NPoco.PrimaryKey("id")] + public class SendAuthInfo + { + [NPoco.Column("id")] + public int Id { get; set; } + + [NPoco.Column("userId")] + public int UserId { get; set; } + + [NPoco.Column("name")] + public string Name { get; set; } + + [NPoco.Column("sendMethodTemplate")] + public string SendMethodTemplate { get; set; } + + [NPoco.Column("authData")] + public string AuthData { get; set; } + + [NPoco.Column("modifyTime")] + public DateTime ModifyTime { get; set; } + + [NPoco.Column("createTime")] + public DateTime CreateTime { get; set; } + } +} diff --git a/Inotify/Data/Models/SendInfo.cs b/Inotify/Data/Models/SendInfo.cs new file mode 100644 index 0000000..b05bce4 --- /dev/null +++ b/Inotify/Data/Models/SendInfo.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Inotify.Data.Models +{ + [NPoco.TableName("sendInfo")] + [NPoco.PrimaryKey(new string[] { "templateID", "date" }, AutoIncrement = false)] + public class SendInfo + { + [NPoco.Column("templateID")] + public string TemplateID { get; set; } + + [NPoco.Column("date")] + public string Date { get; set; } + + [NPoco.Column("count")] + public int Count { get; set; } + } +} diff --git a/Inotify/Data/Models/SendUserInfo.cs b/Inotify/Data/Models/SendUserInfo.cs new file mode 100644 index 0000000..082fc29 --- /dev/null +++ b/Inotify/Data/Models/SendUserInfo.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Inotify.Data.Models +{ + [NPoco.TableName("userInfo")] + [NPoco.PrimaryKey("id")] + public class SendUserInfo : SystemUserInfo + { + [NPoco.Column("token")] + public string? Token { get; set; } + + [NPoco.Column("sendAuthId")] + public int SendAuthId { get; set; } + } +} diff --git a/Inotify/Data/Models/System/JwtInfo.cs b/Inotify/Data/Models/System/JwtInfo.cs new file mode 100644 index 0000000..77191c5 --- /dev/null +++ b/Inotify/Data/Models/System/JwtInfo.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Inotify.Data.Models.System +{ + public class JwtInfo + { + public int ClockSkew { get; set; } + + public string Audience { get; set; } + + public string Issuer { get; set; } + + public string IssuerSigningKey { get; set; } + + public int Expiration { get; set; } + + } +} diff --git a/Inotify/Data/Models/System/SystemInfo.cs b/Inotify/Data/Models/System/SystemInfo.cs new file mode 100644 index 0000000..f285334 --- /dev/null +++ b/Inotify/Data/Models/System/SystemInfo.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Inotify.Data.Models +{ + + [NPoco.TableName("systemInfo")] + [NPoco.PrimaryKey("key", AutoIncrement = false)] + public class SystemInfo + { + [NPoco.Column("key")] + public string key; + + [NPoco.Column("value")] + public string Value; + + public SystemInfo() + { + + } + + public SystemInfo(string key, string value) + { + this.key = key; + Value = value; + } + } +} diff --git a/Inotify/Data/Models/System/SystemUserInfo.cs b/Inotify/Data/Models/System/SystemUserInfo.cs new file mode 100644 index 0000000..740a4e5 --- /dev/null +++ b/Inotify/Data/Models/System/SystemUserInfo.cs @@ -0,0 +1,37 @@ +using System; + +namespace Inotify.Data.Models +{ + [NPoco.TableName("systemUser")] + [NPoco.PrimaryKey("id")] + public class SystemUserInfo + { + public SystemUserInfo() + { + Avatar = ""; + } + + [NPoco.Column("id")] + public int Id { get; set; } + + [NPoco.Column("userName")] + public string UserName { get; set; } + + [NPoco.Column("password")] + public string? Password { get; set; } + + [NPoco.Column("avatar")] + public string? Avatar { get; set; } + + [NPoco.Column("email")] + public string? Email { get; set; } + + + [NPoco.Column("active")] + public bool Active { get; set; } + + [NPoco.Column("createTime")] + public DateTime? CreateTime { get; set; } + + } +} diff --git a/Inotify/Dockerfile b/Inotify/Dockerfile new file mode 100644 index 0000000..6687b41 --- /dev/null +++ b/Inotify/Dockerfile @@ -0,0 +1,26 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base +VOLUME ["/im_data"] +WORKDIR /app +EXPOSE 80 +EXPOSE 443 +ENV GitHubClientId="" +ENV GitClientSecret="" + +FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build +WORKDIR /src +COPY ["/Inotify/Inotify.csproj", "Inotify/"] +RUN dotnet restore "Inotify/Inotify.csproj" +COPY . . +WORKDIR "/src/Inotify" +RUN dotnet build "Inotify.csproj" -c Release -o /app/build +FROM build AS publish +RUN dotnet publish "Inotify.csproj" -c Release -o /app/publish + + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Inotify.dll"] + diff --git a/Inotify/Inotify.csproj b/Inotify/Inotify.csproj new file mode 100644 index 0000000..b5c6c84 --- /dev/null +++ b/Inotify/Inotify.csproj @@ -0,0 +1,33 @@ + + + + netcoreapp3.1 + 47575d38-f493-4d8d-9adf-16aa974a0905 + Linux + Inotify.Program + enable + + + + 3 + + + + + + + + + + + + + + + + + + + + + diff --git a/Inotify/Inotify.sln b/Inotify/Inotify.sln new file mode 100644 index 0000000..e9c17fe --- /dev/null +++ b/Inotify/Inotify.sln @@ -0,0 +1,30 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30804.86 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{80E8FF2E-B685-44E6-82A8-FDCA48EDAD47}" + ProjectSection(SolutionItems) = preProject + ..\.editorconfig = ..\.editorconfig + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Inotify", "Inotify.csproj", "{9AF94AE0-E8C6-414C-A0AC-EDFB301CA05E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9AF94AE0-E8C6-414C-A0AC-EDFB301CA05E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9AF94AE0-E8C6-414C-A0AC-EDFB301CA05E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9AF94AE0-E8C6-414C-A0AC-EDFB301CA05E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9AF94AE0-E8C6-414C-A0AC-EDFB301CA05E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D0BF7DFA-F6E1-456A-BDE8-7294188EFC51} + EndGlobalSection +EndGlobal diff --git a/Inotify/Program.cs b/Inotify/Program.cs new file mode 100644 index 0000000..2235cae --- /dev/null +++ b/Inotify/Program.cs @@ -0,0 +1,42 @@ +using Inotify.Data; +using Inotify.Sends; + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Inotify +{ + public class Program + { + public static void Main(string[] args) + { + DBManager.Instance.Run(); + SendTaskManager.Instance.Run(); + + JsonConvert.DefaultSettings = () => + { + return new JsonSerializerSettings { StringEscapeHandling = StringEscapeHandling.EscapeNonAscii }; + }; + try + { + var appManager = StartUpManager.Load(); + do + { + appManager.Start(args); + } while (appManager.Restarting); + } + catch (Exception) + { + + } + } + } +} diff --git a/Inotify/Properties/PublishProfiles/FolderProfile.pubxml b/Inotify/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..c0c4984 --- /dev/null +++ b/Inotify/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,16 @@ + + + + + False + False + True + Release + Any CPU + FileSystem + bin\Release\netcoreapp3.1\publish\ + FileSystem + + \ No newline at end of file diff --git a/Inotify/Properties/PublishProfiles/registry.hub.docker.com_xpnas.pubxml b/Inotify/Properties/PublishProfiles/registry.hub.docker.com_xpnas.pubxml new file mode 100644 index 0000000..f43bfad --- /dev/null +++ b/Inotify/Properties/PublishProfiles/registry.hub.docker.com_xpnas.pubxml @@ -0,0 +1,18 @@ + + + + + Custom + true + https://registry.hub.docker.com/xpnas + xpnas + latest + ContainerRegistry + Release + Any CPU + adf05021-a07e-4231-9959-1f5cee20d7b3 + + \ No newline at end of file diff --git a/Inotify/Sends/Products/EmailSendTemplate.cs b/Inotify/Sends/Products/EmailSendTemplate.cs new file mode 100644 index 0000000..e3d47bb --- /dev/null +++ b/Inotify/Sends/Products/EmailSendTemplate.cs @@ -0,0 +1,67 @@ +using FluentEmail.Core; +using FluentEmail.Liquid; +using FluentEmail.Smtp; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Mail; +using System.Text; + +namespace Inotify.Sends.Products +{ + + public class EmailAuth + { + [InputTypeAttribte(0, "FromName", "发件人", "管理员")] + public string FromName { get; set; } + + [InputTypeAttribte(0, "From", "发件地址", "abc@qq.com")] + public string From { get; set; } + + [InputTypeAttribte(1, "Password", "发件密码", "123456")] + public string Password { get; set; } + + [InputTypeAttribte(2, "Host", "SMTP地址", "smtp.qq.com")] + public string Host { get; set; } + + [InputTypeAttribte(2, "Port", "SMTP端口", "587")] + public int Port { get; set; } + + [InputTypeAttribte(3, "EnableSSL", "SSL", "true|false")] + public bool EnableSSL { get; set; } + + [InputTypeAttribte(4, "To", "收件箱", "abcd@qq.com")] + public string To { get; set; } + } + + + [SendMethodKey("EA2B43F7-956C-4C01-B583-0C943ABB36C3", "邮件推送", Order = 1)] + public class EmailSendTemplate : SendTemplate + { + public override EmailAuth Auth { get; set; } + + public override bool SendMessage(SendMessage message) + { + var smtpSender = new SmtpSender(new SmtpClient() + { + EnableSsl = Auth.EnableSSL, + UseDefaultCredentials = false, + DeliveryMethod = SmtpDeliveryMethod.Network, + Port = Auth.Port, + Host = Auth.Host, + Credentials = new NetworkCredential(Auth.From, Auth.Password), + }); + + var email = + Email.From(Auth.From, Auth.FromName) + .Subject(message.Title) + .Body(message.Data ?? "") + .To(Auth.To); + + smtpSender.SendAsync(email); + + return true; + + } + } +} diff --git a/Inotify/Sends/Products/TelegramBotSendTemplate.cs b/Inotify/Sends/Products/TelegramBotSendTemplate.cs new file mode 100644 index 0000000..9fc8c01 --- /dev/null +++ b/Inotify/Sends/Products/TelegramBotSendTemplate.cs @@ -0,0 +1,49 @@ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Telegram.Bot; +using Telegram.Bot.Types.InputFiles; + +namespace Inotify.Sends.Products +{ + + public class TelegramBotAuth + { + [InputTypeAttribte(1, "BotToken", "BotToken", "ID:Token")] + public string BotToken { get; set; } + + [InputTypeAttribte(2, "Chat_id", "ChatId", "ChatId")] + public string Chat_id { get; set; } + } + + + [SendMethodKey("E9669473-FF0B-4474-92BB-E939D92045BB", "电报机器人", Order = 2)] + + public class TelegramBotSendTemplate : SendTemplate + { + + public override TelegramBotAuth Auth { get; set; } + + public override bool SendMessage(SendMessage message) + { + + var proxy = GetProxy(); + var client = proxy == null ? new TelegramBotClient(Auth.BotToken) : new TelegramBotClient(Auth.BotToken, proxy); + var content = string.IsNullOrEmpty(message.Title) ? message.Title : message.Title + "\n" + message.Data; + var isIMG = !String.IsNullOrEmpty(message.Title) && IsUrl(message.Title) && IsImage(message.Title) && String.IsNullOrEmpty(message.Data); + if (isIMG) + { + client.SendPhotoAsync(Auth.Chat_id, new InputOnlineFile(new Uri(message.Title))); + } + else + { + client.SendTextMessageAsync(Auth.Chat_id, content); + } + return true; + } + } +} diff --git a/Inotify/Sends/Products/WeixiSendTemplate.cs b/Inotify/Sends/Products/WeixiSendTemplate.cs new file mode 100644 index 0000000..58ec469 --- /dev/null +++ b/Inotify/Sends/Products/WeixiSendTemplate.cs @@ -0,0 +1,187 @@ +using Inotify.Common; +using Inotify.Sends; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; + +namespace Inotify.Sends.Products +{ + + public class WeixiAuth + { + [InputTypeAttribte(0, "Corpid", "企业ID", "Corpid")] + public string Corpid { get; set; } + + [InputTypeAttribte(1, "Corpsecret", "密钥", "Corpsecret")] + public string Corpsecret { get; set; } + + [InputTypeAttribte(2, "AgentID", "应用ID", "AgentID")] + public string AgentID { get; set; } + + [InputTypeAttribte(2, "OpengId", "OpengId", "@all")] + public string OpengId { get; set; } + } + + + [SendMethodKey("409A30D5-ABE8-4A28-BADD-D04B9908D763", "企业微信", Order = 0)] + public class WeixiSendTemplate : SendTemplate + { + public override WeixiAuth Auth { get; set; } + + public override bool SendMessage(SendMessage message) + { + if (Auth == null) return false; + + var token = GetAccessToken(); + if (token == null) return false; + + return PostMail(token, message.Title, message.Data); + } + + /// 获取AccessToken + /// + /// + private string GetAccessToken() + { + var key = Auth.Corpid + Auth.AgentID + Auth.Corpsecret; + var toekn = SendCacheStore.Get(key); + if (toekn == null) + { + var url = string.Format(@"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={0}&corpsecret={1}", Auth.Corpid, Auth.Corpsecret); + + WebRequest request = WebRequest.Create(url); + request.Credentials = CredentialCache.DefaultCredentials; + using WebResponse response = request.GetResponse(); + using Stream streamResponse = response.GetResponseStream(); + StreamReader reader = new StreamReader(streamResponse); + string responseFromServer = reader.ReadToEnd(); + if (!string.IsNullOrEmpty(responseFromServer)) + { + if (JsonConvert.DeserializeObject(responseFromServer) is JObject res) + { + if (res.TryGetValue("access_token", out JToken? jtoken)) + { + toekn = jtoken.ToString(); + } + } + } + reader.Close(); + } + + if (toekn != null) + SendCacheStore.Set(key, toekn, DateTimeOffset.Now.AddHours(2)); + + return toekn; + } + + private bool PostMail(string accessToken, string title, string? data) + { + var uri = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=" + accessToken; + + var content = string.IsNullOrEmpty(data) ? title : title + "\n" + data; + var isImage = !String.IsNullOrEmpty(title) && IsUrl(title) && IsImage(title) && String.IsNullOrEmpty(data); + var imageData = isImage ? GetImage(title) : null; + string mediaId = null; + if (imageData != null) + mediaId = UpLoadIMage(accessToken, imageData); + + //创建请求 + WebRequest myWebRequest = WebRequest.Create(uri); + myWebRequest.Credentials = CredentialCache.DefaultCredentials; + myWebRequest.ContentType = "application/json;charset=UTF-8"; + myWebRequest.Method = "POST"; + + //向服务器发送的内容 + using (Stream streamResponse = myWebRequest.GetRequestStream()) + { + object jsonObject; + if (isImage && imageData != null && mediaId != null) + { + jsonObject = new + { + touser = Auth.OpengId, + agentid = Auth.AgentID, + msgtype = "image", + image = new + { + media_id = mediaId + }, + safe = 0 + }; + } + else + { + jsonObject = new + { + touser = Auth.OpengId, + msgtype = "text", + agentid = Auth.AgentID, + text = new + { + content, + }, + safe = 0 + }; + } + + string paramString = JsonConvert.SerializeObject(jsonObject, Formatting.None); + byte[] byteArray = Encoding.UTF8.GetBytes(paramString); + //向请求中写入内容 + streamResponse.Write(byteArray, 0, byteArray.Length); + } + //创建应答 + WebResponse myWebResponse = myWebRequest.GetResponse(); + //创建应答的读写流 + string responseFromServer; + using (Stream streamResponse = myWebResponse.GetResponseStream()) + { + StreamReader streamRead = new StreamReader(streamResponse); + responseFromServer = streamRead.ReadToEnd(); + } + //关闭应答 + myWebResponse.Close(); + + return true; + } + + private string UpLoadIMage(string accessToken, byte[] bytes) + { + try + { + var uri = string.Format("https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token={0}&type=file", accessToken); + string boundary = DateTime.Now.Ticks.ToString("X"); + WebRequest request = WebRequest.Create(uri); + request.Method = "POST"; + request.Credentials = CredentialCache.DefaultCredentials; + request.ContentType = "multipart/form-data;charset=utf-8;boundary=" + boundary; + var itemBoundaryBytes = Encoding.UTF8.GetBytes("\r\n--" + boundary + "\r\n"); + var endBoundaryBytes = Encoding.UTF8.GetBytes("\r\n--" + boundary + "--\r\n"); + + var sbHeader = new StringBuilder(string.Format("Content-Disposition:form-data;name=\"file\";filename=\"{0}\"\r\nContent-Type:application/octet-stream\r\n\r\n", boundary)); + var postHeaderBytes = Encoding.UTF8.GetBytes(sbHeader.ToString()); + + Stream postStream = request.GetRequestStream(); + postStream.Write(itemBoundaryBytes, 0, itemBoundaryBytes.Length); + postStream.Write(postHeaderBytes, 0, postHeaderBytes.Length); + postStream.Write(bytes, 0, bytes.Length); + postStream.Write(endBoundaryBytes, 0, endBoundaryBytes.Length); + postStream.Close(); + + HttpWebResponse response = request.GetResponse() as HttpWebResponse; + Stream instream = response.GetResponseStream(); + StreamReader sr = new StreamReader(instream, Encoding.UTF8); + string content = sr.ReadToEnd(); + var result = JObject.Parse(content); + return result.Value("media_id").ToString(); + } + catch + { + return null; + } + } + } +} diff --git a/Inotify/Sends/SendCacheStore.cs b/Inotify/Sends/SendCacheStore.cs new file mode 100644 index 0000000..d356091 --- /dev/null +++ b/Inotify/Sends/SendCacheStore.cs @@ -0,0 +1,58 @@ +using Inotify; +using Inotify.Data; +using Inotify.Data.Models; +using Inotify.Sends; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Web; + +namespace Inotify.Sends +{ + public class SendCacheStore + { + private static readonly System.Runtime.Caching.MemoryCache m_cache; + + private static readonly Dictionary m_systemInfos; + + static SendCacheStore() + { + m_cache = new System.Runtime.Caching.MemoryCache("CacheStore"); + m_systemInfos = DBManager.Instance.DBase.Query().ToList().ToDictionary(e => e.key, e => e.Value); + } + + public static string Get(string key) + { + object obj = m_cache.Get(key); + if (obj != null && obj is string) + return obj as string; + return null; + + } + + public static void Set(string key, string value, DateTimeOffset offset) + { + m_cache.Set(key, value, offset); + } + + public static string GetSystemValue(string key) + { + if (m_systemInfos.ContainsKey(key)) + return m_systemInfos[key]; + return ""; + + } + + public static void SetSystemValue(string key, string value) + { + m_systemInfos[key] = value ?? ""; + var systemInfo = new SystemInfo(key, m_systemInfos[key]); + if (DBManager.Instance.DBase.Query().Where(e => e.key == key).Any()) + { + DBManager.Instance.DBase.Delete(systemInfo); + } + DBManager.Instance.DBase.Insert(systemInfo); + } + } +} diff --git a/Inotify/Sends/SendMessage.cs b/Inotify/Sends/SendMessage.cs new file mode 100644 index 0000000..7667e67 --- /dev/null +++ b/Inotify/Sends/SendMessage.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Inotify.Sends +{ + public class SendMessage + { + public string Token; + public string Title; + public string? Data; + } +} diff --git a/Inotify/Sends/SendTaskManager.cs b/Inotify/Sends/SendTaskManager.cs new file mode 100644 index 0000000..4623cb4 --- /dev/null +++ b/Inotify/Sends/SendTaskManager.cs @@ -0,0 +1,232 @@ +using Inotify; +using Inotify.Data; +using Inotify.Data.Models; +using Inotify.Sends; +using Newtonsoft.Json; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; + + +namespace Inotify.Sends +{ + + public class SendTaskManager + { + private static SendTaskManager m_Instance; + + public static SendTaskManager Instance + { + get + { + if (m_Instance == null) m_Instance = new SendTaskManager(); + return m_Instance; + } + } + + private readonly Thread m_analyseThread; + + private readonly List m_sendThreads; + + private readonly BlockingCollection m_sendMessages; + + private readonly BlockingCollection m_analyseMessages; + + private readonly Dictionary m_sendMethodTemplateTypes; + + + private SendTaskManager() + { + m_sendMessages = new BlockingCollection(); + m_analyseMessages = new BlockingCollection(); + m_sendMethodTemplateTypes = new Dictionary(); + m_sendThreads = new List(); + + var sendMethodTemplates = Assembly.GetExecutingAssembly() + .GetTypes() + .Where(e => e.GetCustomAttribute() != null) + .OrderBy(e => e.GetCustomAttribute().Order) + .ToList(); + + sendMethodTemplates.ForEach(sendMethodTemplate => + { + var attribute = sendMethodTemplate.GetCustomAttribute(); + if (attribute != null) + { + var key = attribute.Key; + m_sendMethodTemplateTypes.Add(key, sendMethodTemplate); + } + }); + + m_analyseThread = null; + m_analyseThread = new Thread(e => { SendLog(m_analyseThread); }) + { + IsBackground = true, + Priority = ThreadPriority.Highest + }; + m_analyseThread.Start(); + } + + + public EventHandler OnMessageAdd; + + public EventHandler OnSendSucessed; + + public EventHandler OnSendFailed; + + public void Run() + { + var sendthread = SendCacheStore.GetSystemValue("sendthread"); + int.TryParse(sendthread, out int threadCount); + threadCount = threadCount <= 0 ? 1 : threadCount; + for (int i = 0; i < threadCount; i++) + { + Thread thread = null; + thread = new Thread(e => { SendWork(thread); }) + { + IsBackground = true, + Priority = ThreadPriority.Highest + }; + thread.Start(); + m_sendThreads.Add(thread); + } + } + + public void Stop() + { + foreach (var thread in m_sendThreads) + { + // m_messages.Add(new StopMessage()); + thread.Interrupt(); + } + m_sendThreads.Clear(); + } + + public bool SendMessage(SendMessage message) + { + if (m_sendMessages.Count > 10000) + return false; + + m_sendMessages.Add(message); + + OnMessageAdd?.Invoke(this, message); + + return true; + } + + public Dictionary GetInputTemeplates() + { + var sendTemplates = new Dictionary(); + foreach (var sendMethodTemplateType in m_sendMethodTemplateTypes.Values) + { + var obj = Activator.CreateInstance(sendMethodTemplateType); + var getTemeplateMethod = sendMethodTemplateType.GetMethod("GetTemeplate"); + if (getTemeplateMethod != null) + { + if (getTemeplateMethod.Invoke(obj, null) is InputTemeplate temeplate && temeplate.Key != null) + sendTemplates.Add(temeplate.Key, temeplate); + } + } + return sendTemplates; + } + + public InputTemeplate? GetInputTemplate(string key) + { + var sendMethodTemplateType = m_sendMethodTemplateTypes[key]; + var obj = Activator.CreateInstance(sendMethodTemplateType); + var getTemeplateMethod = sendMethodTemplateType.GetMethod("GetTemeplate"); + if (getTemeplateMethod != null) + { + var temeplate = getTemeplateMethod.Invoke(obj, null) as InputTemeplate; + return temeplate; + } + return null; + } + + private void SendWork(Thread thread) + { + while (true && thread.ThreadState != ThreadState.WaitSleepJoin) + { + try + { + var message = m_sendMessages.Take(); + var authData = DBManager.Instance.GetAuth(message.Token, out string temeplateId); + if (temeplateId != null && authData != null) + { + if (m_sendMethodTemplateTypes.ContainsKey(temeplateId)) + { + var sendMethodTemplateActor = Activator.CreateInstance(m_sendMethodTemplateTypes[temeplateId]); + if (sendMethodTemplateActor != null) + { + var sendMethodType = sendMethodTemplateActor.GetType(); + var compositonMethod = sendMethodType.GetMethod("Composition"); + if (compositonMethod != null) + { + compositonMethod.Invoke(sendMethodTemplateActor, new object[] { authData }); + } + + var sendMessageMethod = sendMethodType.GetMethod("SendMessage"); + if (sendMessageMethod != null) + { + var result = sendMessageMethod.Invoke(sendMethodTemplateActor, new object[] { message }); + if (result != null) + { + m_analyseMessages.Add(message); + if ((bool)result) + { + OnSendSucessed?.Invoke(this, message); + continue; + } + } + + } + } + } + } + + OnSendFailed?.Invoke(this, message); + + } + catch (ThreadInterruptedException) + { + break; + } + finally + { + + } + } + } + + private void SendLog(Thread thread) + { + while (true && thread.ThreadState != ThreadState.WaitSleepJoin) + { + var message = m_analyseMessages.Take(); + var date = DateTime.Now.ToString("yyyyMMdd"); + var authData = DBManager.Instance.GetAuth(message.Token, out string temeplateId); + + if (temeplateId != null) + { + var sendInfo = DBManager.Instance.DBase.Query().FirstOrDefault(e => e.Date == date && e.TemplateID == temeplateId); + if (sendInfo == null) + { + sendInfo = new SendInfo() { Date = date, TemplateID = temeplateId, Count = 1 }; + DBManager.Instance.DBase.Insert(sendInfo); + } + else + { + sendInfo.Count++; + DBManager.Instance.DBase.Update(sendInfo, e => e.Count); + } + } + } + } + + } +} diff --git a/Inotify/Sends/SendTemplate.cs b/Inotify/Sends/SendTemplate.cs new file mode 100644 index 0000000..dd4e983 --- /dev/null +++ b/Inotify/Sends/SendTemplate.cs @@ -0,0 +1,228 @@ +using Inotify.Sends; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Mail; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; + +namespace Inotify.Sends +{ + public enum InputType + { + TEXT = 1, + CHECK = 2, + ENUM = 3, + } + + [AttributeUsage(AttributeTargets.Class)] + public class SendMethodKeyAttribute : Attribute + { + public string Key; + + public bool Open; + + public string Name; + + public int Order; + + public SendMethodKeyAttribute(string key, string name, bool open = true) + { + Key = key; + Name = name; + Open = open; + } + } + + public class InputTypeValue + { + public string? Name { get; set; } + public string? Description { get; set; } + public string? Default { get; set; } + public InputType? Type { get; set; } + public int? Order { get; set; } + public string? Value { get; set; } + } + + public class InputTemeplate + { + public string? Key { get; set; } + public string? Type { get; set; } + public string? Name { get; set; } + public bool IsActive { get; set; } + public string? AuthData { get; set; } + public int? SendAuthId { get; set; } + public List? Values { get; set; } + + public string TemplateToAuth() + { + var jObject = new JObject(); + + if (Values != null) + { + foreach (var item in Values) + { + if (item.Name != null && item.Value != null) + { + if (item.Type == InputType.CHECK) + jObject.Add(item.Name, item.Value.ToLower() == "true"); + else jObject.Add(item.Name, item.Value); + } + } + } + + return jObject.ToString(); + } + + public void AuthToTemplate(string authInfo) + { + if (!string.IsNullOrEmpty(authInfo)) + { + var jObject = JObject.Parse(authInfo); + + if (Values != null) + { + foreach (var value in Values) + { + if (value.Name != null) + { + if (jObject.TryGetValue(value.Name, out JToken? jtoken)) + { + value.Value = jtoken == null ? "" : jtoken.Value(); + } + } + } + } + } + AuthData = authInfo; + } + } + + [AttributeUsage(AttributeTargets.Property)] + [JsonObject(MemberSerialization.OptIn)] + public class InputTypeAttribte : Attribute + { + public InputTypeValue InputTypeData { get; set; } + + private InputTypeAttribte(int order, string name, string description, string defaultValue, InputType type) + { + InputTypeData = new InputTypeValue + { + Name = name, + Description = description, + Default = defaultValue, + Order = order, + Type = type + }; + } + + public InputTypeAttribte(int order, string name, string description, string defaultValue) + : this(order, name, description, defaultValue, InputType.TEXT) + { + + } + + public InputTypeAttribte(int order, string name, string description, bool defaultValue) + : this(order, name, description, "", InputType.CHECK) + { + InputTypeData.Default = defaultValue ? "是" : "否"; + } + } + + + public abstract class SendTemplate + { + + public abstract T Auth { get; set; } + + public void Composition(string authInfo) + { + Auth = JsonConvert.DeserializeObject(authInfo); + } + + public InputTemeplate GetTemeplate() + { + var values = typeof(T) + .GetProperties() + .SelectMany(e => e.GetCustomAttributes(typeof(InputTypeAttribte), false)) + .Cast() + .Select(e => e.InputTypeData) + .ToList(); + + var sendMethodKeyAttribute = this.GetType().GetCustomAttribute(); + + if (sendMethodKeyAttribute != null) + { + return new InputTemeplate() + { + Name = sendMethodKeyAttribute.Name, + Type = sendMethodKeyAttribute.Name, + Key = sendMethodKeyAttribute.Key, + Values = values + }; + } + + return null; + } + + public virtual bool SendMessage(SendMessage message) + { + return false; + } + + protected WebProxy GetProxy() + { + if (SendCacheStore.GetSystemValue("proxyenable") == "true") + { + var proxyurl = SendCacheStore.GetSystemValue("proxy"); + if (proxyurl != null) + { + + WebProxy proxy = new WebProxy + { + Address = new Uri(proxyurl) + }; + return proxy; + } + } + return null; + } + + protected bool IsUrl(string url) + { + var regex = new Regex(@"(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]"); + if (!regex.IsMatch(url)) + { + return false; + } + return true; + } + + protected bool IsImage(string url) + { + var regex = new Regex(@".*?(jpeg|jpe|png|jpg)"); + if (!regex.IsMatch(url)) + { + return false; + } + return true; + } + + protected byte[] GetImage(string url) + { + if (IsUrl(url) && IsImage(url)) + { + WebClient mywebclient = new WebClient(); + return mywebclient.DownloadData(url); + } + return null; + } + } + + +} diff --git a/Inotify/Startup.cs b/Inotify/Startup.cs new file mode 100644 index 0000000..4fc96b6 --- /dev/null +++ b/Inotify/Startup.cs @@ -0,0 +1,118 @@ +using Inotify.Controllers; +using Inotify.Data; +using Inotify.Sends; +using Inotify.ThridOauth; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Rewrite; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json; +using NPoco; +using System; +using System.Net; +using System.Net.Http; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Unicode; +using System.Threading.Tasks; + +namespace Inotify +{ + public class Startup + { + + public Startup() + { + + } + + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + services.AddMemoryCache(); + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ClockSkew = TimeSpan.FromSeconds(DBManager.Instance.JWT.ClockSkew), + ValidAudience = DBManager.Instance.JWT.Audience, + ValidIssuer = DBManager.Instance.JWT.Issuer, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(DBManager.Instance.JWT.IssuerSigningKey)) + }; + + options.Events = new JwtBearerEvents + { + OnAuthenticationFailed = context => + { + var payload = JsonConvert.SerializeObject(new { message = "֤ʧ", code = 403 }); + context.Response.ContentType = "application/json"; + context.Response.StatusCode = StatusCodes.Status200OK; + context.Response.WriteAsync(payload); + return Task.FromResult(1); + + }, + OnForbidden = context => + { + var payload = JsonConvert.SerializeObject(new { message = "δȨ", code = 405 }); + context.Response.ContentType = "application/json"; + context.Response.StatusCode = StatusCodes.Status200OK; + context.Response.WriteAsync(payload); + return Task.FromResult(1); + } + }; + }); + + services.AddAuthorization(options => + { + options.AddPolicy(Policys.Users, policy => policy.RequireRole(Roles.User).Build()); + options.AddPolicy(Policys.Systems, policy => policy.RequireRole(Roles.System).Build()); + options.AddPolicy(Policys.SystemOrUsers, policy => policy.RequireRole(Roles.User, Roles.System).Build()); + options.AddPolicy(Policys.All, policy => policy.RequireRole(Roles.User, Roles.System).Build()); + }); + + services.AddSingleton(); + services.AddGitHubLogin(p => + { + p.ClientId = SendCacheStore.GetSystemValue("githubClientID"); + p.ClientSecret = SendCacheStore.GetSystemValue("githubClientSecret"); + }); + services.AddControllersWithViews().AddJsonOptions(options => + { + options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All); + }); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + var options = new RewriteOptions(); + options.AddRewrite(@"api/(.*).send/(.*)/(.*)", "api/send?token=$1&title=$2&data=$3", true); + options.AddRewrite(@"api/(.*).send/(.*)", "api/send?token=$1&title=$2", true); + + app.UseRewriter(options); + app.UseRouting(); + app.UseAuthentication(); + app.UseAuthorization(); + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} \ No newline at end of file diff --git a/Inotify/StartupManager.cs b/Inotify/StartupManager.cs new file mode 100644 index 0000000..45ff7c6 --- /dev/null +++ b/Inotify/StartupManager.cs @@ -0,0 +1,73 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Inotify +{ + public class StartUpManager + { + private static StartUpManager _appManager; + private CancellationTokenSource _tokenSource; + private bool _running; + private bool _restart; + + public bool Restarting => _restart; + + public StartUpManager() + { + _running = false; + _restart = false; + + } + + public static StartUpManager Load() + { + if (_appManager == null) + _appManager = new StartUpManager(); + + return _appManager; + } + + public void Start(string[] args) + { + if (_running) + return; + + if (_tokenSource != null && _tokenSource.IsCancellationRequested) + return; + + _tokenSource = new CancellationTokenSource(); + _tokenSource.Token.ThrowIfCancellationRequested(); + _running = true; + + var hostBuilder = Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }).Build(); + hostBuilder.RunAsync(_tokenSource.Token); + hostBuilder.WaitForShutdown(); + } + + public void Stop() + { + if (!_running) + return; + + _tokenSource.Cancel(); + _running = false; + } + + public void Restart() + { + Stop(); + + _restart = true; + _tokenSource = null; + } + } +} diff --git a/Inotify/ThridOauth/Common/AuthorizeResult.cs b/Inotify/ThridOauth/Common/AuthorizeResult.cs new file mode 100644 index 0000000..f58d3d3 --- /dev/null +++ b/Inotify/ThridOauth/Common/AuthorizeResult.cs @@ -0,0 +1,26 @@ +using Inotify.ThridOauth.Common; +using Newtonsoft.Json.Linq; + + + +namespace Inotify.ThridOauth.Common +{ + public class AuthorizeResult + { + public Code Code { get; set; } + + public string Error { get; set; } + + public JObject Result { get; set; } + + public string Token { get; set; } + } + + public enum Code + { + Success, + Exception, + UserInfoErrorMsg, + AccessTokenErrorMsg + } +} \ No newline at end of file diff --git a/Inotify/ThridOauth/Common/JsonCommon.cs b/Inotify/ThridOauth/Common/JsonCommon.cs new file mode 100644 index 0000000..2cd9899 --- /dev/null +++ b/Inotify/ThridOauth/Common/JsonCommon.cs @@ -0,0 +1,21 @@ +using Inotify.ThridOauth.Common; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + + + +namespace Inotify.ThridOauth.Common +{ + public static class JsonCommon + { + public static JObject Deserialize(string objStr) + { + return JsonConvert.DeserializeObject(objStr); + } + + public static string Serialize(object obj) + { + return JsonConvert.SerializeObject(obj); + } + } +} \ No newline at end of file diff --git a/Inotify/ThridOauth/Entity/CredentialSetting.cs b/Inotify/ThridOauth/Entity/CredentialSetting.cs new file mode 100644 index 0000000..eb9de9f --- /dev/null +++ b/Inotify/ThridOauth/Entity/CredentialSetting.cs @@ -0,0 +1,19 @@ +using Inotify.ThridOauth.Entity; + + + +namespace Inotify.ThridOauth.Entity +{ + public class CredentialSetting + { + /// + /// AppKey + /// + public string ClientId { get; set; } + + /// + /// AppSecret + /// + public string ClientSecret { get; set; } + } +} \ No newline at end of file diff --git a/Inotify/ThridOauth/Entity/FaceBookCredential.cs b/Inotify/ThridOauth/Entity/FaceBookCredential.cs new file mode 100644 index 0000000..857eefb --- /dev/null +++ b/Inotify/ThridOauth/Entity/FaceBookCredential.cs @@ -0,0 +1,10 @@ +using Inotify.ThridOauth.Entity; + + + +namespace Inotify.ThridOauth.Entity +{ + public class FaceBookCredential : CredentialSetting + { + } +} \ No newline at end of file diff --git a/Inotify/ThridOauth/Entity/GitHubCredential.cs b/Inotify/ThridOauth/Entity/GitHubCredential.cs new file mode 100644 index 0000000..1f593e0 --- /dev/null +++ b/Inotify/ThridOauth/Entity/GitHubCredential.cs @@ -0,0 +1,10 @@ +using Inotify.ThridOauth.Entity; + + + +namespace Inotify.ThridOauth.Entity +{ + public class GitHubCredential : CredentialSetting + { + } +} \ No newline at end of file diff --git a/Inotify/ThridOauth/Entity/QQCredential.cs b/Inotify/ThridOauth/Entity/QQCredential.cs new file mode 100644 index 0000000..d345fc7 --- /dev/null +++ b/Inotify/ThridOauth/Entity/QQCredential.cs @@ -0,0 +1,10 @@ +using Inotify.ThridOauth.Entity; + + + +namespace Inotify.ThridOauth.Entity +{ + public class QQCredential : CredentialSetting + { + } +} \ No newline at end of file diff --git a/Inotify/ThridOauth/Entity/WechatCredential.cs b/Inotify/ThridOauth/Entity/WechatCredential.cs new file mode 100644 index 0000000..f0255ce --- /dev/null +++ b/Inotify/ThridOauth/Entity/WechatCredential.cs @@ -0,0 +1,10 @@ +using Inotify.ThridOauth.Entity; + + + +namespace Inotify.ThridOauth.Entity +{ + public class WechatCredential : CredentialSetting + { + } +} \ No newline at end of file diff --git a/Inotify/ThridOauth/Entity/WeiBoCredential.cs b/Inotify/ThridOauth/Entity/WeiBoCredential.cs new file mode 100644 index 0000000..3d05977 --- /dev/null +++ b/Inotify/ThridOauth/Entity/WeiBoCredential.cs @@ -0,0 +1,10 @@ +using Inotify.ThridOauth.Entity; + + + +namespace Inotify.ThridOauth.Entity +{ + public class WeiBoCredential : CredentialSetting + { + } +} \ No newline at end of file diff --git a/Inotify/ThridOauth/IService/IFacebookLogin.cs b/Inotify/ThridOauth/IService/IFacebookLogin.cs new file mode 100644 index 0000000..c797fae --- /dev/null +++ b/Inotify/ThridOauth/IService/IFacebookLogin.cs @@ -0,0 +1,10 @@ +using Inotify.ThridOauth.IService; + + + +namespace Inotify.ThridOauth.IService +{ + public interface IFacebookLogin : ILogin + { + } +} \ No newline at end of file diff --git a/Inotify/ThridOauth/IService/IGitHubLogin.cs b/Inotify/ThridOauth/IService/IGitHubLogin.cs new file mode 100644 index 0000000..5b55157 --- /dev/null +++ b/Inotify/ThridOauth/IService/IGitHubLogin.cs @@ -0,0 +1,11 @@ +using Inotify.ThridOauth.IService; + + + +namespace Inotify.ThridOauth.IService +{ + public interface IGitHubLogin : ILogin + { + public string GetOauthUrl(); + } +} \ No newline at end of file diff --git a/Inotify/ThridOauth/IService/ILogin.cs b/Inotify/ThridOauth/IService/ILogin.cs new file mode 100644 index 0000000..743bcd5 --- /dev/null +++ b/Inotify/ThridOauth/IService/ILogin.cs @@ -0,0 +1,15 @@ +using Inotify.ThridOauth.Common; +using Inotify.ThridOauth.IService; + + + + +namespace Inotify.ThridOauth.IService +{ + public interface ILogin + { + AuthorizeResult Authorize(); + + string AuthorizeCode { get; } + } +} \ No newline at end of file diff --git a/Inotify/ThridOauth/IService/IQqLogin.cs b/Inotify/ThridOauth/IService/IQqLogin.cs new file mode 100644 index 0000000..c467c8a --- /dev/null +++ b/Inotify/ThridOauth/IService/IQqLogin.cs @@ -0,0 +1,10 @@ +using Inotify.ThridOauth.IService; + + + +namespace Inotify.ThridOauth.IService +{ + public interface IQqLogin : ILogin + { + } +} \ No newline at end of file diff --git a/Inotify/ThridOauth/IService/ISinaLogin.cs b/Inotify/ThridOauth/IService/ISinaLogin.cs new file mode 100644 index 0000000..fbe999a --- /dev/null +++ b/Inotify/ThridOauth/IService/ISinaLogin.cs @@ -0,0 +1,10 @@ +using Inotify.ThridOauth.IService; + + + +namespace Inotify.ThridOauth.IService +{ + public interface ISinaLogin : ILogin + { + } +} \ No newline at end of file diff --git a/Inotify/ThridOauth/IService/IWeChatLogin.cs b/Inotify/ThridOauth/IService/IWeChatLogin.cs new file mode 100644 index 0000000..7b9abcf --- /dev/null +++ b/Inotify/ThridOauth/IService/IWeChatLogin.cs @@ -0,0 +1,10 @@ +using Inotify.ThridOauth.IService; + + + +namespace Inotify.ThridOauth.IService +{ + public interface IWeChatLogin : ILogin + { + } +} \ No newline at end of file diff --git a/Inotify/ThridOauth/Service/FacebookLogin.cs b/Inotify/ThridOauth/Service/FacebookLogin.cs new file mode 100644 index 0000000..e039b44 --- /dev/null +++ b/Inotify/ThridOauth/Service/FacebookLogin.cs @@ -0,0 +1,125 @@ +using Inotify.ThridOauth.Common; +using Inotify.ThridOauth.Entity; +using Inotify.ThridOauth.IService; +using Inotify.ThridOauth.Service; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; + +namespace Inotify.ThridOauth.Service +{ + public class FacebookLogin : LoginBase, IFacebookLogin + { + private static string _authorizeUrl; + private static readonly string _oauthUrl = "https://graph.facebook.com/v2.8/oauth/access_token"; + private static readonly string _userInfoUrl = "https://graph.facebook.com/me"; + private static readonly string _userInfoUrlParams = "fields=picture{url},name&access_token="; + + public FacebookLogin(IHttpContextAccessor contextAccessor, IOptions options) : base( + contextAccessor) + { + Credential = options.Value; + _authorizeUrl = "https://www.facebook.com/v2.8/dialog/oauth?client_id=" + Credential.ClientId + + "&scope=email,public_profile&response_type=code&redirect_uri="; + } + + public AuthorizeResult Authorize() + { + try + { + var code = AuthorizeCode; + + if (string.IsNullOrEmpty(code)) + { + HttpContext.Response.Redirect(_authorizeUrl + RedirectUri, true); + + return null; + } + + if (!string.IsNullOrEmpty(code)) + { + var errMsg = string.Empty; + + var token = GetAccessToken(code, ref errMsg); + + if (!string.IsNullOrEmpty(errMsg)) + return new AuthorizeResult { Code = Code.UserInfoErrorMsg, Error = errMsg }; + var accessToken = token.Value("access_token"); + + var user = UserInfo(accessToken, ref errMsg); + + return string.IsNullOrEmpty(errMsg) + ? new AuthorizeResult { Code = Code.Success, Result = user, Token = accessToken } + : new AuthorizeResult { Code = Code.AccessTokenErrorMsg, Error = errMsg, Token = accessToken }; + } + } + + catch (Exception ex) + { + return new AuthorizeResult { Code = Code.Exception, Error = ex.Message }; + } + + return null; + } + + private JObject GetAccessToken(string code, ref string errMsg) + { + var data = new SortedDictionary + { + { "client_id", Credential.ClientId }, + { "client_secret", Credential.ClientSecret }, + { "code", code }, + { "redirect_uri", RedirectUri } + }; + + var Params = string.Join("&", data.Select(x => x.Key + "=" + x.Value).ToArray()); + + using var client = new HttpClient(); + try + { + var response = client.PostAsync(_oauthUrl, new StringContent(Params)).Result; + + var result = response.Content.ReadAsStringAsync().Result; + + return JsonCommon.Deserialize(result); + } + catch (Exception ex) + { + errMsg = ex.Message; + + return null; + } + } + + private JObject UserInfo(string token, ref string errMsg) + { + try + { + string result; + + using (var wc = new HttpClient()) + { + var content = _userInfoUrlParams + token; + + var response = wc.PostAsync(_userInfoUrl, new StringContent(content)).Result; + + result = response.Content.ReadAsStringAsync().Result; + } + + var user = JsonCommon.Deserialize(result); + + return user; + } + catch (Exception ex) + { + errMsg = ex.Message; + + return null; + } + } + } +} \ No newline at end of file diff --git a/Inotify/ThridOauth/Service/GitHubLogin.cs b/Inotify/ThridOauth/Service/GitHubLogin.cs new file mode 100644 index 0000000..afefbb0 --- /dev/null +++ b/Inotify/ThridOauth/Service/GitHubLogin.cs @@ -0,0 +1,162 @@ +using Inotify.Sends; +using Inotify.ThridOauth.Common; +using Inotify.ThridOauth.Entity; +using Inotify.ThridOauth.IService; +using Inotify.ThridOauth.Service; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; + + + + + + +namespace Inotify.ThridOauth.Service +{ + public class GitHubLogin : LoginBase, IGitHubLogin + { + private static readonly string _oauthUrl = "https://github.com/login/oauth/access_token"; + private static string _userInfoUrl = "https://api.github.com/user"; + private readonly string _authorizeUrl; + + + public GitHubLogin(IHttpContextAccessor contextAccessor, IOptions options) : base( + contextAccessor) + { + Credential = new CredentialSetting() + { + ClientId = SendCacheStore.GetSystemValue("githubClientID"), + ClientSecret = SendCacheStore.GetSystemValue("githubClientSecret") + }; + + _authorizeUrl = "https://github.com/login/oauth/authorize?client_id=" + Credential.ClientId; + } + + public AuthorizeResult Authorize() + { + try + { + var code = AuthorizeCode; + + if (string.IsNullOrEmpty(code)) + { + HttpContext.Response.Redirect(string.Format(_authorizeUrl), true); + return new AuthorizeResult { Code = Code.Exception, Error = "code is null " }; + } + else + { + var errorMsg = string.Empty; + + var token = GetAccessToken(code, ref errorMsg); + + if (!string.IsNullOrEmpty(errorMsg)) + return new AuthorizeResult + { + Code = Code.UserInfoErrorMsg, + Error = errorMsg + }; + var accessToken = token.Value("access_token"); + + var user = UserInfo(accessToken, ref errorMsg); + + return string.IsNullOrEmpty(errorMsg) + ? new AuthorizeResult { Code = Code.Success, Result = user, Token = accessToken } + : new AuthorizeResult { Code = Code.AccessTokenErrorMsg, Error = errorMsg, Token = accessToken }; + } + } + + catch (Exception ex) + { + return new AuthorizeResult { Code = Code.Exception, Error = ex.Message }; + } + } + + private JObject UserInfo(object accessToken, ref string errorMsg) + { + try + { + string result; + _userInfoUrl = string.Format(_userInfoUrl, accessToken); + using (var wc = GetHttpClientProxy()) + { + wc.DefaultRequestHeaders.Add("User-Agent", @"Mozilla/5.0 (Windows NT 10; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0"); + wc.DefaultRequestHeaders.Add("Authorization", "token " + accessToken); + var response = wc.GetAsync(_userInfoUrl).Result; + result = response.Content.ReadAsStringAsync().Result; + } + var user = JsonCommon.Deserialize(result); + return user; + } + catch (Exception ex) + { + errorMsg = ex.Message; + + return null; + } + } + + private JObject GetAccessToken(string code, ref string errorMsg) + { + var data = new SortedDictionary + { + {"client_id",Credential.ClientId}, + {"client_secret",Credential.ClientSecret}, + {"code",code} + }; + using var client = GetHttpClientProxy(); + try + { + client.DefaultRequestHeaders.Add("Accept", "application/json"); + var response = client.PostAsync(_oauthUrl, new FormUrlEncodedContent(data)).Result; + var result = response.Content.ReadAsStringAsync().Result; + if (result.Contains("bad_verification_code")) + { + errorMsg = "bad_verification_code"; + return null; + } + return JsonCommon.Deserialize(result); + + } + + catch (Exception ex) + { + errorMsg = ex.Message; + + return null; + } + } + + public string MidStrEx(string sourse, string startstr, string endstr) + { + string result = string.Empty; + int startindex, endindex; + try + { + startindex = sourse.IndexOf(startstr, StringComparison.Ordinal); + if (startindex == -1) + return result; + string tmpstr = sourse[(startindex + startstr.Length)..]; + endindex = tmpstr.IndexOf(endstr, StringComparison.Ordinal); + if (endindex == -1) + return result; + result = tmpstr.Remove(endindex); + } + catch (Exception ex) + { + return ex.Message; + } + return result; + } + + public string GetOauthUrl() + { + return _authorizeUrl; + } + } +} \ No newline at end of file diff --git a/Inotify/ThridOauth/Service/LoginBase.cs b/Inotify/ThridOauth/Service/LoginBase.cs new file mode 100644 index 0000000..fc6c862 --- /dev/null +++ b/Inotify/ThridOauth/Service/LoginBase.cs @@ -0,0 +1,62 @@ +using Inotify.Sends; +using Inotify.ThridOauth.Entity; +using Inotify.ThridOauth.Service; +using Microsoft.AspNetCore.Http; +using System; +using System.Net; +using System.Net.Http; + + + + +namespace Inotify.ThridOauth.Service +{ + public class LoginBase + { + private const string Code = "code"; + + protected static CredentialSetting Credential; + + protected readonly HttpContext HttpContext; + + protected LoginBase(IHttpContextAccessor contextAccessor) + { + HttpContext = contextAccessor.HttpContext; + } + + public string AuthorizeCode + { + get + { + var result = HttpContext.Request.Query[Code].ToString(); + + return !string.IsNullOrEmpty(result) ? result : string.Empty; + } + } + + protected string RedirectUri => + $"{HttpContext.Request.Scheme}://{HttpContext.Request.Host.Value}{HttpContext.Request.Path.Value}"; + + protected HttpClient GetHttpClientProxy() + { + if (SendCacheStore.GetSystemValue("proxyenable") == "true") + { + var proxyurl = SendCacheStore.GetSystemValue("proxy"); + if (proxyurl != null) + { + WebProxy proxy = new WebProxy + { + Address = new Uri(proxyurl) + }; + HttpClientHandler handler = new HttpClientHandler + { + Proxy = proxy + }; + HttpClient httpClient = new HttpClient(handler); + return httpClient; + } + } + return new HttpClient(); + } + } +} \ No newline at end of file diff --git a/Inotify/ThridOauth/Service/QQLogin.cs b/Inotify/ThridOauth/Service/QQLogin.cs new file mode 100644 index 0000000..e9431f0 --- /dev/null +++ b/Inotify/ThridOauth/Service/QQLogin.cs @@ -0,0 +1,151 @@ +using Inotify.ThridOauth.Common; +using Inotify.ThridOauth.Entity; +using Inotify.ThridOauth.IService; +using Inotify.ThridOauth.Service; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; + + + + + + +namespace Inotify.ThridOauth.Service +{ + public sealed class QqLogin : LoginBase, IQqLogin + { + private const string OauthUrl = "https://graph.qq.com/oauth2.0/token"; + + private const string OpenidUrl = "https://graph.qq.com/oauth2.0/me"; + + private const string UserInfoUrl = "https://graph.qq.com/user/get_user_info"; + private readonly string _authorizeUrl; + private readonly string _userInfoUrlParams; + + + public QqLogin(IHttpContextAccessor contextAccessor, IOptions options) : base( + contextAccessor) + { + Credential = options.Value; + _authorizeUrl = "https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=" + + Credential.ClientId + "&redirect_uri="; + _userInfoUrlParams = + "format=json&oauth_consumer_key=" + Credential.ClientId + "&openid={0}&access_token={1}"; + } + + public AuthorizeResult Authorize() + { + try + { + var code = AuthorizeCode; + + if (string.IsNullOrEmpty(code)) + { + HttpContext.Response.Redirect(_authorizeUrl + RedirectUri, true); + + return null; + } + + if (!string.IsNullOrEmpty(code)) + { + var errorMsg = string.Empty; + + var token = GetAccessToken(code, ref errorMsg); + + if (!string.IsNullOrEmpty(errorMsg)) + return new AuthorizeResult { Code = Code.UserInfoErrorMsg, Error = errorMsg }; + var accessToken = token["access_token"]; + + var user = UserInfo(accessToken, ref errorMsg); + + return string.IsNullOrEmpty(errorMsg) + ? new AuthorizeResult { Code = Code.Success, Result = user, Token = accessToken } + : new AuthorizeResult { Code = Code.AccessTokenErrorMsg, Error = errorMsg, Token = accessToken }; + } + } + + catch (Exception ex) + { + return new AuthorizeResult { Code = Code.Exception, Error = ex.Message }; + } + + return null; + } + + private Dictionary GetAccessToken(string code, ref string errMsg) + { + var data = new SortedDictionary + { + {"client_id", Credential.ClientId}, + {"client_secret", Credential.ClientSecret}, + {"grant_type", "authorization_code"}, + {"code", code}, + {"redirect_uri", RedirectUri} + }; + + var Params = string.Join("&", data.Select(x => x.Key + "=" + x.Value).ToArray()); + + using var wb = new HttpClient(); + try + { + var response = wb.PostAsync(OauthUrl, new StringContent(Params)).Result; + + var result = response.Content.ReadAsStringAsync().Result; + + var kvs = result.Split(new[] { "&" }, StringSplitOptions.RemoveEmptyEntries); + + return kvs.Select(v => v.Split(new[] { "=" }, StringSplitOptions.RemoveEmptyEntries)) + .ToDictionary(kv => kv[0], kv => kv[1]); + } + catch (Exception ex) + { + errMsg = ex.Message; + + return null; + } + } + + private JObject UserInfo(string token, ref string errMsg) + { + try + { + string result; + + using (var wc = new HttpClient()) + { + var response = wc.PostAsync(OpenidUrl, new StringContent("access_token=" + token)).Result; + result = response.Content.ReadAsStringAsync().Result; + } + + result = result.Replace("callback(", string.Empty).Replace(");", string.Empty).Trim(); + + var openid = JsonCommon.Deserialize(result).Value("openid"); + + using (var wc = new HttpClient()) + { + var response = wc.PostAsync(UserInfoUrl, + new StringContent(string.Format(_userInfoUrlParams, openid, token))).Result; + + result = response.Content.ReadAsStringAsync().Result; + } + + var user = JsonCommon.Deserialize(result); + + user.Add("openid", openid); + + return user; + } + catch (Exception ex) + { + errMsg = ex.Message; + + return null; + } + } + } +} \ No newline at end of file diff --git a/Inotify/ThridOauth/Service/WeChatLogin.cs b/Inotify/ThridOauth/Service/WeChatLogin.cs new file mode 100644 index 0000000..932a600 --- /dev/null +++ b/Inotify/ThridOauth/Service/WeChatLogin.cs @@ -0,0 +1,136 @@ +using Inotify.ThridOauth.Common; +using Inotify.ThridOauth.Entity; +using Inotify.ThridOauth.IService; +using Inotify.ThridOauth.Service; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; + + + + + + +namespace Inotify.ThridOauth.Service +{ + public sealed class WeChatLogin : LoginBase, IWeChatLogin + { + private const string OauthUrl = "https://api.weixin.qq.com/sns/oauth2/access_token"; + + private const string UserInfoUrl = "https://api.weixin.qq.com/sns/userinfo"; + + private const string UserInfoUrlParams = "access_token={0}&openid={1}&lang=zh_CN"; + + private readonly string _authorizeUrl; + + + public WeChatLogin(IHttpContextAccessor contextAccessor, IOptions options) : base( + contextAccessor) + { + Credential = options.Value; + _authorizeUrl = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=" + Credential.ClientId + + "&redirect_uri={0}&response_type=code&scope=snsapi_userinfo#wechat_redirect"; + } + + public AuthorizeResult Authorize() + { + try + { + var code = AuthorizeCode; + + if (string.IsNullOrEmpty(code)) + { + HttpContext.Response.Redirect(string.Format(_authorizeUrl, RedirectUri), true); + + return null; + } + + if (!string.IsNullOrEmpty(code)) + { + var errorMsg = string.Empty; + + var token = GetAccessToken(code, ref errorMsg); + + if (!string.IsNullOrEmpty(errorMsg)) return new AuthorizeResult { Code = Code.UserInfoErrorMsg, Error = errorMsg }; + var accessToken = token.Value("access_token"); + + var uid = token.Value("openid"); + + var user = UserInfo(accessToken, uid, ref errorMsg); + + return string.IsNullOrEmpty(errorMsg) + ? new AuthorizeResult { Code = Code.Success, Result = user, Token = accessToken } + : new AuthorizeResult { Code = Code.AccessTokenErrorMsg, Error = errorMsg, Token = accessToken }; + } + } + + catch (Exception ex) + { + return new AuthorizeResult { Code = Code.Exception, Error = ex.Message }; + } + + return null; + } + + private static JObject GetAccessToken(string code, ref string errMsg) + { + var data = new SortedDictionary + { + {"appid", Credential.ClientId}, {"secret", Credential.ClientSecret}, + {"grant_type", "authorization_code"}, {"code", code} + }; + + var Params = string.Join("&", data.Select(x => x.Key + "=" + x.Value).ToArray()); + + using var client = new HttpClient(); + try + { + var response = client.PostAsync(OauthUrl, new StringContent(Params)).Result; + + var result = response.Content.ReadAsStringAsync().Result; + + return JsonCommon.Deserialize(result); + } + + catch (Exception ex) + { + errMsg = ex.Message; + + return null; + } + } + + private static JObject UserInfo(string token, string uid, ref string errMsg) + { + try + { + string result; + + using (var wc = new HttpClient()) + { + var content = new StringContent(string.Format(UserInfoUrlParams, token, uid)); + + var response = wc.PostAsync(UserInfoUrl, content).Result; + + result = response.Content.ReadAsStringAsync().Result; + } + + var user = JsonCommon.Deserialize(result); + + user.Add("uid", uid); + + return user; + } + catch (Exception ex) + { + errMsg = ex.Message; + + return null; + } + } + } +} \ No newline at end of file diff --git a/Inotify/ThridOauth/Service/WeiBoLogin.cs b/Inotify/ThridOauth/Service/WeiBoLogin.cs new file mode 100644 index 0000000..e3c2d0c --- /dev/null +++ b/Inotify/ThridOauth/Service/WeiBoLogin.cs @@ -0,0 +1,136 @@ +using Inotify.ThridOauth.Common; +using Inotify.ThridOauth.Entity; +using Inotify.ThridOauth.IService; +using Inotify.ThridOauth.Service; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; + + + + + + +namespace Inotify.ThridOauth.Service +{ + public sealed class WeiBoLogin : LoginBase, ISinaLogin + { + private const string OauthUrl = "https://api.weibo.com/oauth2/access_token?"; + private const string UserInfoUrl = "https://api.weibo.com/2/users/show.json?"; + private const string UserInfoUrlParams = "uid={0}&access_token={1}"; + private readonly string _authorizeUrl; + + public WeiBoLogin(IHttpContextAccessor contextAccessor, IOptions options) : base( + contextAccessor) + { + Credential = options.Value; + _authorizeUrl = "https://api.weibo.com/oauth2/authorize?client_id=" + Credential.ClientId + + "&response_type=code&redirect_uri="; + } + + + public AuthorizeResult Authorize() + { + try + { + var code = AuthorizeCode; + + if (string.IsNullOrEmpty(code)) + { + HttpContext.Response.Redirect(_authorizeUrl + RedirectUri, true); + + return null; + } + + if (!string.IsNullOrEmpty(code)) + { + var errorMsg = string.Empty; + + var token = GetAccessToken(code, ref errorMsg); + + if (!string.IsNullOrEmpty(errorMsg)) + return new AuthorizeResult { Code = Code.UserInfoErrorMsg, Error = errorMsg }; + var accessToken = token.Value("access_token"); + + var uid = token.Value("uid"); + + var user = UserInfo(accessToken, uid, ref errorMsg); + + return string.IsNullOrEmpty(errorMsg) + ? new AuthorizeResult { Code = Code.Success, Result = user, Token = accessToken } + : new AuthorizeResult { Code = Code.AccessTokenErrorMsg, Error = errorMsg, Token = accessToken }; + } + } + + catch (Exception ex) + { + return new AuthorizeResult { Code = Code.Exception, Error = ex.Message }; + } + + return null; + } + + private JObject GetAccessToken(string code, ref string errMsg) + { + var data = new SortedDictionary + { + {"client_id", Credential.ClientId}, + {"client_secret", Credential.ClientSecret}, + {"grant_type", "authorization_code"}, + {"code", code}, + {"redirect_uri", RedirectUri} + }; + + var Params = string.Join("&", data.Select(x => x.Key + "=" + x.Value).ToArray()); + + using var wb = new HttpClient(); + try + { + var accessTokenUrl = OauthUrl + Params; + + var response = wb.PostAsync(accessTokenUrl, null).Result; + + var result = response.Content.ReadAsStringAsync().Result; + + return JsonCommon.Deserialize(result); + } + catch (Exception ex) + { + errMsg = ex.Message; + + return null; + } + } + + private static JObject UserInfo(string token, string uid, ref string errMsg) + { + try + { + string result; + + using (var wc = new HttpClient()) + { + var url = UserInfoUrl + string.Format(UserInfoUrlParams, uid, token); + + var response = wc.GetAsync(url).Result; + + result = response.Content.ReadAsStringAsync().Result; + } + + var user = JsonCommon.Deserialize(result); + + return user; + } + catch (Exception ex) + { + errMsg = ex.Message; + + return null; + } + } + } +} \ No newline at end of file diff --git a/Inotify/ThridOauth/ThridPartyLoginExtensions.cs b/Inotify/ThridOauth/ThridPartyLoginExtensions.cs new file mode 100644 index 0000000..69947c8 --- /dev/null +++ b/Inotify/ThridOauth/ThridPartyLoginExtensions.cs @@ -0,0 +1,61 @@ +using Inotify.ThridOauth; +using Inotify.ThridOauth.Entity; +using Inotify.ThridOauth.IService; +using Inotify.ThridOauth.Service; +using Microsoft.Extensions.DependencyInjection; +using System; + + + + + +namespace Inotify.ThridOauth +{ + public static class ThridPartyLoginExtensions + { + public static IServiceCollection AddWeChatLogin(this IServiceCollection services, + Action credential) + { + if (services == null) throw new ArgumentNullException(nameof(services)); + services.Configure(credential); + services.AddScoped(); + return services; + } + + public static IServiceCollection AddQqLogin(this IServiceCollection services, + Action credential) + { + if (services == null) throw new ArgumentNullException(nameof(services)); + services.Configure(credential); + services.AddScoped(); + return services; + } + + public static IServiceCollection AddSinaLogin(this IServiceCollection services, + Action credential) + { + if (services == null) throw new ArgumentNullException(nameof(services)); + services.Configure(credential); + services.AddScoped(); + return services; + } + + public static IServiceCollection AddFackbookLogin(this IServiceCollection services, + Action credential) + { + if (services == null) throw new ArgumentNullException(nameof(services)); + services.Configure(credential); + services.AddScoped(); + return services; + } + + public static IServiceCollection AddGitHubLogin(this IServiceCollection services, + Action credential) + { + if (services == null) throw new ArgumentNullException(nameof(services)); + services.Configure(credential); + services.AddScoped(); + return services; + } + } +} \ No newline at end of file diff --git a/Inotify/appsettings.Development.json b/Inotify/appsettings.Development.json new file mode 100644 index 0000000..b5464e7 --- /dev/null +++ b/Inotify/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/Inotify/appsettings.json b/Inotify/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/Inotify/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/README.md b/README.md index 0c9f015..6bd86ea 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,6 @@ # inotify a message notify center for weChat&telegarm&email +简易的消息通知系统,支持企业微信、邮件推送 +[![docker_service](https://github.com/xpnas/Inotify/actions/workflows/dockerservice.yml/badge.svg)](https://github.com/xpnas/Inotify/actions/workflows/dockerservice.yml) +[![docker_vue](https://github.com/xpnas/Inotify/actions/workflows/dockervue.yml/badge.svg)](https://github.com/xpnas/Inotify/actions/workflows/dockervue.yml) +