├── .eslintrc ├── www ├── favicon.ico └── static │ ├── bootstrap.min.js │ └── jquery.slim.min.js ├── src ├── logic │ ├── index.js │ ├── base.js │ └── user.js ├── bootstrap │ └── master.js ├── config │ ├── router.js │ ├── extend.js │ ├── middleware.js │ ├── config.js │ └── adapter.js ├── controller │ ├── index.js │ ├── base.js │ └── user.js └── service │ ├── weibo.js │ ├── github.js │ ├── baidu.js │ └── qq.js ├── .dockerignore ├── production.js ├── development.js ├── .editorconfig ├── view ├── inc │ ├── footer.html │ ├── showMsg.html │ └── header.html ├── index_index.html ├── layout.html ├── user_changepassword.html ├── user_index.html ├── user_login.html ├── user_reg.html └── user_oauth.html ├── Dockerfile ├── .gitignore ├── .github └── workflows │ ├── docker-build-test.yml │ └── publish.yml ├── LICENSE ├── package.json └── README.md /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "think" 3 | } -------------------------------------------------------------------------------- /www/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuexb/web-oauth-app/HEAD/www/favicon.ico -------------------------------------------------------------------------------- /src/logic/index.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base.js'); 2 | 3 | module.exports = class extends Base { 4 | }; 5 | -------------------------------------------------------------------------------- /src/bootstrap/master.js: -------------------------------------------------------------------------------- 1 | const process = require('process'); 2 | process.on('SIGINT', () => { 3 | process.exit(0); 4 | }); 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /app/ 3 | *.log 4 | .cache 5 | .git 6 | /dist/ 7 | runtime/ 8 | output/ 9 | .vscode 10 | .DS_Store -------------------------------------------------------------------------------- /src/config/router.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | ['/user/login/:type', 'user/oauthLogin'], 3 | ['/user/login/:type/callback', 'user/oauthCallback'] 4 | ]; 5 | -------------------------------------------------------------------------------- /production.js: -------------------------------------------------------------------------------- 1 | const Application = require('thinkjs'); 2 | 3 | const instance = new Application({ 4 | ROOT_PATH: __dirname, 5 | proxy: true, // use proxy 6 | env: 'production' 7 | }); 8 | 9 | instance.run(); 10 | -------------------------------------------------------------------------------- /src/controller/index.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base.js'); 2 | 3 | module.exports = class extends Base { 4 | /** 5 | * 首页 6 | * 7 | * @return {Object} 8 | */ 9 | async indexAction() { 10 | return this.display(); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /development.js: -------------------------------------------------------------------------------- 1 | const Application = require('thinkjs'); 2 | const watcher = require('think-watcher'); 3 | 4 | const instance = new Application({ 5 | ROOT_PATH: __dirname, 6 | watcher: watcher, 7 | env: 'development' 8 | }); 9 | 10 | instance.run(); 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | trim_trailing_whitespace = true 9 | 10 | [*.md] 11 | indent_size = 4 12 | trim_trailing_whitespace = false 13 | 14 | [*.js] 15 | insert_final_newline = true -------------------------------------------------------------------------------- /src/config/extend.js: -------------------------------------------------------------------------------- 1 | const view = require('think-view'); 2 | const model = require('think-model'); 3 | const cache = require('think-cache'); 4 | const session = require('think-session'); 5 | const fetch = require('think-fetch'); 6 | 7 | module.exports = [ 8 | view, // make application support view 9 | model(think.app), 10 | cache, 11 | session, 12 | fetch 13 | ]; 14 | -------------------------------------------------------------------------------- /view/inc/footer.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /view/inc/showMsg.html: -------------------------------------------------------------------------------- 1 | {% if showMsg %} 2 |
3 | 9 |
10 | {% endif %} -------------------------------------------------------------------------------- /view/index_index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}首页 - {{ config.pkg.name }}{% endblock %} 4 | 5 | {% block content %} 6 |

{{ config.pkg.description }}

7 |

8 | 这是一个用来实验WEB网站集成第三方登录的项目,一个帐号可以关联多个第三方网站,如:GitHub、微信、微博、QQ、百度等,为了更好的熟悉整个流程,该项目应运而生。 9 |

10 |

11 | 查看更多介绍>> 12 |

13 | {% endblock %} -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-alpine 2 | 3 | LABEL maintainer="xuexb " 4 | LABEL org.opencontainers.image.source https://github.com/xuexb/web-oauth-app 5 | 6 | ENV DOCKER true 7 | 8 | # Create app directory 9 | WORKDIR /usr/src/app 10 | 11 | COPY package.json . 12 | COPY yarn.lock . 13 | 14 | RUN yarn install --production \ 15 | && yarn cache clean 16 | 17 | COPY . . 18 | 19 | # forward request and error logs to docker log collector 20 | RUN ln -sf /dev/stdout /var/log/node.log 21 | 22 | EXPOSE 8080 23 | CMD [ "node", "production.js" ] -------------------------------------------------------------------------------- /src/logic/base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 信息在 cookie 里的名称 3 | * 4 | * @const 5 | * @type {string} 6 | */ 7 | const MSG_COOKIE_KEY = 'showMsg'; 8 | 9 | /** 10 | * 信息在模板里的变量名称 11 | * 12 | * @const 13 | * @type {string} 14 | */ 15 | const MSG_TPL_KEY = 'showMsg'; 16 | 17 | module.exports = class extends think.Logic { 18 | showMsg(url) { 19 | const text = Object.values(this.validateErrors).join(', '); 20 | 21 | if (this.isPost || url) { 22 | this.cookie(MSG_COOKIE_KEY, encodeURIComponent(text)); 23 | return this.redirect(url || this.ctx.url); 24 | } 25 | 26 | this.assign(MSG_TPL_KEY, text); 27 | return this.display(); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /view/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}hello world!{% endblock %} 6 | 7 | 8 | 9 | 10 | 11 | {% block header %}{% include 'inc/header.html' %}{% endblock %} 12 | 13 | {% block showErrorMsg %}{% include 'inc/showMsg.html' %}{% endblock %} 14 | 15 |
16 | {% block content %}{% endblock %} 17 |
18 | 19 | {% block footer %}{% include 'inc/footer.html' %}{% endblock %} 20 | 21 | {% block scripts %}{% endblock %} 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage/ 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Dependency directory 23 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 24 | node_modules/ 25 | 26 | # IDE config 27 | .idea 28 | 29 | # output 30 | output/ 31 | output.tar.gz 32 | 33 | runtime/ 34 | app/ 35 | 36 | config.development.js 37 | adapter.development.js 38 | config.production.js 39 | adapter.production.js 40 | 41 | package-lock.json 42 | .DS_Store 43 | .vscode/ -------------------------------------------------------------------------------- /src/config/middleware.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pkg = require('../../package.json'); 3 | const isDev = think.env === 'development'; 4 | 5 | module.exports = [ 6 | { 7 | handle: 'meta', 8 | options: { 9 | logRequest: isDev, 10 | sendResponseTime: isDev 11 | } 12 | }, 13 | { 14 | handle: 'resource', 15 | enable: true, 16 | options: { 17 | root: path.join(think.ROOT_PATH, 'www'), 18 | publicPath: /^\/(static|favicon\.ico)/ 19 | } 20 | }, 21 | { 22 | handle: 'trace', 23 | enable: !think.isCli, 24 | options: { 25 | debug: isDev 26 | } 27 | }, 28 | { 29 | handle: 'payload', 30 | options: {} 31 | }, 32 | { 33 | handle: 'router', 34 | options: { 35 | prefix: [pkg.prefix] 36 | } 37 | }, 38 | 'logic', 39 | 'controller' 40 | ]; 41 | -------------------------------------------------------------------------------- /.github/workflows/docker-build-test.yml: -------------------------------------------------------------------------------- 1 | name: Docker Build Test 2 | 3 | on: 4 | push: 5 | tags: 6 | - '!v**' 7 | branches: 8 | - '**' 9 | pull_request: 10 | 11 | env: 12 | REGISTRY: ghcr.io 13 | IMAGE_NAME: ${{ github.repository }} 14 | 15 | jobs: 16 | build-and-push-image: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v3 22 | 23 | - name: Extract metadata (tags, labels) for Docker 24 | id: meta 25 | uses: docker/metadata-action@v4 26 | with: 27 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 28 | 29 | - name: Build Docker image 30 | uses: docker/build-push-action@v4 31 | with: 32 | context: . 33 | tags: ${{ steps.meta.outputs.tags }} 34 | labels: ${{ steps.meta.outputs.labels }} -------------------------------------------------------------------------------- /src/config/config.js: -------------------------------------------------------------------------------- 1 | const pkg = require('../../package.json'); 2 | 3 | module.exports = Object.assign({}, {pkg}, { 4 | workers: 1, 5 | port: 8080, 6 | oauth: { 7 | github: { 8 | client_id: process.env.OAUTH_GITHUB_CLIENT_ID || '', 9 | client_secret: process.env.OAUTH_GITHUB_CLIENT_SECRET || '' 10 | }, 11 | qq: { 12 | appid: process.env.OAUTH_QQ_APPID || 0, 13 | appkey: process.env.OAUTH_QQ_APPKEY || '', 14 | callback: process.env.OAUTH_QQ_CALLBACK || '' 15 | }, 16 | weibo: { 17 | appkey: process.env.OAUTH_WEIBO_APPKEY || 0, 18 | appsecret: process.env.OAUTH_WEIBO_APPSECRET || '', 19 | callback: process.env.OAUTH_WEIBO_CALLBACK || '' 20 | }, 21 | baidu: { 22 | appkey: process.env.OAUTH_BAIDU_APPKEY || '', 23 | secretkey: process.env.OAUTH_BAIDU_SECRETKEY || '', 24 | callback: process.env.OAUTH_BAIDU_CALLBACK || '' 25 | } 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 前端小武 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /view/user_changepassword.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}修改密码 - {{ config.pkg.name }}{% endblock %} 4 | 5 | {% block content %} 6 |
7 | 8 |
9 | 10 | 11 |
12 |
13 | 14 | 15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 |
23 |
24 | {% endblock %} -------------------------------------------------------------------------------- /view/user_index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}用户中心 - {{ config.pkg.name }}{% endblock %} 4 | 5 | {% block content %} 6 | 13 | 14 | 31 | {% endblock %} -------------------------------------------------------------------------------- /view/user_login.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}登录 - {{ config.pkg.name }}{% endblock %} 4 | 5 | {% block content %} 6 |
7 | {% if oauth %} 8 | 11 | {% endif %} 12 |
13 | 14 | 15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 | 没有帐号?立即注册 23 |
24 |
25 | 第三方登录:GitHubQQ微博百度 26 |
27 |
28 | {% endblock %} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-oauth-app", 3 | "description": "第三方登录WEB网站示例", 4 | "version": "1.1.4", 5 | "author": "xuexb ", 6 | "prefix": "", 7 | "scripts": { 8 | "start": "yarn dev", 9 | "dev": "node development.js", 10 | "lint": "eslint src/", 11 | "lint-fix": "eslint --fix src/" 12 | }, 13 | "dependencies": { 14 | "github": "^13.1.0", 15 | "think-cache": "^1.0.0", 16 | "think-cache-file": "^1.0.8", 17 | "think-fetch": "^1.1.0", 18 | "think-logger3": "^1.0.0", 19 | "think-model": "^1.0.0", 20 | "think-model-mysql": "^1.0.0", 21 | "think-session": "^1.0.0", 22 | "think-session-file": "^1.0.5", 23 | "think-view": "^1.0.0", 24 | "think-view-nunjucks": "^1.0.1", 25 | "thinkjs": "^3.0.0" 26 | }, 27 | "devDependencies": { 28 | "think-watcher": "^3.0.0", 29 | "eslint": "^4.2.0", 30 | "eslint-config-think": "^1.0.0", 31 | "ava": "^0.18.0", 32 | "nyc": "^7.0.0" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/xuexb/web-oauth-app.git" 37 | }, 38 | "license": "MIT", 39 | "engines": { 40 | "node": ">=7.8.0" 41 | }, 42 | "main": "development.js", 43 | "keywords": [ 44 | "oauth", 45 | "第三方登录" 46 | ], 47 | "bugs": { 48 | "url": "https://github.com/xuexb/web-oauth-app/issues" 49 | }, 50 | "homepage": "https://github.com/xuexb/web-oauth-app#readme" 51 | } 52 | -------------------------------------------------------------------------------- /src/service/weibo.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | module.exports = class extends think.Service { 5 | constructor(ctx, config) { 6 | super(); 7 | this.ctx = ctx; 8 | this.config = config; 9 | } 10 | 11 | login() { 12 | this.ctx.status = 302; 13 | this.ctx.redirect(`https://api.weibo.com/oauth2/authorize?client_id=${this.config.appkey}&redirect_uri=${encodeURIComponent(this.config.callback)}`); 14 | } 15 | 16 | async getToken() { 17 | const self = this; 18 | return await this.fetch(`https://api.weibo.com/oauth2/access_token?client_id=${this.config.appkey}&client_secret=${this.config.appsecret}&grant_type=authorization_code&code=${this.ctx.query.code}&redirect_uri=${encodeURIComponent(this.config.callback)}`, { 19 | method: 'POST', 20 | headers: { 21 | 'Cache-Control': 'no-cache', 22 | }, 23 | timeout: 10000 24 | }).then(res => res.json()).then(res => { 25 | if (!res.access_token) { 26 | return this.login(); 27 | } 28 | return res; 29 | }); 30 | } 31 | 32 | async getUserInfo() { 33 | const data = await this.getToken(); 34 | if (!data) { 35 | return {}; 36 | } 37 | const userinfo = await this.fetch(`https://api.weibo.com/2/users/show.json?access_token=${data.access_token}&uid=${data.uid}`).then(res => res.json()); 38 | return { 39 | uid: data.access_token, 40 | name: userinfo.screen_name, 41 | info: { 42 | avatar: userinfo.avatar_hd 43 | }, 44 | type: 'weibo' 45 | }; 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /src/service/github.js: -------------------------------------------------------------------------------- 1 | const github = new require('github')({ 2 | timeout: 10000 3 | }); 4 | 5 | module.exports = class extends think.Service { 6 | constructor(ctx, config) { 7 | super(); 8 | this.ctx = ctx; 9 | this.config = config; 10 | } 11 | 12 | login() { 13 | this.ctx.status = 302; 14 | this.ctx.redirect(`https://github.com/login/oauth/authorize?client_id=${this.config.client_id}&scope=read:user,user:email`); 15 | } 16 | 17 | async getToken() { 18 | return await this.fetch(`https://github.com/login/oauth/access_token?client_id=${this.config.client_id}&client_secret=${this.config.client_secret}&code=${this.ctx.query.code}`, { 19 | headers: { 20 | 'Cache-Control': 'no-cache', 21 | 'Accept': 'application/json' 22 | }, 23 | timeout: 10000 24 | }).then(res => res.json()).then(res => { 25 | // 如果过期再请求个新的 26 | if (!res.access_token) { 27 | return this.login(); 28 | } 29 | this.access_token = res.access_token; 30 | return res.access_token; 31 | }); 32 | } 33 | 34 | async getUserInfo() { 35 | const token = await this.getToken(); 36 | if (!token) { 37 | return {}; 38 | } 39 | github.authenticate({ 40 | type: 'token', 41 | token 42 | }); 43 | const userinfo = await github.users.get({}); 44 | return { 45 | uid: userinfo.data.id, 46 | name: userinfo.data.name || 'github用户', 47 | info: { 48 | email: userinfo.data.email, 49 | avatar: userinfo.data.avatar_url 50 | }, 51 | token, 52 | type: 'github' 53 | }; 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /view/user_reg.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}注册 - {{ config.pkg.name }}{% endblock %} 4 | 5 | {% block content %} 6 |
7 | {% if oauth %} 8 | 11 | {% endif %} 12 |
13 | 14 | 15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 | 23 |
24 |
25 | 26 | 已有帐号?立即登录 27 |
28 |
29 | 第三方登录:GitHubQQ微博百度 30 |
31 |
32 | {% endblock %} -------------------------------------------------------------------------------- /src/service/baidu.js: -------------------------------------------------------------------------------- 1 | module.exports = class extends think.Service { 2 | constructor(ctx, config) { 3 | super(); 4 | this.ctx = ctx; 5 | this.config = config; 6 | } 7 | 8 | login() { 9 | const display = /Android|webOS|iPhone|iPad|iPod|ucweb|BlackBerry|IEMobile|Opera Mini/i.test(this.ctx.request.header['user-agent']) ? 'mobile' : 'popup'; 10 | this.ctx.status = 302; 11 | this.ctx.redirect(`http://openapi.baidu.com/oauth/2.0/authorize?response_type=code&client_id=${this.config.appkey}&redirect_uri=${encodeURIComponent(this.config.callback)}&scope=basic&display=${display}`); 12 | } 13 | 14 | async getToken() { 15 | const self = this; 16 | return await this.fetch(`https://openapi.baidu.com/oauth/2.0/token?grant_type=authorization_code&code=${this.ctx.query.code}&client_id=${this.config.appkey}&client_secret=${this.config.secretkey}&redirect_uri=${encodeURIComponent(this.config.callback)}`, { 17 | method: 'POST', 18 | headers: { 19 | 'Cache-Control': 'no-cache', 20 | }, 21 | timeout: 10000 22 | }).then(res => res.json()).then(res => { 23 | if (!res.access_token) { 24 | return this.login(); 25 | } 26 | return res; 27 | }); 28 | } 29 | 30 | async getUserInfo() { 31 | const data = await this.getToken(); 32 | if (!data) { 33 | return {}; 34 | } 35 | const userinfo = await this.fetch(`https://openapi.baidu.com/rest/2.0/passport/users/getInfo?access_token=${data.access_token}`).then(res => res.json()); 36 | return { 37 | uid: userinfo.userid, 38 | name: userinfo.username, 39 | info: { 40 | avatar: `http://tb.himg.baidu.com/sys/portrait/item/${userinfo.portrait}` 41 | }, 42 | type: 'baidu' 43 | }; 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/controller/base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 信息在 cookie 里的名称 3 | * 4 | * @const 5 | * @type {string} 6 | */ 7 | const MSG_COOKIE_KEY = 'showMsg'; 8 | 9 | /** 10 | * 信息在模板里的变量名称 11 | * 12 | * @const 13 | * @type {string} 14 | */ 15 | const MSG_TPL_KEY = 'showMsg'; 16 | 17 | /** 18 | * 信息类似在模板里的变量名称 19 | * 20 | * @const 21 | * @type {string} 22 | */ 23 | const MSG_TYPE_TPL_KEY = 'showMsgType'; 24 | 25 | module.exports = class extends think.Controller { 26 | /** 27 | * 前置操作,处理用户登录状态、显示信息 28 | */ 29 | async __before() { 30 | const userinfo = await this.session('userinfo'); 31 | this.userinfo = userinfo; 32 | this.isLogin = !!userinfo; 33 | this.assign({userinfo}); 34 | 35 | // 处理显示信息 36 | const msg = this.cookie(MSG_COOKIE_KEY); 37 | if (msg) { 38 | this.assign(MSG_TPL_KEY, decodeURIComponent(msg)); 39 | this.assign(MSG_TYPE_TPL_KEY, decodeURIComponent(msg).indexOf('成功') > -1 ? 'success' : 'danger'); 40 | this.cookie(MSG_COOKIE_KEY, null); 41 | } 42 | } 43 | 44 | /** 45 | * 显示信息到页面中 46 | * 47 | * @param {string} text 显示文本 48 | * @param {string} url 跳转链接 49 | * 50 | * @return {Object} 51 | */ 52 | async showMsg(text, url) { 53 | if (this.isPost || url) { 54 | this.cookie(MSG_COOKIE_KEY, encodeURIComponent(text)); 55 | return this.redirect(url || this.ctx.url); 56 | } 57 | 58 | this.assign(MSG_TPL_KEY, text); 59 | return this.display(); 60 | } 61 | 62 | /** 63 | * 写入用户信息到 session 64 | * 65 | * @param {number} id 用户ID 66 | */ 67 | async writeUserinfo(id) { 68 | const user = await this.model('user').where({ 69 | id 70 | }).find(); 71 | 72 | if (!think.isEmpty(user)) { 73 | delete user.password; 74 | await this.session('userinfo', user); 75 | } 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /view/inc/header.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /view/user_oauth.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}注册 - {{ config.pkg.name }}{% endblock %} 4 | 5 | {% block content %} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% for item in items %} 17 | 18 | 19 | 20 | 21 | 41 | 42 | {% else %} 43 | 44 | 45 | 46 | {% endfor %} 47 | 48 |
类型名称标识操作
{{ item.type }}{{ item.name | default('-') }}{{ item.uid | default('-') }} 22 | {% if item.uid %} 23 | 36 | 37 | {% else %} 38 | 39 | {% endif %} 40 |
没有绑定第三方授权登录
49 | {% endblock %} -------------------------------------------------------------------------------- /src/service/qq.js: -------------------------------------------------------------------------------- 1 | module.exports = class extends think.Service { 2 | constructor(ctx, config) { 3 | super(); 4 | this.ctx = ctx; 5 | this.config = config; 6 | } 7 | 8 | login() { 9 | const display = /Android|webOS|iPhone|iPad|iPod|ucweb|BlackBerry|IEMobile|Opera Mini/i.test(this.ctx.request.header['user-agent']) ? 'mobile' : 'pc'; 10 | this.ctx.status = 302; 11 | this.ctx.redirect(`https://graph.qq.com/oauth2.0/show?which=Login&display=${display}&client_id=${this.config.appid}&response_type=code&scope=get_user_info&redirect_uri=${encodeURIComponent(this.config.callback)}`); 12 | } 13 | 14 | async getToken() { 15 | const self = this; 16 | return await this.fetch(`https://graph.qq.com/oauth2.0/token?grant_type=authorization_code&client_id=${this.config.appid}&client_secret=${this.config.appkey}&code=${this.ctx.query.code}&redirect_uri=${encodeURIComponent(this.config.callback)}`, { 17 | headers: { 18 | 'Cache-Control': 'no-cache', 19 | }, 20 | timeout: 10000 21 | }).then(res => res.text()).then(text => { 22 | const matched = text.match(/access_token=([^&]+)&/); 23 | if (!matched) { 24 | return this.login(); 25 | } 26 | return matched[1]; 27 | }).then(async function (token) { 28 | return await self.fetch(`https://graph.qq.com/oauth2.0/me?access_token=${token}`).then(res => res.text()).then(text => { 29 | const matched = text.match(/\"openid\"\:\"([^\"]+)"/); 30 | if (!matched) { 31 | return self.login(); 32 | } 33 | return { 34 | token, 35 | openid: matched[1] 36 | }; 37 | }) 38 | }); 39 | } 40 | 41 | async getUserInfo() { 42 | const data = await this.getToken(); 43 | if (!data) { 44 | return {}; 45 | } 46 | 47 | const userinfo = await this.fetch(`https://graph.qq.com/user/get_user_info?access_token=${data.token}&oauth_consumer_key=${this.config.appid}&openid=${data.openid}`).then(res => res.json()); 48 | return { 49 | uid: data.openid, 50 | name: userinfo.nickname || 'qq用户', 51 | info: { 52 | avatar: userinfo.figureurl_qq_2 53 | }, 54 | type: 'qq' 55 | }; 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | 3 | on: 4 | push: 5 | tags: 6 | - v** 7 | env: 8 | REGISTRY: ghcr.io 9 | DOMAIN: web-oauth-app.xuexb.com 10 | IMAGE_NAME: ${{ github.repository }} 11 | 12 | jobs: 13 | build-and-push-image: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v3 18 | 19 | - name: Login to GitHub Container Registry 20 | uses: docker/login-action@v2 21 | with: 22 | registry: ${{ env.REGISTRY }} 23 | username: ${{ secrets.PERSONAL_ACCESS_RW_NAME }} 24 | password: ${{ secrets.PERSONAL_ACCESS_RW_TOKEN }} 25 | 26 | - name: Extract metadata (tags, labels) for Docker 27 | id: meta 28 | uses: docker/metadata-action@v4 29 | with: 30 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 31 | tags: type=semver,pattern={{version}} 32 | 33 | - name: Build and push Docker image 34 | uses: docker/build-push-action@v4 35 | with: 36 | context: . 37 | push: true 38 | tags: ${{ steps.meta.outputs.tags }} 39 | labels: ${{ steps.meta.outputs.labels }} 40 | - name: deploy 41 | uses: xuexb/dyups-actions/deploy@master 42 | with: 43 | ssh-host: ${{ secrets.SSH_HOST }} 44 | ssh-username: ${{ secrets.SSH_USERNAME }} 45 | ssh-key: ${{ secrets.SSH_KEY }} 46 | ssh-port: ${{ secrets.SSH_PORT }} 47 | image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} 48 | dyups-server: ${{ secrets.DYUPS_SERVER }} 49 | dyups-token: ${{ secrets.DYUPS_TOKEN }} 50 | domain: ${{ env.DOMAIN }} 51 | env-list: | 52 | MYSQL_USER=${{ secrets.MYSQL_USER }} 53 | MYSQL_PASSWORD=${{ secrets.MYSQL_PASSWORD }} 54 | MYSQL_DATABASE=${{ secrets.MYSQL_DATABASE }} 55 | MYSQL_HOST=${{ secrets.MYSQL_HOST }} 56 | MYSQL_PORT=${{ secrets.MYSQL_PORT }} 57 | OAUTH_GITHUB_CLIENT_ID=${{ secrets.OAUTH_GITHUB_CLIENT_ID }} 58 | OAUTH_GITHUB_CLIENT_SECRET=${{ secrets.OAUTH_GITHUB_CLIENT_SECRET }} 59 | OAUTH_QQ_APPID=${{ secrets.OAUTH_QQ_APPID }} 60 | OAUTH_QQ_APPKEY=${{ secrets.OAUTH_QQ_APPKEY }} 61 | OAUTH_QQ_CALLBACK=${{ secrets.OAUTH_QQ_CALLBACK }} 62 | OAUTH_WEIBO_APPKEY=${{ secrets.OAUTH_WEIBO_APPKEY }} 63 | OAUTH_WEIBO_APPSECRET=${{ secrets.OAUTH_WEIBO_APPSECRET }} 64 | OAUTH_WEIBO_CALLBACK=${{ secrets.OAUTH_WEIBO_CALLBACK }} 65 | OAUTH_BAIDU_APPKEY=${{ secrets.OAUTH_BAIDU_APPKEY }} 66 | OAUTH_BAIDU_SECRETKEY=${{ secrets.OAUTH_BAIDU_SECRETKEY }} 67 | OAUTH_BAIDU_CALLBACK=${{ secrets.OAUTH_BAIDU_CALLBACK }} 68 | -------------------------------------------------------------------------------- /src/logic/user.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base.js'); 2 | 3 | module.exports = class extends Base { 4 | regAction() { 5 | if (this.isPost) { 6 | const rules = { 7 | username: { 8 | required: true, 9 | length: { 10 | min: 5, 11 | max: 16 12 | } 13 | }, 14 | password: { 15 | required: true, 16 | length: { 17 | min: 5, 18 | max: 16 19 | } 20 | }, 21 | password2: { 22 | equals: 'password' 23 | } 24 | }; 25 | 26 | if (!this.validate(rules)) { 27 | return this.showMsg(); 28 | } 29 | } 30 | } 31 | 32 | changepasswordAction() { 33 | if (this.isPost) { 34 | const rules = { 35 | password: { 36 | required: true, 37 | length: { 38 | min: 5, 39 | max: 16 40 | } 41 | }, 42 | newpassword: { 43 | required: true, 44 | length: { 45 | min: 5, 46 | max: 16 47 | } 48 | }, 49 | newpassword2: { 50 | equals: 'newpassword' 51 | } 52 | }; 53 | 54 | if (!this.validate(rules)) { 55 | return this.showMsg(); 56 | } 57 | } 58 | } 59 | 60 | oauthLoginAction() { 61 | const rules = { 62 | type: { 63 | required: true, 64 | in: ['github', 'qq', 'weibo', 'baidu'] 65 | } 66 | }; 67 | 68 | this.allowMethods = 'get'; 69 | 70 | if (!this.validate(rules)) { 71 | return this.showMsg(this.config('pkg.prefix')); 72 | } 73 | } 74 | 75 | oauthDeleteAction() { 76 | return this.oauthLoginAction(); 77 | } 78 | 79 | oauthCallbackAction() { 80 | const rules = { 81 | type: { 82 | required: true, 83 | in: ['github', 'qq', 'weibo', 'baidu'] 84 | }, 85 | code: { 86 | requiredIf: ['type', 'github', 'qq', 'weibo', 'baidu'], 87 | method: 'GET' 88 | } 89 | }; 90 | 91 | this.allowMethods = 'get'; 92 | 93 | if (!this.validate(rules)) { 94 | return this.showMsg(this.config('pkg.prefix')); 95 | } 96 | } 97 | 98 | loginAction() { 99 | if (this.isPost) { 100 | const rules = { 101 | username: { 102 | required: true, 103 | length: { 104 | min: 5, 105 | max: 16 106 | } 107 | }, 108 | password: { 109 | required: true, 110 | length: { 111 | min: 5, 112 | max: 16 113 | } 114 | } 115 | }; 116 | 117 | if (!this.validate(rules)) { 118 | return this.showMsg(); 119 | } 120 | } 121 | } 122 | }; 123 | -------------------------------------------------------------------------------- /src/config/adapter.js: -------------------------------------------------------------------------------- 1 | const fileCache = require('think-cache-file'); 2 | const nunjucks = require('think-view-nunjucks'); 3 | const fileSession = require('think-session-file'); 4 | const {Console, DateFile} = require('think-logger3'); 5 | const mysql = require('think-model-mysql'); 6 | const path = require('path'); 7 | const isDev = think.env === 'development'; 8 | 9 | /** 10 | * cache adapter config 11 | * @type {Object} 12 | */ 13 | exports.cache = { 14 | type: 'file', 15 | common: { 16 | timeout: 24 * 60 * 60 * 1000 // millisecond 17 | }, 18 | file: { 19 | handle: fileCache, 20 | cachePath: path.join(think.ROOT_PATH, 'runtime/cache'), // absoulte path is necessarily required 21 | pathDepth: 1, 22 | gcInterval: 24 * 60 * 60 * 1000 // gc interval 23 | } 24 | }; 25 | 26 | /** 27 | * session adapter config 28 | * @type {Object} 29 | */ 30 | exports.session = { 31 | type: 'file', 32 | common: { 33 | cookie: { 34 | name: 'thinkjs' 35 | // keys: ['werwer', 'werwer'], 36 | // signed: true 37 | } 38 | }, 39 | file: { 40 | handle: fileSession, 41 | sessionPath: path.join(think.ROOT_PATH, 'runtime/session') 42 | } 43 | }; 44 | 45 | /** 46 | * view adapter config 47 | * @type {Object} 48 | */ 49 | exports.view = { 50 | type: 'nunjucks', 51 | common: { 52 | viewPath: path.join(think.ROOT_PATH, 'view'), 53 | sep: '_', 54 | extname: '.html' 55 | }, 56 | nunjucks: { 57 | handle: nunjucks, 58 | options: { 59 | lstripBlocks: true, 60 | trimBlocks: true 61 | }, 62 | beforeRender(env, nunjucks, config) { 63 | env.addFilter('format', (unix = Date.now(), str = 'yyyy-MM-dd HH:mm') => { 64 | const date = new Date(parseInt(unix, 10)); 65 | const getTime = { 66 | 'M+': date.getMonth() + 1, 67 | 'd+': date.getDate(), 68 | 'h+': date.getHours() % 12 === 0 ? 12 : date.getHours() % 12, 69 | 'H+': date.getHours(), 70 | 'm+': date.getMinutes(), 71 | 's+': date.getSeconds() 72 | }; 73 | 74 | // 如果有年 75 | if (/(y+)/i.test(str)) { 76 | str = str.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length)); 77 | } 78 | 79 | Object.keys(getTime).forEach(key => { 80 | if (new RegExp('(' + key + ')').test(str)) { 81 | str = str.replace(RegExp.$1, (RegExp.$1.length === 1) ? (getTime[key]) : (('00' + getTime[key]).substr(('' + getTime[key]).length))); 82 | } 83 | }); 84 | 85 | return str; 86 | }); 87 | } 88 | } 89 | }; 90 | 91 | /** 92 | * logger adapter config 93 | * @type {Object} 94 | */ 95 | exports.logger = { 96 | type: isDev ? 'console' : 'dateFile', 97 | console: { 98 | handle: Console 99 | }, 100 | dateFile: { 101 | handle: DateFile, 102 | level: 'ALL', 103 | absolute: true, 104 | 105 | // 如果是 Docker 运行,则配合 Dockerfile 输出日志 106 | pattern: process.env.DOCKER ? '' : '-yyyy-MM-dd', 107 | alwaysIncludePattern: process.env.DOCKER ? false : true, 108 | filename: process.env.DOCKER ? '/var/log/node.log' : path.join(think.ROOT_PATH, 'logs/app.log') 109 | } 110 | }; 111 | 112 | // 数据库配置 113 | exports.model = { 114 | type: 'mysql', // 默认使用的类型,调用时可以指定参数切换 115 | common: { // 通用配置 116 | logConnect: true, // 是否打印数据库连接信息 117 | logSql: true, // 是否打印 SQL 语句 118 | logger: msg => think.logger.info(msg) // 打印信息的 logger 119 | }, 120 | mysql: { 121 | handle: mysql, // Adapter handle 122 | user: process.env.MYSQL_USER || 'root', // 用户名 123 | password: process.env.MYSQL_PASSWORD || '', // 密码 124 | database: process.env.MYSQL_DATABASE || '', // 数据库 125 | host: process.env.MYSQL_HOST || '127.0.0.1', // host 126 | port: process.env.MYSQL_PORT || 3306, // 端口 127 | connectionLimit: 1, // 连接池的连接个数,默认为 1 128 | prefix: process.env.MYSQL_PREFIX || '', // 数据表前缀,如果一个数据库里有多个项目,那项目之间的数据表可以通过前缀来区分 129 | acquireWaitTimeout: 0 // 等待连接的超时时间,避免获取不到连接一直卡在那里,开发环境下有用 130 | } 131 | }; 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 第三方登录服务 Web OAuth 示例 2 | 3 | ## 声明 4 | 5 | 该项目只是用户走通整个第三方的流程,并写出对应的思路,代码不提供参考价值。 6 | 7 | ## 使用技术 8 | 9 | - 后端基于 [Node.js v7.8+](http://nodejs.org/) + [ThinkJS v3](http://thinkjs.org/) 10 | - 数据库基于 MySQL 11 | - 前端样式基于 [Bootstrap v4](https://v4.bootcss.com/) 12 | - 代码托管于 [GitHub@xuexb/web-oauth-app](https://github.com/xuexb/web-oauth-app) 13 | - 示例链接 14 | 15 | ## 数据库 16 | 17 | > 请注意修改 `src/config/adapter.js` 中 MySQL 数据库配置。 18 | 19 | ```sql 20 | SET NAMES utf8mb4; 21 | SET FOREIGN_KEY_CHECKS = 0; 22 | 23 | -- ---------------------------- 24 | -- Table structure for `oauth` 25 | -- ---------------------------- 26 | DROP TABLE IF EXISTS `oauth`; 27 | CREATE TABLE `oauth` ( 28 | `id` int(11) NOT NULL AUTO_INCREMENT, 29 | `type` char(50) NOT NULL COMMENT '类型,有 qq、github、weibo', 30 | `uid` varchar(255) NOT NULL COMMENT '唯一标识', 31 | `info` varchar(255) DEFAULT '' COMMENT '其他信息,JSON 形式', 32 | `user_id` int(11) NOT NULL COMMENT '用户ID', 33 | `create_time` bigint(13) NOT NULL COMMENT '创建时间', 34 | `name` varchar(255) DEFAULT NULL COMMENT '显示名称', 35 | PRIMARY KEY (`id`) 36 | ) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4; 37 | 38 | -- ---------------------------- 39 | -- Table structure for `user` 40 | -- ---------------------------- 41 | DROP TABLE IF EXISTS `user`; 42 | CREATE TABLE `user` ( 43 | `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID', 44 | `username` varchar(255) NOT NULL COMMENT '用户名', 45 | `password` varchar(255) NOT NULL COMMENT '密码', 46 | `create_time` bigint(13) NOT NULL COMMENT '创建时间', 47 | `update_time` bigint(13) NOT NULL COMMENT '更新时间', 48 | PRIMARY KEY (`id`) 49 | ) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4; 50 | 51 | SET FOREIGN_KEY_CHECKS = 1; 52 | ``` 53 | 54 | ## 第三方登录说明 55 | 56 | - 每个用户必须拥有自己的用户名和密码。 57 | - 每个用户可以绑定不同的第三方帐户系统。 58 | - 用户可以对第三方绑定进行管理。 59 | - 用户可以通过已绑定的任意第三方帐户系统进行登录。 60 | - 用户授权表中需要存放第三方系统的唯一标识、显示名称,唯一标识用来和用户、第三方系统进行关联。 61 | - 基于 oAuth2.0 进行授权认证。 62 | - 第三方登录回调成功后,判断当前是否登录: 63 | - 已登录,刷新绑定信息。 64 | - 未登录,记录授权信息,在登录、注册成功后绑定信息。 65 | 66 | 当然你可以根据自己实际项目需求修改,比如: 67 | 68 | - 第三方登录后判断当前是否已登录,如果已登录且绑定的对应平台不是当前帐号,则提示是否更新绑定,或者登录失败 69 | - 第三方登录后判断是否绑定过,如果没有绑定过则自动生成一个用户自动绑定 70 | - 一个帐户只允许绑定一个第三方平台 71 | - 等等 72 | 73 | ### GitHub 74 | 75 | 1. 跳转到授权页 `https://github.com/login/oauth/authorize?` 76 | 1. 认证通过后自动跳转到回调地址,并携带 `code` 77 | 2. 使用 `code` 请求 `https://github.com/login/oauth/access_token` 来获取 `access_token` ,有个小坑是,在想使用 JSON 返回值时,需要在请求头里添加 `'Accept': 'application/json'` 78 | 3. 使用 `access_token` 请求 `https://api.github.com/` 获取用户信息: 79 | - `id` - 唯一标识 80 | - `name` - 显示名称 81 | - `avatar_url` - 用户头像 82 | 83 | 参考链接: 84 | 85 | ### QQ 86 | 87 | 1. 跳转到授权页 `https://graph.qq.com/oauth2.0/show?which=Login&display=` ,需要区分下 PC 端和移动端传,参数 `display` 不一样,需要单独处理下 88 | 1. 认证通过后自动跳转到参数 `redirect_uri` 中,并携带 `code` 89 | 2. 使用 `code` 请求 `https://graph.qq.com/oauth2.0/token?` 获取 `access_token` ,有个大坑是成功时返回 `access_token=xxx` ,错误时返回 `callback( {code: xxx} )` ,好尴尬。。。 90 | 3. 使用 `access_token` 请求 `https://graph.qq.com/oauth2.0/me?` 获取 `openid` ,而这里又是返回个 `callback({"openid": "1"})` 91 | 4. 使用 `access_token` 和 `openid` 请求 `https://graph.qq.com/user/get_user_info` 来获取用户信息,最终为: 92 | - `openid` - 唯一标识 93 | - `nickname` - 显示名称 94 | - `figureurl_qq_2` - 用户头像 95 | 96 | 参数链接:[http://wiki.connect.qq.com/开发攻略_server-side](http://wiki.connect.qq.com/%E5%BC%80%E5%8F%91%E6%94%BB%E7%95%A5_server-side) 97 | 98 | ### 微博 99 | 100 | 1. 跳转到授权页 `https://api.weibo.com/oauth2/authorize` 101 | 1. 认证通过后自动跳转到参数 `redirect_uri` 中,并携带 `code` 102 | 2. 使用 `code` 请求 `https://api.weibo.com/oauth2/access_token?` 获取 `access_token` 103 | 3. 使用 `access_token` 请求 `https://api.weibo.com/2/users/show.json` 获取用户信息,最终为: 104 | - `access_token` - 唯一标识 105 | - `screen_name` - 显示名称 106 | - `avatar_hd` - 用户头像 107 | 108 | 参考链接:[http://open.weibo.com/wiki/授权机制说明](http://open.weibo.com/wiki/%E6%8E%88%E6%9D%83%E6%9C%BA%E5%88%B6%E8%AF%B4%E6%98%8E) 109 | 110 | ### 百度 111 | 112 | 1. 跳转到授权页面 `http://openapi.baidu.com/oauth/2.0/authorize?` ,需要区分下 PC 端和移动端传,参数 `display` 不一样,需要单独处理下 113 | 2. 认证通过后自动跳转到参数 `redirect_uri` 中,并携带 `code` 114 | 1. 使用 `code` 请求 `https://openapi.baidu.com/oauth/2.0/token` 获取 `access_token` 115 | 2. 使用 `access_token` 请求 `https://openapi.baidu.com/rest/2.0/passport/users/getInfo` 来获取用户信息,最终为: 116 | - `userid` - 唯一标识 117 | - `username` - 显示名称 118 | - `http://tb.himg.baidu.com/sys/portrait/item/${userinfo.portrait}` - 用户头像 119 | 120 | 参考链接: 121 | 122 | ## 隐私声明 123 | 124 | - 本项目只是演示示例,登录、注册的密码通过 MD5 加密后存储于 MySQL 中。 125 | - 第三方登录相关信息不会对外暴露。 126 | - 所有数据不定期的进行删除。 127 | 128 | ## 本地开发 129 | 130 | ```bash 131 | # 导入 MySQL 数据 132 | ... 133 | 134 | # 申请第三方开发平台 135 | ... 136 | 137 | # 克隆代码 138 | git clone https://github.com/xuexb/web-oauth-app.git && cd web-oauth-app 139 | 140 | # 修改数据库配置 141 | vi src/config/adapter.js 142 | 143 | # 修改配置信息 144 | vi src/config/config.js 145 | 146 | # 安装依赖 147 | yarn install 148 | 149 | # 本地开发 150 | yarn start 151 | ``` 152 | 153 | ### Docker 运行 154 | 155 | ```bash 156 | # 构建 157 | docker build --no-cache -t demo/web-oauth-app:latest . 158 | 159 | # 命令式运行 160 | docker run \ 161 | -p 8080:8080 \ 162 | -d \ 163 | --name web-oauth-app \ 164 | --env MYSQL_USER=root \ 165 | --env MYSQL_PASSWORD=123456 \ 166 | --env MYSQL_DATABASE=app \ 167 | --env MYSQL_HOST=locaclhost \ 168 | --env MYSQL_PORT=3306 \ 169 | --env OAUTH_GITHUB_CLIENT_ID= \ 170 | --env OAUTH_... \ 171 | demo/web-oauth-app:latest 172 | 173 | # 配置文件式运行 174 | docker run \ 175 | -p 8080:8080 \ 176 | -d \ 177 | --name web-oauth-app \ 178 | -v "$(pwd)/config.js":/usr/src/app/src/config/config.js \ 179 | -v "$(pwd)/adapter.js":/usr/src/app/src/config/adapter.js \ 180 | demo/web-oauth-app:latest 181 | ``` 182 | 183 | ## License 184 | MIT 185 | -------------------------------------------------------------------------------- /src/controller/user.js: -------------------------------------------------------------------------------- 1 | const Base = require('./base.js'); 2 | 3 | module.exports = class extends Base { 4 | /** 5 | * 前置处理用户信息验证、注入第三方登录信息 6 | */ 7 | async __before() { 8 | await super.__before(); 9 | 10 | if (['login', 'reg', 'exit', 'oauthLogin', 'oauthCallback'].indexOf(this.ctx.action) == -1 && !this.isLogin) { 11 | return this.showMsg('请先登录', `${this.config('pkg.prefix')}/user/login`); 12 | } 13 | 14 | if (['login', 'reg'].indexOf(this.ctx.action) > -1 && this.cookie('oauth')) { 15 | const oauth = JSON.parse(decodeURIComponent(this.cookie('oauth'))); 16 | this.assign('oauth', oauth); 17 | this.oauth = oauth; 18 | } 19 | } 20 | 21 | /** 22 | * 授权回调 23 | * 24 | * @return {Object} 25 | */ 26 | async oauthCallbackAction() { 27 | const type = this.get('type'); 28 | const service = think.service(type, this.ctx, this.config(`oauth.${type}`)); 29 | 30 | try { 31 | // 获取第三方标识 32 | const userinfo = await service.getUserInfo(); 33 | const oauth = await this.model('oauth').where({uid: userinfo.uid, type}).find(); 34 | 35 | // 如果当前是登录状态,则判断第三方有没有绑定过其他帐户 36 | if (this.userinfo) { 37 | if (!think.isEmpty(oauth) && this.userinfo.id === oauth.user_id) { 38 | return this.showMsg('已经绑定通过', `${this.config('pkg.prefix')}/user/oauth`); 39 | } else if (!think.isEmpty(oauth)) { 40 | return this.showMsg(`该${type}已经绑定其他帐号`, `${this.config('pkg.prefix')}/user/oauth`); 41 | } else { 42 | await this.model('oauth').add({ 43 | type: type, 44 | name: userinfo.name, 45 | uid: userinfo.uid, 46 | info: JSON.stringify(userinfo.info), 47 | user_id: this.userinfo.id, 48 | create_time: Date.now() 49 | }); 50 | return this.showMsg(`绑定成功`, `${this.config('pkg.prefix')}/user/oauth`); 51 | } 52 | } 53 | 54 | // 已经绑定 55 | if (!think.isEmpty(oauth)) { 56 | const user = await this.model('user').where({id: oauth.user_id}).find(); 57 | if (!think.isEmpty(user)) { 58 | // 写入 session 59 | delete user.password; 60 | await this.session('userinfo', user); 61 | return this.showMsg(`登录成功,欢迎通过${type}登录!`, `${this.config('pkg.prefix')}/user`); 62 | } else { 63 | // 用户都不存在了,授权数据可以删了。这可能就是传说中的人一走,茶就凉吧。。。 64 | await this.model('oauth').where({uid: userinfo.uid, type}).delete(); 65 | } 66 | } 67 | 68 | // 记录当前第三方信息,用来登录、注册后绑定 69 | this.cookie('oauth', encodeURIComponent(JSON.stringify(userinfo))); 70 | return this.redirect(`${this.config('pkg.prefix')}/user/login`); 71 | } catch (e) { 72 | console.error(e) 73 | return this.showMsg(`${type} 登录失败`, this.userinfo ? `${this.config('pkg.prefix')}/user/oauth` : `${this.config('pkg.prefix')}/user/login`); 74 | } 75 | } 76 | 77 | /** 78 | * 第三方登录 79 | * 80 | * @param {string} type 登录类型 81 | * @return {Object} 82 | */ 83 | async oauthLoginAction() { 84 | const type = this.get('type'); 85 | return think.service(type, this.ctx, this.config(`oauth.${type}`)).login(); 86 | } 87 | 88 | /** 89 | * 登录 90 | * 91 | * @param {string} username 用户名 92 | * @param {string} password 密码 93 | * @return {Object} 94 | */ 95 | async loginAction() { 96 | if (this.isGet) { 97 | return this.display(); 98 | } 99 | 100 | const {username, password} = this.post(); 101 | const first = await this.model('user').where({ 102 | username 103 | }).find(); 104 | 105 | if (think.isEmpty(first)) { 106 | return this.showMsg('用户名不存在'); 107 | } 108 | 109 | const user = await this.model('user').where({username, password: think.md5(password)}).find(); 110 | if (think.isEmpty(user)) { 111 | return this.showMsg('用户名或者密码错误'); 112 | } 113 | 114 | // 判断绑定第三方登录 115 | if (this.oauth) { 116 | const oauth = await this.model('oauth').where({type: this.oauth.type, user_id: user.id}).find(); 117 | // 已经绑定其他帐号 118 | if (!think.isEmpty(oauth)) { 119 | return this.showMsg(`该用户名已经绑定其他${this.oauth.type}帐号`); 120 | } 121 | 122 | await this.model('oauth').add({ 123 | type: this.oauth.type, 124 | name: this.oauth.name, 125 | uid: this.oauth.uid, 126 | info: JSON.stringify(this.oauth.info), 127 | user_id: user.id, 128 | create_time: Date.now() 129 | }); 130 | this.cookie('oauth', null); 131 | 132 | // 更新登录时间 133 | await this.model('user').where({ 134 | id: user.id 135 | }).update({ 136 | update_time: Date.now() 137 | }); 138 | 139 | // 写入 session 140 | await this.writeUserinfo(user.id); 141 | 142 | return this.showMsg('绑定登录成功', `${this.config('pkg.prefix')}/user`); 143 | } 144 | 145 | // 更新登录时间 146 | await this.model('user').where({ 147 | id: user.id 148 | }).update({ 149 | update_time: Date.now() 150 | }); 151 | 152 | // 写入 session 153 | await this.writeUserinfo(user.id); 154 | 155 | return this.showMsg('登录成功', `${this.config('pkg.prefix')}/user`); 156 | } 157 | 158 | /** 159 | * 注册 160 | * 161 | * @return {Object} 162 | */ 163 | async regAction() { 164 | if (this.isGet) { 165 | return this.display(); 166 | } 167 | 168 | const {username, password} = this.post(); 169 | const user = await this.model('user').where({username}).thenAdd({ 170 | username, 171 | password: think.md5(password), 172 | create_time: Date.now(), 173 | update_time: Date.now() 174 | }); 175 | 176 | // 写入 session 177 | await this.writeUserinfo(user.id); 178 | 179 | // 如果有第三方登录信息则关联 180 | if (this.oauth) { 181 | await this.model('oauth').add({ 182 | type: this.oauth.type, 183 | name: this.oauth.name, 184 | uid: this.oauth.uid, 185 | info: JSON.stringify(this.oauth.info), 186 | user_id: user.id, 187 | create_time: Date.now() 188 | }); 189 | this.cookie('oauth', null); 190 | } 191 | 192 | return this.showMsg(this.oauth ? '绑定注册成功' : '注册成功',`${this.config('pkg.prefix')}/user`); 193 | } 194 | 195 | /** 196 | * 退出 197 | * 198 | * @param {string} [ref=/] 退出成功后跳转链接 199 | * @return {Ojbect} 200 | */ 201 | async exitAction() { 202 | await this.session('userinfo', null); 203 | this.cookie('oauth', null); 204 | return this.redirect(this.get('ref') || this.config('pkg.prefix')); 205 | } 206 | 207 | /** 208 | * 个人中心 209 | * 210 | * @return {Object} 211 | */ 212 | async indexAction() { 213 | return this.display(); 214 | } 215 | 216 | /** 217 | * 修改密码 218 | * 219 | * @return {Object} 220 | */ 221 | async changepasswordAction() { 222 | if (this.isGet) { 223 | return this.display(); 224 | } 225 | 226 | await this.model('user').where({id: this.userinfo.id}).update({ 227 | password: think.md5(this.get('newpassword')) 228 | }); 229 | 230 | return this.showMsg('更新成功'); 231 | } 232 | 233 | /** 234 | * 销毁账户 235 | * 236 | * @return {Object} 237 | */ 238 | async destroyAction() { 239 | await this.model('user').where({id: this.userinfo.id}).delete(); 240 | await this.model('oauth').where({user_id: this.userinfo.id}).delete(); 241 | return this.exitAction(); 242 | } 243 | 244 | /** 245 | * 第三方授权管理 246 | * 247 | * @return {Object} 248 | */ 249 | async oauthAction() { 250 | const oauth = Object.keys(this.config('oauth')); 251 | const items = []; 252 | 253 | for (const type of oauth) { 254 | const result = await this.model('oauth').where({ 255 | user_id: this.userinfo.id, 256 | type 257 | }).find(); 258 | if (!think.isEmpty(result)) { 259 | items.push(result); 260 | } else { 261 | items.push({ 262 | type 263 | }); 264 | } 265 | } 266 | 267 | this.assign('items', items); 268 | return this.display(); 269 | } 270 | 271 | /** 272 | * 删除第三方登录授权 273 | * 274 | * @param {string} type 类型 275 | * @return {Object} 276 | */ 277 | async oauthDeleteAction() { 278 | const type = this.get('type'); 279 | await this.model('oauth').where({ 280 | user_id: this.userinfo.id, 281 | type 282 | }).delete(); 283 | 284 | return this.showMsg(`删除${type}授权成功`, `${this.config('pkg.prefix')}/user/oauth`); 285 | } 286 | }; 287 | -------------------------------------------------------------------------------- /www/static/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v4.6.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap={},t.jQuery,t.Popper)}(this,(function(t,e,n){"use strict";function i(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var o=i(e),a=i(n);function s(t,e){for(var n=0;n=4)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}};d.jQueryDetection(),o.default.fn.emulateTransitionEnd=function(t){var e=this,n=!1;return o.default(this).one(d.TRANSITION_END,(function(){n=!0})),setTimeout((function(){n||d.triggerTransitionEnd(e)}),t),this},o.default.event.special[d.TRANSITION_END]={bindType:f,delegateType:f,handle:function(t){if(o.default(t.target).is(this))return t.handleObj.handler.apply(this,arguments)}};var c="bs.alert",h=o.default.fn.alert,g=function(){function t(t){this._element=t}var e=t.prototype;return e.close=function(t){var e=this._element;t&&(e=this._getRootElement(t)),this._triggerCloseEvent(e).isDefaultPrevented()||this._removeElement(e)},e.dispose=function(){o.default.removeData(this._element,c),this._element=null},e._getRootElement=function(t){var e=d.getSelectorFromElement(t),n=!1;return e&&(n=document.querySelector(e)),n||(n=o.default(t).closest(".alert")[0]),n},e._triggerCloseEvent=function(t){var e=o.default.Event("close.bs.alert");return o.default(t).trigger(e),e},e._removeElement=function(t){var e=this;if(o.default(t).removeClass("show"),o.default(t).hasClass("fade")){var n=d.getTransitionDurationFromElement(t);o.default(t).one(d.TRANSITION_END,(function(n){return e._destroyElement(t,n)})).emulateTransitionEnd(n)}else this._destroyElement(t)},e._destroyElement=function(t){o.default(t).detach().trigger("closed.bs.alert").remove()},t._jQueryInterface=function(e){return this.each((function(){var n=o.default(this),i=n.data(c);i||(i=new t(this),n.data(c,i)),"close"===e&&i[e](this)}))},t._handleDismiss=function(t){return function(e){e&&e.preventDefault(),t.close(this)}},l(t,null,[{key:"VERSION",get:function(){return"4.6.1"}}]),t}();o.default(document).on("click.bs.alert.data-api",'[data-dismiss="alert"]',g._handleDismiss(new g)),o.default.fn.alert=g._jQueryInterface,o.default.fn.alert.Constructor=g,o.default.fn.alert.noConflict=function(){return o.default.fn.alert=h,g._jQueryInterface};var m="bs.button",p=o.default.fn.button,_="active",v='[data-toggle^="button"]',y='input:not([type="hidden"])',b=".btn",E=function(){function t(t){this._element=t,this.shouldAvoidTriggerChange=!1}var e=t.prototype;return e.toggle=function(){var t=!0,e=!0,n=o.default(this._element).closest('[data-toggle="buttons"]')[0];if(n){var i=this._element.querySelector(y);if(i){if("radio"===i.type)if(i.checked&&this._element.classList.contains(_))t=!1;else{var a=n.querySelector(".active");a&&o.default(a).removeClass(_)}t&&("checkbox"!==i.type&&"radio"!==i.type||(i.checked=!this._element.classList.contains(_)),this.shouldAvoidTriggerChange||o.default(i).trigger("change")),i.focus(),e=!1}}this._element.hasAttribute("disabled")||this._element.classList.contains("disabled")||(e&&this._element.setAttribute("aria-pressed",!this._element.classList.contains(_)),t&&o.default(this._element).toggleClass(_))},e.dispose=function(){o.default.removeData(this._element,m),this._element=null},t._jQueryInterface=function(e,n){return this.each((function(){var i=o.default(this),a=i.data(m);a||(a=new t(this),i.data(m,a)),a.shouldAvoidTriggerChange=n,"toggle"===e&&a[e]()}))},l(t,null,[{key:"VERSION",get:function(){return"4.6.1"}}]),t}();o.default(document).on("click.bs.button.data-api",v,(function(t){var e=t.target,n=e;if(o.default(e).hasClass("btn")||(e=o.default(e).closest(b)[0]),!e||e.hasAttribute("disabled")||e.classList.contains("disabled"))t.preventDefault();else{var i=e.querySelector(y);if(i&&(i.hasAttribute("disabled")||i.classList.contains("disabled")))return void t.preventDefault();"INPUT"!==n.tagName&&"LABEL"===e.tagName||E._jQueryInterface.call(o.default(e),"toggle","INPUT"===n.tagName)}})).on("focus.bs.button.data-api blur.bs.button.data-api",v,(function(t){var e=o.default(t.target).closest(b)[0];o.default(e).toggleClass("focus",/^focus(in)?$/.test(t.type))})),o.default(window).on("load.bs.button.data-api",(function(){for(var t=[].slice.call(document.querySelectorAll('[data-toggle="buttons"] .btn')),e=0,n=t.length;e0,this._pointerEvent=Boolean(window.PointerEvent||window.MSPointerEvent),this._addEventListeners()}var e=t.prototype;return e.next=function(){this._isSliding||this._slide(N)},e.nextWhenVisible=function(){var t=o.default(this._element);!document.hidden&&t.is(":visible")&&"hidden"!==t.css("visibility")&&this.next()},e.prev=function(){this._isSliding||this._slide(D)},e.pause=function(t){t||(this._isPaused=!0),this._element.querySelector(".carousel-item-next, .carousel-item-prev")&&(d.triggerTransitionEnd(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},e.cycle=function(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},e.to=function(t){var e=this;this._activeElement=this._element.querySelector(I);var n=this._getItemIndex(this._activeElement);if(!(t>this._items.length-1||t<0))if(this._isSliding)o.default(this._element).one(A,(function(){return e.to(t)}));else{if(n===t)return this.pause(),void this.cycle();var i=t>n?N:D;this._slide(i,this._items[t])}},e.dispose=function(){o.default(this._element).off(".bs.carousel"),o.default.removeData(this._element,w),this._items=null,this._config=null,this._element=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},e._getConfig=function(t){return t=r({},k,t),d.typeCheckConfig(T,t,O),t},e._handleSwipe=function(){var t=Math.abs(this.touchDeltaX);if(!(t<=40)){var e=t/this.touchDeltaX;this.touchDeltaX=0,e>0&&this.prev(),e<0&&this.next()}},e._addEventListeners=function(){var t=this;this._config.keyboard&&o.default(this._element).on("keydown.bs.carousel",(function(e){return t._keydown(e)})),"hover"===this._config.pause&&o.default(this._element).on("mouseenter.bs.carousel",(function(e){return t.pause(e)})).on("mouseleave.bs.carousel",(function(e){return t.cycle(e)})),this._config.touch&&this._addTouchEventListeners()},e._addTouchEventListeners=function(){var t=this;if(this._touchSupported){var e=function(e){t._pointerEvent&&j[e.originalEvent.pointerType.toUpperCase()]?t.touchStartX=e.originalEvent.clientX:t._pointerEvent||(t.touchStartX=e.originalEvent.touches[0].clientX)},n=function(e){t._pointerEvent&&j[e.originalEvent.pointerType.toUpperCase()]&&(t.touchDeltaX=e.originalEvent.clientX-t.touchStartX),t._handleSwipe(),"hover"===t._config.pause&&(t.pause(),t.touchTimeout&&clearTimeout(t.touchTimeout),t.touchTimeout=setTimeout((function(e){return t.cycle(e)}),500+t._config.interval))};o.default(this._element.querySelectorAll(".carousel-item img")).on("dragstart.bs.carousel",(function(t){return t.preventDefault()})),this._pointerEvent?(o.default(this._element).on("pointerdown.bs.carousel",(function(t){return e(t)})),o.default(this._element).on("pointerup.bs.carousel",(function(t){return n(t)})),this._element.classList.add("pointer-event")):(o.default(this._element).on("touchstart.bs.carousel",(function(t){return e(t)})),o.default(this._element).on("touchmove.bs.carousel",(function(e){return function(e){t.touchDeltaX=e.originalEvent.touches&&e.originalEvent.touches.length>1?0:e.originalEvent.touches[0].clientX-t.touchStartX}(e)})),o.default(this._element).on("touchend.bs.carousel",(function(t){return n(t)})))}},e._keydown=function(t){if(!/input|textarea/i.test(t.target.tagName))switch(t.which){case 37:t.preventDefault(),this.prev();break;case 39:t.preventDefault(),this.next()}},e._getItemIndex=function(t){return this._items=t&&t.parentNode?[].slice.call(t.parentNode.querySelectorAll(".carousel-item")):[],this._items.indexOf(t)},e._getItemByDirection=function(t,e){var n=t===N,i=t===D,o=this._getItemIndex(e),a=this._items.length-1;if((i&&0===o||n&&o===a)&&!this._config.wrap)return e;var s=(o+(t===D?-1:1))%this._items.length;return-1===s?this._items[this._items.length-1]:this._items[s]},e._triggerSlideEvent=function(t,e){var n=this._getItemIndex(t),i=this._getItemIndex(this._element.querySelector(I)),a=o.default.Event("slide.bs.carousel",{relatedTarget:t,direction:e,from:i,to:n});return o.default(this._element).trigger(a),a},e._setActiveIndicatorElement=function(t){if(this._indicatorsElement){var e=[].slice.call(this._indicatorsElement.querySelectorAll(".active"));o.default(e).removeClass(S);var n=this._indicatorsElement.children[this._getItemIndex(t)];n&&o.default(n).addClass(S)}},e._updateInterval=function(){var t=this._activeElement||this._element.querySelector(I);if(t){var e=parseInt(t.getAttribute("data-interval"),10);e?(this._config.defaultInterval=this._config.defaultInterval||this._config.interval,this._config.interval=e):this._config.interval=this._config.defaultInterval||this._config.interval}},e._slide=function(t,e){var n,i,a,s=this,l=this._element.querySelector(I),r=this._getItemIndex(l),u=e||l&&this._getItemByDirection(t,l),f=this._getItemIndex(u),c=Boolean(this._interval);if(t===N?(n="carousel-item-left",i="carousel-item-next",a="left"):(n="carousel-item-right",i="carousel-item-prev",a="right"),u&&o.default(u).hasClass(S))this._isSliding=!1;else if(!this._triggerSlideEvent(u,a).isDefaultPrevented()&&l&&u){this._isSliding=!0,c&&this.pause(),this._setActiveIndicatorElement(u),this._activeElement=u;var h=o.default.Event(A,{relatedTarget:u,direction:a,from:r,to:f});if(o.default(this._element).hasClass("slide")){o.default(u).addClass(i),d.reflow(u),o.default(l).addClass(n),o.default(u).addClass(n);var g=d.getTransitionDurationFromElement(l);o.default(l).one(d.TRANSITION_END,(function(){o.default(u).removeClass(n+" "+i).addClass(S),o.default(l).removeClass("active "+i+" "+n),s._isSliding=!1,setTimeout((function(){return o.default(s._element).trigger(h)}),0)})).emulateTransitionEnd(g)}else o.default(l).removeClass(S),o.default(u).addClass(S),this._isSliding=!1,o.default(this._element).trigger(h);c&&this.cycle()}},t._jQueryInterface=function(e){return this.each((function(){var n=o.default(this).data(w),i=r({},k,o.default(this).data());"object"==typeof e&&(i=r({},i,e));var a="string"==typeof e?e:i.slide;if(n||(n=new t(this,i),o.default(this).data(w,n)),"number"==typeof e)n.to(e);else if("string"==typeof a){if("undefined"==typeof n[a])throw new TypeError('No method named "'+a+'"');n[a]()}else i.interval&&i.ride&&(n.pause(),n.cycle())}))},t._dataApiClickHandler=function(e){var n=d.getSelectorFromElement(this);if(n){var i=o.default(n)[0];if(i&&o.default(i).hasClass("carousel")){var a=r({},o.default(i).data(),o.default(this).data()),s=this.getAttribute("data-slide-to");s&&(a.interval=!1),t._jQueryInterface.call(o.default(i),a),s&&o.default(i).data(w).to(s),e.preventDefault()}}},l(t,null,[{key:"VERSION",get:function(){return"4.6.1"}},{key:"Default",get:function(){return k}}]),t}();o.default(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",P._dataApiClickHandler),o.default(window).on("load.bs.carousel.data-api",(function(){for(var t=[].slice.call(document.querySelectorAll('[data-ride="carousel"]')),e=0,n=t.length;e0&&(this._selector=s,this._triggerArray.push(a))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}var e=t.prototype;return e.toggle=function(){o.default(this._element).hasClass(q)?this.hide():this.show()},e.show=function(){var e,n,i=this;if(!(this._isTransitioning||o.default(this._element).hasClass(q)||(this._parent&&0===(e=[].slice.call(this._parent.querySelectorAll(".show, .collapsing")).filter((function(t){return"string"==typeof i._config.parent?t.getAttribute("data-parent")===i._config.parent:t.classList.contains(F)}))).length&&(e=null),e&&(n=o.default(e).not(this._selector).data(R))&&n._isTransitioning))){var a=o.default.Event("show.bs.collapse");if(o.default(this._element).trigger(a),!a.isDefaultPrevented()){e&&(t._jQueryInterface.call(o.default(e).not(this._selector),"hide"),n||o.default(e).data(R,null));var s=this._getDimension();o.default(this._element).removeClass(F).addClass(Q),this._element.style[s]=0,this._triggerArray.length&&o.default(this._triggerArray).removeClass(B).attr("aria-expanded",!0),this.setTransitioning(!0);var l="scroll"+(s[0].toUpperCase()+s.slice(1)),r=d.getTransitionDurationFromElement(this._element);o.default(this._element).one(d.TRANSITION_END,(function(){o.default(i._element).removeClass(Q).addClass("collapse show"),i._element.style[s]="",i.setTransitioning(!1),o.default(i._element).trigger("shown.bs.collapse")})).emulateTransitionEnd(r),this._element.style[s]=this._element[l]+"px"}}},e.hide=function(){var t=this;if(!this._isTransitioning&&o.default(this._element).hasClass(q)){var e=o.default.Event("hide.bs.collapse");if(o.default(this._element).trigger(e),!e.isDefaultPrevented()){var n=this._getDimension();this._element.style[n]=this._element.getBoundingClientRect()[n]+"px",d.reflow(this._element),o.default(this._element).addClass(Q).removeClass("collapse show");var i=this._triggerArray.length;if(i>0)for(var a=0;a0},e._getOffset=function(){var t=this,e={};return"function"==typeof this._config.offset?e.fn=function(e){return e.offsets=r({},e.offsets,t._config.offset(e.offsets,t._element)),e}:e.offset=this._config.offset,e},e._getPopperConfig=function(){var t={placement:this._getPlacement(),modifiers:{offset:this._getOffset(),flip:{enabled:this._config.flip},preventOverflow:{boundariesElement:this._config.boundary}}};return"static"===this._config.display&&(t.modifiers.applyStyle={enabled:!1}),r({},t,this._config.popperConfig)},t._jQueryInterface=function(e){return this.each((function(){var n=o.default(this).data(K);if(n||(n=new t(this,"object"==typeof e?e:null),o.default(this).data(K,n)),"string"==typeof e){if("undefined"==typeof n[e])throw new TypeError('No method named "'+e+'"');n[e]()}}))},t._clearMenus=function(e){if(!e||3!==e.which&&("keyup"!==e.type||9===e.which))for(var n=[].slice.call(document.querySelectorAll(it)),i=0,a=n.length;i0&&s--,40===e.which&&sdocument.documentElement.clientHeight;n||(this._element.style.overflowY="hidden"),this._element.classList.add(ht);var i=d.getTransitionDurationFromElement(this._dialog);o.default(this._element).off(d.TRANSITION_END),o.default(this._element).one(d.TRANSITION_END,(function(){t._element.classList.remove(ht),n||o.default(t._element).one(d.TRANSITION_END,(function(){t._element.style.overflowY=""})).emulateTransitionEnd(t._element,i)})).emulateTransitionEnd(i),this._element.focus()}},e._showElement=function(t){var e=this,n=o.default(this._element).hasClass(dt),i=this._dialog?this._dialog.querySelector(".modal-body"):null;this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.appendChild(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),o.default(this._dialog).hasClass("modal-dialog-scrollable")&&i?i.scrollTop=0:this._element.scrollTop=0,n&&d.reflow(this._element),o.default(this._element).addClass(ct),this._config.focus&&this._enforceFocus();var a=o.default.Event("shown.bs.modal",{relatedTarget:t}),s=function(){e._config.focus&&e._element.focus(),e._isTransitioning=!1,o.default(e._element).trigger(a)};if(n){var l=d.getTransitionDurationFromElement(this._dialog);o.default(this._dialog).one(d.TRANSITION_END,s).emulateTransitionEnd(l)}else s()},e._enforceFocus=function(){var t=this;o.default(document).off(pt).on(pt,(function(e){document!==e.target&&t._element!==e.target&&0===o.default(t._element).has(e.target).length&&t._element.focus()}))},e._setEscapeEvent=function(){var t=this;this._isShown?o.default(this._element).on(yt,(function(e){t._config.keyboard&&27===e.which?(e.preventDefault(),t.hide()):t._config.keyboard||27!==e.which||t._triggerBackdropTransition()})):this._isShown||o.default(this._element).off(yt)},e._setResizeEvent=function(){var t=this;this._isShown?o.default(window).on(_t,(function(e){return t.handleUpdate(e)})):o.default(window).off(_t)},e._hideModal=function(){var t=this;this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._showBackdrop((function(){o.default(document.body).removeClass(ft),t._resetAdjustments(),t._resetScrollbar(),o.default(t._element).trigger(gt)}))},e._removeBackdrop=function(){this._backdrop&&(o.default(this._backdrop).remove(),this._backdrop=null)},e._showBackdrop=function(t){var e=this,n=o.default(this._element).hasClass(dt)?dt:"";if(this._isShown&&this._config.backdrop){if(this._backdrop=document.createElement("div"),this._backdrop.className="modal-backdrop",n&&this._backdrop.classList.add(n),o.default(this._backdrop).appendTo(document.body),o.default(this._element).on(vt,(function(t){e._ignoreBackdropClick?e._ignoreBackdropClick=!1:t.target===t.currentTarget&&("static"===e._config.backdrop?e._triggerBackdropTransition():e.hide())})),n&&d.reflow(this._backdrop),o.default(this._backdrop).addClass(ct),!t)return;if(!n)return void t();var i=d.getTransitionDurationFromElement(this._backdrop);o.default(this._backdrop).one(d.TRANSITION_END,t).emulateTransitionEnd(i)}else if(!this._isShown&&this._backdrop){o.default(this._backdrop).removeClass(ct);var a=function(){e._removeBackdrop(),t&&t()};if(o.default(this._element).hasClass(dt)){var s=d.getTransitionDurationFromElement(this._backdrop);o.default(this._backdrop).one(d.TRANSITION_END,a).emulateTransitionEnd(s)}else a()}else t&&t()},e._adjustDialog=function(){var t=this._element.scrollHeight>document.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},e._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},e._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=Math.round(t.left+t.right)
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",customClass:"",sanitize:!0,sanitizeFn:null,whiteList:{"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},popperConfig:null},Ut={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(number|string|function)",container:"(string|element|boolean)",fallbackPlacement:"(string|array)",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",whiteList:"object",popperConfig:"(null|object)"},Mt={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"},Wt=function(){function t(t,e){if("undefined"==typeof a.default)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var e=t.prototype;return e.enable=function(){this._isEnabled=!0},e.disable=function(){this._isEnabled=!1},e.toggleEnabled=function(){this._isEnabled=!this._isEnabled},e.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=o.default(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),o.default(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(o.default(this.getTipElement()).hasClass(Rt))return void this._leave(null,this);this._enter(null,this)}},e.dispose=function(){clearTimeout(this._timeout),o.default.removeData(this.element,this.constructor.DATA_KEY),o.default(this.element).off(this.constructor.EVENT_KEY),o.default(this.element).closest(".modal").off("hide.bs.modal",this._hideModalHandler),this.tip&&o.default(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},e.show=function(){var t=this;if("none"===o.default(this.element).css("display"))throw new Error("Please use show on visible elements");var e=o.default.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){o.default(this.element).trigger(e);var n=d.findShadowRoot(this.element),i=o.default.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(e.isDefaultPrevented()||!i)return;var s=this.getTipElement(),l=d.getUID(this.constructor.NAME);s.setAttribute("id",l),this.element.setAttribute("aria-describedby",l),this.setContent(),this.config.animation&&o.default(s).addClass(Lt);var r="function"==typeof this.config.placement?this.config.placement.call(this,s,this.element):this.config.placement,u=this._getAttachment(r);this.addAttachmentClass(u);var f=this._getContainer();o.default(s).data(this.constructor.DATA_KEY,this),o.default.contains(this.element.ownerDocument.documentElement,this.tip)||o.default(s).appendTo(f),o.default(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new a.default(this.element,s,this._getPopperConfig(u)),o.default(s).addClass(Rt),o.default(s).addClass(this.config.customClass),"ontouchstart"in document.documentElement&&o.default(document.body).children().on("mouseover",null,o.default.noop);var c=function(){t.config.animation&&t._fixTransition();var e=t._hoverState;t._hoverState=null,o.default(t.element).trigger(t.constructor.Event.SHOWN),e===qt&&t._leave(null,t)};if(o.default(this.tip).hasClass(Lt)){var h=d.getTransitionDurationFromElement(this.tip);o.default(this.tip).one(d.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},e.hide=function(t){var e=this,n=this.getTipElement(),i=o.default.Event(this.constructor.Event.HIDE),a=function(){e._hoverState!==xt&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),o.default(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(o.default(this.element).trigger(i),!i.isDefaultPrevented()){if(o.default(n).removeClass(Rt),"ontouchstart"in document.documentElement&&o.default(document.body).children().off("mouseover",null,o.default.noop),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,o.default(this.tip).hasClass(Lt)){var s=d.getTransitionDurationFromElement(n);o.default(n).one(d.TRANSITION_END,a).emulateTransitionEnd(s)}else a();this._hoverState=""}},e.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},e.isWithContent=function(){return Boolean(this.getTitle())},e.addAttachmentClass=function(t){o.default(this.getTipElement()).addClass("bs-tooltip-"+t)},e.getTipElement=function(){return this.tip=this.tip||o.default(this.config.template)[0],this.tip},e.setContent=function(){var t=this.getTipElement();this.setElementContent(o.default(t.querySelectorAll(".tooltip-inner")),this.getTitle()),o.default(t).removeClass("fade show")},e.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=At(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?o.default(e).parent().is(t)||t.empty().append(e):t.text(o.default(e).text())},e.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},e._getPopperConfig=function(t){var e=this;return r({},{placement:t,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:".arrow"},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}},this.config.popperConfig)},e._getOffset=function(){var t=this,e={};return"function"==typeof this.config.offset?e.fn=function(e){return e.offsets=r({},e.offsets,t.config.offset(e.offsets,t.element)),e}:e.offset=this.config.offset,e},e._getContainer=function(){return!1===this.config.container?document.body:d.isElement(this.config.container)?o.default(this.config.container):o.default(document).find(this.config.container)},e._getAttachment=function(t){return Bt[t.toUpperCase()]},e._setListeners=function(){var t=this;this.config.trigger.split(" ").forEach((function(e){if("click"===e)o.default(t.element).on(t.constructor.Event.CLICK,t.config.selector,(function(e){return t.toggle(e)}));else if("manual"!==e){var n=e===Ft?t.constructor.Event.MOUSEENTER:t.constructor.Event.FOCUSIN,i=e===Ft?t.constructor.Event.MOUSELEAVE:t.constructor.Event.FOCUSOUT;o.default(t.element).on(n,t.config.selector,(function(e){return t._enter(e)})).on(i,t.config.selector,(function(e){return t._leave(e)}))}})),this._hideModalHandler=function(){t.element&&t.hide()},o.default(this.element).closest(".modal").on("hide.bs.modal",this._hideModalHandler),this.config.selector?this.config=r({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},e._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},e._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||o.default(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),o.default(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Qt:Ft]=!0),o.default(e.getTipElement()).hasClass(Rt)||e._hoverState===xt?e._hoverState=xt:(clearTimeout(e._timeout),e._hoverState=xt,e.config.delay&&e.config.delay.show?e._timeout=setTimeout((function(){e._hoverState===xt&&e.show()}),e.config.delay.show):e.show())},e._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||o.default(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),o.default(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Qt:Ft]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=qt,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout((function(){e._hoverState===qt&&e.hide()}),e.config.delay.hide):e.hide())},e._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},e._getConfig=function(t){var e=o.default(this.element).data();return Object.keys(e).forEach((function(t){-1!==Pt.indexOf(t)&&delete e[t]})),"number"==typeof(t=r({},this.constructor.Default,e,"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),d.typeCheckConfig(It,t,this.constructor.DefaultType),t.sanitize&&(t.template=At(t.template,t.whiteList,t.sanitizeFn)),t},e._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},e._cleanTipClass=function(){var t=o.default(this.getTipElement()),e=t.attr("class").match(jt);null!==e&&e.length&&t.removeClass(e.join(""))},e._handlePopperPlacementChange=function(t){this.tip=t.instance.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},e._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(o.default(t).removeClass(Lt),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},t._jQueryInterface=function(e){return this.each((function(){var n=o.default(this),i=n.data(kt),a="object"==typeof e&&e;if((i||!/dispose|hide/.test(e))&&(i||(i=new t(this,a),n.data(kt,i)),"string"==typeof e)){if("undefined"==typeof i[e])throw new TypeError('No method named "'+e+'"');i[e]()}}))},l(t,null,[{key:"VERSION",get:function(){return"4.6.1"}},{key:"Default",get:function(){return Ht}},{key:"NAME",get:function(){return It}},{key:"DATA_KEY",get:function(){return kt}},{key:"Event",get:function(){return Mt}},{key:"EVENT_KEY",get:function(){return".bs.tooltip"}},{key:"DefaultType",get:function(){return Ut}}]),t}();o.default.fn.tooltip=Wt._jQueryInterface,o.default.fn.tooltip.Constructor=Wt,o.default.fn.tooltip.noConflict=function(){return o.default.fn.tooltip=Ot,Wt._jQueryInterface};var Vt="bs.popover",zt=o.default.fn.popover,Kt=new RegExp("(^|\\s)bs-popover\\S+","g"),Xt=r({},Wt.Default,{placement:"right",trigger:"click",content:"",template:''}),Yt=r({},Wt.DefaultType,{content:"(string|element|function)"}),$t={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"},Jt=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),e.prototype.constructor=e,u(e,n);var a=i.prototype;return a.isWithContent=function(){return this.getTitle()||this._getContent()},a.addAttachmentClass=function(t){o.default(this.getTipElement()).addClass("bs-popover-"+t)},a.getTipElement=function(){return this.tip=this.tip||o.default(this.config.template)[0],this.tip},a.setContent=function(){var t=o.default(this.getTipElement());this.setElementContent(t.find(".popover-header"),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(".popover-body"),e),t.removeClass("fade show")},a._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},a._cleanTipClass=function(){var t=o.default(this.getTipElement()),e=t.attr("class").match(Kt);null!==e&&e.length>0&&t.removeClass(e.join(""))},i._jQueryInterface=function(t){return this.each((function(){var e=o.default(this).data(Vt),n="object"==typeof t?t:null;if((e||!/dispose|hide/.test(t))&&(e||(e=new i(this,n),o.default(this).data(Vt,e)),"string"==typeof t)){if("undefined"==typeof e[t])throw new TypeError('No method named "'+t+'"');e[t]()}}))},l(i,null,[{key:"VERSION",get:function(){return"4.6.1"}},{key:"Default",get:function(){return Xt}},{key:"NAME",get:function(){return"popover"}},{key:"DATA_KEY",get:function(){return Vt}},{key:"Event",get:function(){return $t}},{key:"EVENT_KEY",get:function(){return".bs.popover"}},{key:"DefaultType",get:function(){return Yt}}]),i}(Wt);o.default.fn.popover=Jt._jQueryInterface,o.default.fn.popover.Constructor=Jt,o.default.fn.popover.noConflict=function(){return o.default.fn.popover=zt,Jt._jQueryInterface};var Gt="scrollspy",Zt="bs.scrollspy",te=o.default.fn[Gt],ee="active",ne="position",ie=".nav, .list-group",oe={offset:10,method:"auto",target:""},ae={offset:"number",method:"string",target:"(string|element)"},se=function(){function t(t,e){var n=this;this._element=t,this._scrollElement="BODY"===t.tagName?window:t,this._config=this._getConfig(e),this._selector=this._config.target+" .nav-link,"+this._config.target+" .list-group-item,"+this._config.target+" .dropdown-item",this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,o.default(this._scrollElement).on("scroll.bs.scrollspy",(function(t){return n._process(t)})),this.refresh(),this._process()}var e=t.prototype;return e.refresh=function(){var t=this,e=this._scrollElement===this._scrollElement.window?"offset":ne,n="auto"===this._config.method?e:this._config.method,i=n===ne?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),[].slice.call(document.querySelectorAll(this._selector)).map((function(t){var e,a=d.getSelectorFromElement(t);if(a&&(e=document.querySelector(a)),e){var s=e.getBoundingClientRect();if(s.width||s.height)return[o.default(e)[n]().top+i,a]}return null})).filter((function(t){return t})).sort((function(t,e){return t[0]-e[0]})).forEach((function(e){t._offsets.push(e[0]),t._targets.push(e[1])}))},e.dispose=function(){o.default.removeData(this._element,Zt),o.default(this._scrollElement).off(".bs.scrollspy"),this._element=null,this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null},e._getConfig=function(t){if("string"!=typeof(t=r({},oe,"object"==typeof t&&t?t:{})).target&&d.isElement(t.target)){var e=o.default(t.target).attr("id");e||(e=d.getUID(Gt),o.default(t.target).attr("id",e)),t.target="#"+e}return d.typeCheckConfig(Gt,t,ae),t},e._getScrollTop=function(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop},e._getScrollHeight=function(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)},e._getOffsetHeight=function(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height},e._process=function(){var t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),n=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=n){var i=this._targets[this._targets.length-1];this._activeTarget!==i&&this._activate(i)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(var o=this._offsets.length;o--;)this._activeTarget!==this._targets[o]&&t>=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t li > .active",ge=function(){function t(t){this._element=t}var e=t.prototype;return e.show=function(){var t=this;if(!(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&o.default(this._element).hasClass(ue)||o.default(this._element).hasClass("disabled"))){var e,n,i=o.default(this._element).closest(".nav, .list-group")[0],a=d.getSelectorFromElement(this._element);if(i){var s="UL"===i.nodeName||"OL"===i.nodeName?he:ce;n=(n=o.default.makeArray(o.default(i).find(s)))[n.length-1]}var l=o.default.Event("hide.bs.tab",{relatedTarget:this._element}),r=o.default.Event("show.bs.tab",{relatedTarget:n});if(n&&o.default(n).trigger(l),o.default(this._element).trigger(r),!r.isDefaultPrevented()&&!l.isDefaultPrevented()){a&&(e=document.querySelector(a)),this._activate(this._element,i);var u=function(){var e=o.default.Event("hidden.bs.tab",{relatedTarget:t._element}),i=o.default.Event("shown.bs.tab",{relatedTarget:n});o.default(n).trigger(e),o.default(t._element).trigger(i)};e?this._activate(e,e.parentNode,u):u()}}},e.dispose=function(){o.default.removeData(this._element,le),this._element=null},e._activate=function(t,e,n){var i=this,a=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?o.default(e).children(ce):o.default(e).find(he))[0],s=n&&a&&o.default(a).hasClass(fe),l=function(){return i._transitionComplete(t,a,n)};if(a&&s){var r=d.getTransitionDurationFromElement(a);o.default(a).removeClass(de).one(d.TRANSITION_END,l).emulateTransitionEnd(r)}else l()},e._transitionComplete=function(t,e,n){if(e){o.default(e).removeClass(ue);var i=o.default(e.parentNode).find("> .dropdown-menu .active")[0];i&&o.default(i).removeClass(ue),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}o.default(t).addClass(ue),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),d.reflow(t),t.classList.contains(fe)&&t.classList.add(de);var a=t.parentNode;if(a&&"LI"===a.nodeName&&(a=a.parentNode),a&&o.default(a).hasClass("dropdown-menu")){var s=o.default(t).closest(".dropdown")[0];if(s){var l=[].slice.call(s.querySelectorAll(".dropdown-toggle"));o.default(l).addClass(ue)}t.setAttribute("aria-expanded",!0)}n&&n()},t._jQueryInterface=function(e){return this.each((function(){var n=o.default(this),i=n.data(le);if(i||(i=new t(this),n.data(le,i)),"string"==typeof e){if("undefined"==typeof i[e])throw new TypeError('No method named "'+e+'"');i[e]()}}))},l(t,null,[{key:"VERSION",get:function(){return"4.6.1"}}]),t}();o.default(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]',(function(t){t.preventDefault(),ge._jQueryInterface.call(o.default(this),"show")})),o.default.fn.tab=ge._jQueryInterface,o.default.fn.tab.Constructor=ge,o.default.fn.tab.noConflict=function(){return o.default.fn.tab=re,ge._jQueryInterface};var me="bs.toast",pe=o.default.fn.toast,_e="hide",ve="show",ye="showing",be="click.dismiss.bs.toast",Ee={animation:!0,autohide:!0,delay:500},Te={animation:"boolean",autohide:"boolean",delay:"number"},we=function(){function t(t,e){this._element=t,this._config=this._getConfig(e),this._timeout=null,this._setListeners()}var e=t.prototype;return e.show=function(){var t=this,e=o.default.Event("show.bs.toast");if(o.default(this._element).trigger(e),!e.isDefaultPrevented()){this._clearTimeout(),this._config.animation&&this._element.classList.add("fade");var n=function(){t._element.classList.remove(ye),t._element.classList.add(ve),o.default(t._element).trigger("shown.bs.toast"),t._config.autohide&&(t._timeout=setTimeout((function(){t.hide()}),t._config.delay))};if(this._element.classList.remove(_e),d.reflow(this._element),this._element.classList.add(ye),this._config.animation){var i=d.getTransitionDurationFromElement(this._element);o.default(this._element).one(d.TRANSITION_END,n).emulateTransitionEnd(i)}else n()}},e.hide=function(){if(this._element.classList.contains(ve)){var t=o.default.Event("hide.bs.toast");o.default(this._element).trigger(t),t.isDefaultPrevented()||this._close()}},e.dispose=function(){this._clearTimeout(),this._element.classList.contains(ve)&&this._element.classList.remove(ve),o.default(this._element).off(be),o.default.removeData(this._element,me),this._element=null,this._config=null},e._getConfig=function(t){return t=r({},Ee,o.default(this._element).data(),"object"==typeof t&&t?t:{}),d.typeCheckConfig("toast",t,this.constructor.DefaultType),t},e._setListeners=function(){var t=this;o.default(this._element).on(be,'[data-dismiss="toast"]',(function(){return t.hide()}))},e._close=function(){var t=this,e=function(){t._element.classList.add(_e),o.default(t._element).trigger("hidden.bs.toast")};if(this._element.classList.remove(ve),this._config.animation){var n=d.getTransitionDurationFromElement(this._element);o.default(this._element).one(d.TRANSITION_END,e).emulateTransitionEnd(n)}else e()},e._clearTimeout=function(){clearTimeout(this._timeout),this._timeout=null},t._jQueryInterface=function(e){return this.each((function(){var n=o.default(this),i=n.data(me);if(i||(i=new t(this,"object"==typeof e&&e),n.data(me,i)),"string"==typeof e){if("undefined"==typeof i[e])throw new TypeError('No method named "'+e+'"');i[e](this)}}))},l(t,null,[{key:"VERSION",get:function(){return"4.6.1"}},{key:"DefaultType",get:function(){return Te}},{key:"Default",get:function(){return Ee}}]),t}();o.default.fn.toast=we._jQueryInterface,o.default.fn.toast.Constructor=we,o.default.fn.toast.noConflict=function(){return o.default.fn.toast=pe,we._jQueryInterface},t.Alert=g,t.Button=E,t.Carousel=P,t.Collapse=V,t.Dropdown=lt,t.Modal=Ct,t.Popover=Jt,t.Scrollspy=se,t.Tab=ge,t.Toast=we,t.Tooltip=Wt,t.Util=d,Object.defineProperty(t,"__esModule",{value:!0})})); 7 | -------------------------------------------------------------------------------- /www/static/jquery.slim.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v3.5.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector | (c) JS Foundation and other contributors | jquery.org/license */ 2 | !function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(g,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,v=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,y=n.hasOwnProperty,a=y.toString,l=a.call(Object),m={},b=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},w=g.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function C(e,t,n){var r,i,o=(n=n||w).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function T(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.5.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector",E=function(e,t){return new E.fn.init(e,t)};function d(e){var t=!!e&&"length"in e&&e.length,n=T(e);return!b(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+R+")"+R+"*"),U=new RegExp(R+"|>"),V=new RegExp(W),X=new RegExp("^"+B+"$"),Q={ID:new RegExp("^#("+B+")"),CLASS:new RegExp("^\\.("+B+")"),TAG:new RegExp("^("+B+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+R+"*(even|odd|(([+-]|)(\\d*)n|)"+R+"*(?:([+-]|)"+R+"*(\\d+)|))"+R+"*\\)|)","i"),bool:new RegExp("^(?:"+I+")$","i"),needsContext:new RegExp("^"+R+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+R+"*((?:-\\d)?\\d*)"+R+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,G=/^(?:input|select|textarea|button)$/i,K=/^h\d$/i,J=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+R+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){C()},ae=xe(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{O.apply(t=P.call(d.childNodes),d.childNodes),t[d.childNodes.length].nodeType}catch(e){O={apply:t.length?function(e,t){q.apply(e,P.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,d=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==d&&9!==d&&11!==d)return n;if(!r&&(C(e),e=e||T,E)){if(11!==d&&(u=Z.exec(t)))if(i=u[1]){if(9===d){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return O.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&p.getElementsByClassName&&e.getElementsByClassName)return O.apply(n,e.getElementsByClassName(i)),n}if(p.qsa&&!k[t+" "]&&(!v||!v.test(t))&&(1!==d||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===d&&(U.test(t)||_.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&p.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=A)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+be(l[o]);c=l.join(",")}try{return O.apply(n,f.querySelectorAll(c)),n}catch(e){k(t,!0)}finally{s===A&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>x.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[A]=!0,e}function ce(e){var t=T.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)x.attrHandle[n[r]]=t}function de(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function pe(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in p=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},C=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:d;return r!=T&&9===r.nodeType&&r.documentElement&&(a=(T=r).documentElement,E=!i(T),d!=T&&(n=T.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),p.scope=ce(function(e){return a.appendChild(e).appendChild(T.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),p.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),p.getElementsByTagName=ce(function(e){return e.appendChild(T.createComment("")),!e.getElementsByTagName("*").length}),p.getElementsByClassName=J.test(T.getElementsByClassName),p.getById=ce(function(e){return a.appendChild(e).id=A,!T.getElementsByName||!T.getElementsByName(A).length}),p.getById?(x.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},x.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(x.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},x.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),x.find.TAG=p.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):p.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},x.find.CLASS=p.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(p.qsa=J.test(T.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+R+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+R+"*(?:value|"+I+")"),e.querySelectorAll("[id~="+A+"-]").length||v.push("~="),(t=T.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+R+"*name"+R+"*="+R+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+A+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=T.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+R+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(p.matchesSelector=J.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){p.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",W)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=J.test(a.compareDocumentPosition),y=t||J.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!p.sortDetached&&t.compareDocumentPosition(e)===n?e==T||e.ownerDocument==d&&y(d,e)?-1:t==T||t.ownerDocument==d&&y(d,t)?1:u?H(u,e)-H(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==T?-1:t==T?1:i?-1:o?1:u?H(u,e)-H(u,t):0;if(i===o)return de(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?de(a[r],s[r]):a[r]==d?-1:s[r]==d?1:0}),T},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(C(e),p.matchesSelector&&E&&!k[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||p.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){k(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return Q.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&V.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+R+")"+e+"("+R+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function D(e,n,r){return b(n)?E.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?E.grep(e,function(e){return e===n!==r}):"string"!=typeof n?E.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(E.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||L,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:j.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof E?t[0]:t,E.merge(this,E.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:w,!0)),k.test(r[1])&&E.isPlainObject(t))for(r in t)b(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=w.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):b(e)?void 0!==n.ready?n.ready(e):e(E):E.makeArray(e,this)}).prototype=E.fn,L=E(w);var q=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}E.fn.extend({has:function(e){var t=E(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,pe=/^$|^module$|\/(?:java|ecma)script/i;le=w.createDocumentFragment().appendChild(w.createElement("div")),(ce=w.createElement("input")).setAttribute("type","radio"),ce.setAttribute("checked","checked"),ce.setAttribute("name","t"),le.appendChild(ce),m.checkClone=le.cloneNode(!0).cloneNode(!0).lastChild.checked,le.innerHTML="",m.noCloneChecked=!!le.cloneNode(!0).lastChild.defaultValue,le.innerHTML="",m.option=!!le.lastChild;var he={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ge(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&S(e,t)?E.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n",""]);var ye=/<|&#?\w+;/;function me(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),d=[],p=0,h=e.length;p\s*$/g;function Le(e,t){return S(e,"table")&&S(11!==t.nodeType?t:t.firstChild,"tr")&&E(e).children("tbody")[0]||e}function je(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Oe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n
",2===ft.childNodes.length),E.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(m.createHTMLDocument?((r=(t=w.implementation.createHTMLDocument("")).createElement("base")).href=w.location.href,t.head.appendChild(r)):t=w),o=!n&&[],(i=k.exec(e))?[t.createElement(i[1])]:(i=me([e],t,o),o&&o.length&&E(o).remove(),E.merge([],i.childNodes)));var r,i,o},E.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=E.css(e,"position"),c=E(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=E.css(e,"top"),u=E.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),b(t)&&(t=t.call(e,n,E.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):("number"==typeof f.top&&(f.top+="px"),"number"==typeof f.left&&(f.left+="px"),c.css(f))}},E.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){E.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===E.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===E.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=E(e).offset()).top+=E.css(e,"borderTopWidth",!0),i.left+=E.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-E.css(r,"marginTop",!0),left:t.left-i.left-E.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===E.css(e,"position"))e=e.offsetParent;return e||re})}}),E.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;E.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),E.each(["top","left"],function(e,n){E.cssHooks[n]=Fe(m.pixelPosition,function(e,t){if(t)return t=We(e,n),Ie.test(t)?E(e).position()[n]+"px":t})}),E.each({Height:"height",Width:"width"},function(a,s){E.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){E.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?E.css(e,t,i):E.style(e,t,n,i)},s,n?e:void 0,n)}})}),E.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),E.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){E.fn[n]=function(e,t){return 0