Quellcode durchsuchen

combine server and client

Starink vor 6 Jahren
Ursprung
Commit
484e684f48

+ 1 - 1
middleware/auth.js

@@ -23,7 +23,7 @@ export default function ({route, store, req, res, redirect}) {
   if (!token && path !== "/" && path !=="/login" && path !== "/register") {
     let return_uri = encodeURIComponent("http://localhost:3000/receiveIDToken");
     console.log("encoded path");
-    let url = 'http://localhost:8080/api/interaction?'+
+    let url = 'http://localhost:3000/api/interaction?'+
       'client_id='+ 'seec-portal-1919810-114514' +
       '&scope=openid' +
       '&redirect_uri=' + return_uri +

+ 10 - 10
nuxt.config.js

@@ -66,19 +66,19 @@ module.exports = {
   },
 
   axios: {
-    proxy: true,
+    proxy: false,
     prefix: '/api', // baseURL
     credentials: true,
   },
-  proxy: {
-    '/api/': {
-      target: 'http://localhost:8080', // 代理地址
-      // changeOrigin: true,
-      // pathRewrite: {
-      //   '^/api': ''
-      // },
-    },
-  },
+  // proxy: {
+  //   '/api/': {
+  //     target: 'http://localhost:8080', // 代理地址
+  //     // changeOrigin: true,
+  //     // pathRewrite: {
+  //     //   '^/api': ''
+  //     // },
+  //   },
+  // },
 
   // server: {
   //   https: true

+ 30 - 3
package.json

@@ -21,15 +21,42 @@
     "cross-env": "^5.2.0",
     "element-ui": "^2.4.11",
     "koa": "^2.6.2",
-    "nuxt": "^2.0.0"
+    "nuxt": "^2.0.0",
+
+    "bcrypt": "^3.0.8",
+    "debug": "^4.1.1",
+    "koa-bodyparser": "^4.2.1",
+    "koa-convert": "^1.2.0",
+    "koa-helmet": "^5.2.0",
+    "koa-json": "^2.0.2",
+    "koa-jwt": "^3.6.0",
+    "koa-logger": "^3.2.0",
+    "koa-mount": "^4.0.0",
+    "koa-onerror": "^4.1.0",
+    "koa-router": "^7.4.0",
+    "koa-static": "^5.0.0",
+    "koa-views": "^6.2.0",
+    "koa2-cors": "^2.0.6",
+    "lodash": "^4.17.15",
+    "mysql": "^2.18.1",
+    "mysql2": "^2.1.0",
+    "oidc-provider": "^6.23.1",
+    "openid-client": "^3.14.0",
+    "pug": "^2.0.3",
+    "request": "^2.88.2",
+    "sequelize": "^5.21.4",
+    "util": "^0.12.1"
   },
   "devDependencies": {
     "@vue/test-utils": "^1.0.0-beta.27",
     "babel-jest": "^24.1.0",
     "jest": "^24.1.0",
     "node-sass": "^4.13.1",
-    "nodemon": "^1.18.9",
     "sass-loader": "^8.0.2",
-    "vue-jest": "^4.0.0-0"
+    "vue-jest": "^4.0.0-0",
+    "nodemon": "^1.19.1",
+    "chai": "^4.2.0",
+    "supertest": "^4.0.2",
+    "mocha": "^7.0.1"
   }
 }

+ 2 - 2
pages/login.vue

@@ -2,7 +2,7 @@
   <div class="page-container login-container">
     <el-card class="login-card">
       <h2 class="login-card__title login-card__item">登 录</h2>
-      <el-input ref="phone" v-model="phone"  oninput="value=value.replace(/[^\d]/g,'')" placeholder="请输入手机号" minlength='11' maxlength='11' class="login-card__item" autofocus></el-input>
+      <el-input ref="phone" v-model="phone" oninput="value=value.replace(/[^\d]/g,'')" placeholder="请输入手机号" minlength='11' maxlength='11' class="login-card__item" autofocus></el-input>
       <el-input ref="password" v-model="password" type="password" placeholder="请输入密码" maxlength="20" class="login-card__item"></el-input>
       <el-input ref="iCode" v-model="iCode" placeholder="请输入验证码" class="login-card__short-item"></el-input>
 
@@ -152,7 +152,7 @@
               this.$store.commit("saveToken", res.data.token);
               this.$message({
                 showClose: true,
-                message: "欢迎回来," + res.data.name,
+                message: "欢迎回来," + res.data.user.name,
                 type: 'success'
               });
             }else{

+ 11 - 0
server/.babelrc

@@ -0,0 +1,11 @@
+{
+  "env": {
+    "test": {
+      "presets": ["es2015-node5"],
+      "plugins": [
+        "transform-async-to-generator",
+        "syntax-async-functions"
+      ]
+    }
+  }
+}

+ 0 - 0
server/README.md


+ 87 - 0
server/adapters/memory_adapter.js

@@ -0,0 +1,87 @@
+const LRU = require('lru-cache');
+
+const epochTime = require('../helpers/epoch_time');
+
+let storage = new LRU({});
+
+function grantKeyFor(id) {
+  return `grant:${id}`;
+}
+
+function sessionUidKeyFor(id) {
+  return `sessionUid:${id}`;
+}
+
+function userCodeKeyFor(userCode) {
+  return `userCode:${userCode}`;
+}
+
+class MemoryAdapter {
+  constructor(model) {
+    this.model = model;
+  }
+
+  key(id) {
+    return `${this.model}:${id}`;
+  }
+
+  async destroy(id) {
+    const key = this.key(id);
+    storage.del(key);
+  }
+
+  async consume(id) {
+    storage.get(this.key(id)).consumed = epochTime();
+  }
+
+  async find(id) {
+    return storage.get(this.key(id));
+  }
+
+  async findByUid(uid) {
+    const id = storage.get(sessionUidKeyFor(uid));
+    return this.find(id);
+  }
+
+  async findByUserCode(userCode) {
+    const id = storage.get(userCodeKeyFor(userCode));
+    return this.find(id);
+  }
+
+  async upsert(id, payload, expiresIn) {
+    const key = this.key(id);
+
+    if (this.model === 'Session') {
+      storage.set(sessionUidKeyFor(payload.uid), id, expiresIn * 1000);
+    }
+
+    const { grantId, userCode } = payload;
+    if (grantId) {
+      const grantKey = grantKeyFor(grantId);
+      const grant = storage.get(grantKey);
+      if (!grant) {
+        storage.set(grantKey, [key]);
+      } else {
+        grant.push(key);
+      }
+    }
+
+    if (userCode) {
+      storage.set(userCodeKeyFor(userCode), id, expiresIn * 1000);
+    }
+
+    storage.set(key, payload, expiresIn * 1000);
+  }
+
+  async revokeByGrantId(grantId) { // eslint-disable-line class-methods-use-this
+    const grantKey = grantKeyFor(grantId);
+    const grant = storage.get(grantKey);
+    if (grant) {
+      grant.forEach((token) => storage.del(token));
+      storage.del(grantKey);
+    }
+  }
+}
+
+module.exports = MemoryAdapter;
+module.exports.setStorage = (store) => { storage = store; };

+ 114 - 0
server/adapters/sequelize.js

@@ -0,0 +1,114 @@
+/*
+ * This is a very rough-edged example, the idea is to still work with the fact that oidc-provider
+ * has a rather "dynamic" schema. This example uses sequelize with postgresql, and all dynamic data
+ * uses JSON fields. id is set to be the primary key, grantId should be additionaly indexed for
+ * models where these fields are set (grantId-able models). userCode should be additionaly indexed
+ * for DeviceCode model. uid should be additionaly indexed for Session model. For sequelize
+ * migrations @see https://github.com/Rogger794/node-oidc-provider/tree/examples/example/migrations/sequelize
+*/
+
+// npm i sequelize@^5.21.2
+const Sequelize = require('sequelize'); // eslint-disable-line import/no-unresolved
+
+const sequelize = new Sequelize('seec_sso', 'root', '981018', {
+  host: 'localhost',
+  port: 3306,
+  dialect: 'mysql',
+});
+
+const grantable = new Set([
+  'AccessToken',
+  'AuthorizationCode',
+  'RefreshToken',
+  'DeviceCode',
+]);
+
+const models = [
+  'Session',
+  'AccessToken',
+  'AuthorizationCode',
+  'RefreshToken',
+  'DeviceCode',
+  'ClientCredentials',
+  'Client',
+  'InitialAccessToken',
+  'RegistrationAccessToken',
+  'Interaction',
+  'ReplayDetection',
+  'PushedAuthorizationRequest',
+].reduce((map, name) => {
+  map.set(name, sequelize.define(name, {
+    id: { type: Sequelize.STRING, primaryKey: true },
+    ...(grantable.has(name) ? { grantId: { type: Sequelize.STRING } } : undefined),
+    ...(name === 'DeviceCode' ? { userCode: { type: Sequelize.STRING } } : undefined),
+    ...(name === 'Session' ? { uid: { type: Sequelize.STRING } } : undefined),
+    data: { type: Sequelize.JSONB },
+    expiresAt: { type: Sequelize.DATE },
+    consumedAt: { type: Sequelize.DATE },
+  }));
+
+  return map;
+}, new Map());
+
+class SequelizeAdapter {
+  constructor(name) {
+    this.model = models.get(name);
+    this.name = name;
+  }
+
+  async upsert(id, data, expiresIn) {
+    await this.model.upsert({
+      id,
+      data,
+      ...(data.grantId ? { grantId: data.grantId } : undefined),
+      ...(data.userCode ? { userCode: data.userCode } : undefined),
+      ...(data.uid ? { uid: data.uid } : undefined),
+      ...(expiresIn ? { expiresAt: new Date(Date.now() + (expiresIn * 1000)) } : undefined),
+    });
+  }
+
+  async find(id) {
+    const found = await this.model.findByPk(id);
+    if (!found) return undefined;
+    return {
+      ...found.data,
+      ...(found.consumedAt ? { consumed: true } : undefined),
+    };
+  }
+
+  async findByUserCode(userCode) {
+    const found = await this.model.findOne({ where: { userCode } });
+    if (!found) return undefined;
+    return {
+      ...found.data,
+      ...(found.consumedAt ? { consumed: true } : undefined),
+    };
+  }
+
+  async findByUid(uid) {
+    const found = await this.model.findOne({ where: { uid } });
+    if (!found) return undefined;
+    return {
+      ...found.data,
+      ...(found.consumedAt ? { consumed: true } : undefined),
+    };
+  }
+
+  async destroy(id) {
+    await this.model.destroy({ where: { id } });
+  }
+
+  async consume(id) {
+    await this.model.update({ consumedAt: new Date() }, { where: { id } });
+  }
+
+  async revokeByGrantId(grantId) {
+    await this.model.destroy({ where: { grantId } });
+  }
+
+  static async connect() {
+    return sequelize.sync();
+  }
+}
+
+module.exports = SequelizeAdapter;

+ 91 - 0
server/bin/www

@@ -0,0 +1,91 @@
+// #!/usr/bin/env node
+//
+// /**
+//  * Module dependencies.
+//  */
+//
+// var app = require('../app');
+// var debug = require('debug')('demo:server');
+// var http = require('http');
+//
+// /**
+//  * Get port from environment and store in Express.
+//  */
+//
+// var port = normalizePort(process.env.PORT || '8080');
+// // app.set('port', port);
+//
+// /**
+//  * Create HTTP server.
+//  */
+//
+// var server = http.createServer(app.callback());
+//
+// /**
+//  * Listen on provided port, on all network interfaces.
+//  */
+//
+// server.listen(port);
+// console.log("server at port " + port);
+// server.on('error', onError);
+// server.on('listening', onListening);
+//
+// /**
+//  * Normalize a port into a number, string, or false.
+//  */
+//
+// function normalizePort(val) {
+//   var port = parseInt(val, 10);
+//
+//   if (isNaN(port)) {
+//     // named pipe
+//     return val;
+//   }
+//
+//   if (port >= 0) {
+//     // port number
+//     return port;
+//   }
+//
+//   return false;
+// }
+//
+// /**
+//  * Event listener for HTTP server "error" event.
+//  */
+//
+// function onError(error) {
+//   if (error.syscall !== 'listen') {
+//     throw error;
+//   }
+//
+//   var bind = typeof port === 'string'
+//     ? 'Pipe ' + port
+//     : 'Port ' + port;
+//
+//   // handle specific listen errors with friendly messages
+//   switch (error.code) {
+//     case 'EACCES':
+//       console.error(bind + ' requires elevated privileges');
+//       process.exit(1);
+//       break;
+//     case 'EADDRINUSE':
+//       console.error(bind + ' is already in use');
+//       process.exit(1);
+//       break;
+//     default:
+//       throw error;
+//   }
+// }
+//
+// /**
+//  * Event listener for HTTP server "listening" event.
+//  */
+//
+// function onListening() {
+//   var addr = server.address();
+//   var bind = typeof addr === 'string'
+//     ? 'pipe ' + addr
+//     : 'port ' + addr.port;
+//   debug('Listening on ' + bind);
+// }

+ 9 - 0
server/config/db.js

@@ -0,0 +1,9 @@
+const Sequelize = require('sequelize');
+
+const sequelize = new Sequelize('mysql://root:981018@localhost:3306/seec_portal', {
+    define: {timestamps: false},
+        timezone: '+08:00' //东八时区
+    });
+
+sequelize.sync({alter: true});
+module.exports = sequelize;

+ 3 - 0
server/config/secret.json

@@ -0,0 +1,3 @@
+{
+  "sign": "seec-portal"
+}

+ 37 - 0
server/controller/auth.js

@@ -0,0 +1,37 @@
+const Response = require('../util/response');
+
+class AuthController{
+    static checkClient(ctx){
+        const allowed_client_ids = ['seec-portal-1919810-114514'];
+        const allowed_redirect_domains = ['localhost:3000'];
+
+        const query = ctx.query;
+        console.log('query', query);
+
+        const {client_id, redirect_uri, scope} = query;
+        if(!client_id || !redirect_uri || !scope){
+            ctx.body=Response.failed("非法请求,拒绝访问");
+        }
+        else if(allowed_client_ids.indexOf(client_id) < 0 || scope !== "openid" || (redirect_uri.indexOf('https://')!==0 && redirect_uri.indexOf('http://')!==0) ){
+            ctx.body=Response.failed("非法请求,拒绝访问");
+        }
+        else{
+            let durl=/http:\/\/([^\/]+)\//i;
+            let domain = redirect_uri.match(durl)[1];
+            // console.log('domain',domain);
+            if(allowed_redirect_domains.indexOf(domain) < 0){
+                ctx.body=Response.failed("非法请求,拒绝访问");
+            }else{
+                ctx.cookies.set('redirect_uri', redirect_uri, {
+                    maxAge:60*1000*3
+                });
+                ctx.redirect("/login");
+            }
+        }
+
+
+
+    }
+}
+
+module.exports = AuthController;

+ 39 - 0
server/controller/course.js

@@ -0,0 +1,39 @@
+const myRequest = require('../util/request');
+const Response = require('../util/response');
+
+class CourseController {
+    /**
+     * 获取热门课程
+     * @param ctx
+     * @returns {Promise.<void>}
+     */
+    static async getHotCourses(ctx) {
+        const {uid} = ctx.request.query;
+        const courses = await myRequest("http://mooc.seecoder.cn/api/statistic/hotCourses");
+        if(courses){
+            ctx.body = Response.success({hotCourses: courses});
+        }else{
+            ctx.body = Response.failed("获取热门课程失败");
+        }
+
+    }
+
+
+    /**
+     * 获取我的课程
+     * @param ctx
+     * @returns {Promise.<void>}
+     */
+    static async getMyCourses(ctx) {
+        const {uid} = ctx.request.query;
+        const courses = await myRequest("http://mooc.seecoder.cn/api/statistic/myCourses/"+uid);
+        if(courses){
+            ctx.body = Response.success({myCourses: courses});
+        }else{
+            ctx.body = Response.failed("获取当前课程失败");
+        }
+
+    }
+}
+
+module.exports = CourseController;

+ 13 - 0
server/controller/oidc.js

@@ -0,0 +1,13 @@
+
+const authenticate = async (ctx, provider) => {
+    console.log('ctx.session', ctx.session);
+    let session = await provider.setProviderSession(ctx.req, ctx.res, {account: ctx.req.headers.cookie});
+    console.log('ctx.', ctx.session);
+    const details = await provider.interactionDetails(ctx.req, ctx.res);
+    console.log('detail',JSON.stringify(details));
+};
+
+
+module.exports ={
+    authenticate
+};

+ 57 - 0
server/controller/user.js

@@ -0,0 +1,57 @@
+const userModel = require('../model/user');
+const jwt = require('jsonwebtoken');
+const bcrypt = require('bcrypt');
+const secret = require('../config/secret');
+const Response = require('../util/response');
+
+
+class UserController {
+    /**
+     * 登录
+     * @param ctx
+     * @returns {Promise.<void>}
+     */
+    static async login(ctx) {
+        const data = ctx.request.body;
+        const user = await userModel.findByPhone(data.phone)
+        if (user) {
+            const confirmRes = bcrypt.compareSync(data.password, user.password);
+            if(!confirmRes){
+                ctx.body = Response.failed('密码错误,请重试')
+            }else{
+                console.log('ctx',ctx);
+                // ctx.redirect("/");
+
+                const token = jwt.sign({
+                    name: user.name,
+                    id: user.id
+                }, secret.sign, { expiresIn: '2h' });
+                ctx.body = Response.success(
+                    {user: {phone: user.phone, id: user.id, name: user.name},
+                        token: token});
+            }
+        } else {
+            ctx.body = Response.failed('此账号不存在');
+        }
+    }
+
+
+    /**
+     * 注册
+     * @param ctx
+     * @returns {Promise.<void>}
+     */
+    static async register(ctx) {
+        const data = ctx.request.body;
+        const hashPassword = bcrypt.hashSync(data.password, 10);
+        const user = await userModel.create(data.name, hashPassword, data.phone);
+        if (user) {
+            ctx.body = Response.success({name: user.name});
+        } else {
+            ctx.body = Response.failed('此手机号已注册');
+        }
+    }
+}
+
+module.exports = UserController;
+

+ 126 - 3
server/index.js

@@ -1,8 +1,128 @@
-const Koa = require('koa')
+const Koa = require('koa');
+const app = new Koa();
+const views = require('koa-views');
+const json = require('koa-json');
+const onerror = require('koa-onerror');
+const bodyparser = require('koa-bodyparser');
+const logger = require('koa-logger');
+const cors = require('koa2-cors');
+const jwt = require('koa-jwt');
+const helmet = require('koa-helmet');
+const mount = require('koa-mount');
+const set = require('lodash/set');
+const { Provider } = require('oidc-provider');
 const consola = require('consola')
 const { Nuxt, Builder } = require('nuxt')
 
-const app = new Koa()
+const secret = require('./config/secret.json');
+const Account = require('./support/account');
+const configuration = require('./support/configuration');
+
+const router = require('./routes/index');
+
+const { PORT = 3000, ISSUER = `http://localhost:${PORT}` } = process.env;
+
+// error handler
+onerror(app);
+
+// middlewares
+app.use(bodyparser({
+    enableTypes:['json', 'form', 'text']
+}));
+
+app.use(cors({
+    origin:  'http://localhost',
+    // exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'],
+    maxAge: 3600,
+    credentials: true,
+    allowMethods: ['GET', 'POST', 'OPTIONS'],
+    allowHeaders: ['Content-Type', 'Authorization', 'Accept'],
+}));
+
+
+app.use(json());
+app.use(logger());
+app.use(require('koa-static')(__dirname + '/public'));
+
+app.use(views(__dirname + '/views', {
+    extension: 'ejs'
+}));
+
+// logger
+app.use(async (ctx, next) => {
+    const start = new Date();
+    await next();
+    const ms = new Date() - start;
+    console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
+});
+
+// app.use(jwt({
+//     secret: secret.sign
+// }).unless({
+//     path: [/\/api\/user\/login/, /\/api\/user\/register/]
+// }));
+
+// Custom 401 handling if you don't want to expose koa-jwt errors to users
+app.use(function(ctx, next){
+    return next().catch((err) => {
+        if (401 === err.status) {
+            ctx.status = 401;
+            ctx.body = 'Protected resource, use Authorization header to get access\n';
+        } else {
+            throw err;
+        }
+    });
+});
+//
+// // routes
+// app.use(router.routes(provider), router.allowedMethods());
+
+// error-handling
+app.on('error', (err, ctx) => {
+    console.error('server error', err, ctx)
+});
+
+
+// SSO
+app.use(helmet());
+
+if (process.env.NODE_ENV === 'production') {
+    app.proxy = true;
+    set(configuration, 'cookies.short.secure', true);
+    set(configuration, 'cookies.long.secure', true);
+
+    app.use(async (ctx, next) => {
+        if (ctx.secure) {
+            await next();
+        } else if (ctx.method === 'GET' || ctx.method === 'HEAD') {
+            ctx.redirect(ctx.href.replace(/^http:\/\//i, 'https://'));
+        } else {
+            ctx.body = {
+                error: 'invalid_request',
+                error_description: 'do yourself a favor and only use https',
+            };
+            ctx.status = 400;
+        }
+    });
+}
+
+(async () => {
+    let adapter;
+    if (process.env.MONGODB_URI) {
+        adapter = require('./adapters/memory_adapter'); // eslint-disable-line global-require
+        // await adapter.connect();
+    }
+
+    const provider = new Provider(ISSUER, { adapter, ...configuration });
+
+    provider.use(helmet());
+
+    app.use(router(provider).routes(), router(provider).allowedMethods());
+    app.use(mount(provider.app));
+})().catch((err) => {
+    console.error(err);
+    process.exitCode = 1;
+});
 
 // Import and Set Nuxt.js options
 const config = require('../nuxt.config.js')
@@ -38,4 +158,7 @@ async function start () {
   })
 }
 
-start()
+start();
+
+
+// module.exports = app;

+ 39 - 0
server/model/user.js

@@ -0,0 +1,39 @@
+const db = require('../config/db');
+const getNowFormatTime = require('../util/time-processer');
+const User = db.import('../schema/user.js');
+
+User.sync();
+
+class UserModel {
+    /**
+     * 按手机号码查询
+     * @param phone
+     * @returns {Promise.<*>}
+     */
+    static async findByPhone(phone){
+        return User.findOne({
+            where: {phone:phone}
+        });
+    }
+
+
+    /**
+     * 创建用户
+     * @param name 昵称
+     * @param password 密码
+     * @param phone 手机号码
+     * @returns {Promise.<*>}
+     */
+    static async create(name, password, phone ){
+        let user = await User.findOne({
+            where: {phone:phone}
+        });
+        if(user){
+            return false;
+        }
+        user = await User.create({name: name, password: password, phone: phone, createdTime: getNowFormatTime()});
+        return user;
+    }
+}
+
+module.exports = UserModel;

+ 43 - 0
server/package.json

@@ -0,0 +1,43 @@
+{
+  "name": "portal-backend",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "start": "node bin/www",
+    "dev": "./node_modules/.bin/nodemon bin/www",
+    "prd": "pm2 start bin/www",
+    "test": "mocha --compilers js:babel-core/register"
+  },
+  "dependencies": {
+    "bcrypt": "^3.0.8",
+    "debug": "^4.1.1",
+    "koa": "^2.7.0",
+    "koa-bodyparser": "^4.2.1",
+    "koa-convert": "^1.2.0",
+    "koa-helmet": "^5.2.0",
+    "koa-json": "^2.0.2",
+    "koa-jwt": "^3.6.0",
+    "koa-logger": "^3.2.0",
+    "koa-mount": "^4.0.0",
+    "koa-onerror": "^4.1.0",
+    "koa-router": "^7.4.0",
+    "koa-static": "^5.0.0",
+    "koa-views": "^6.2.0",
+    "koa2-cors": "^2.0.6",
+    "lodash": "^4.17.15",
+    "mysql": "^2.18.1",
+    "mysql2": "^2.1.0",
+    "oidc-provider": "^6.23.1",
+    "openid-client": "^3.14.0",
+    "pug": "^2.0.3",
+    "request": "^2.88.2",
+    "sequelize": "^5.21.4",
+    "util": "^0.12.1"
+  },
+  "devDependencies": {
+    "nodemon": "^1.19.1",
+    "chai": "^4.2.0",
+    "supertest": "^4.0.2",
+    "mocha": "^7.0.1"
+  }
+}

+ 8 - 0
server/public/stylesheets/style.css

@@ -0,0 +1,8 @@
+body {
+  padding: 50px;
+  font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
+}
+
+a {
+  color: #00B7FF;
+}

+ 20 - 0
server/routes/index.js

@@ -0,0 +1,20 @@
+const Router = require('koa-router');
+const UserController = require('../controller/user');
+const CourseController = require('../controller/course');
+const OIDCController = require('../controller/oidc');
+const AuthController = require('../controller/auth');
+const router = new Router({prefix: '/api'});
+
+module.exports = (provider) => {
+    router.post('/user/login', UserController.login); // 登录
+    router.post('/user/register', UserController.register);  // 登录(回调)
+
+    router.get('/course/getMyCourses',  CourseController.getMyCourses);
+    router.get('/course/getHotCourses',  CourseController.getHotCourses);
+
+    router.get('/interaction', AuthController.checkClient);
+
+    //用户信息
+    //.post('/user/get', UserController.get); // 获取用户信息
+    return router;
+};

+ 39 - 0
server/schema/user.js

@@ -0,0 +1,39 @@
+
+const moment = require('moment');
+
+module.exports = (sequelize, DataTypes) =>
+    sequelize.define('user', {
+        id: {
+            type: DataTypes.INTEGER,
+            allowNull: false,
+            primaryKey: true,
+            autoIncrement: true
+        },
+        name: {
+            type: DataTypes.STRING(64),
+            allowNull: false
+        },
+        password: {
+            type: DataTypes.STRING(512),
+            allowNull: false
+        },
+        phone: {
+            type: DataTypes.STRING(64),
+            allowNull: false
+        },
+        signature: {
+            type: DataTypes.STRING(512),
+            allowNull: false,
+            defaultValue: ""
+        },
+        createdTime: {
+            type: DataTypes.DATE,
+            allowNull: false,
+            get() {
+                return moment(this.getDataValue('createdTime')).format('YYYY-MM-DD HH:mm:ss');
+            }
+
+        }
+    }, {
+        tableName: 'user'
+    });

+ 90 - 0
server/support/account.js

@@ -0,0 +1,90 @@
+const store = new Map();
+const logins = new Map();
+const nanoid = require('nanoid');
+
+class Account {
+  constructor(id, profile) {
+    this.accountId = id || nanoid();
+    this.profile = profile;
+    store.set(this.accountId, this);
+  }
+
+  /**
+   * @param use - can either be "id_token" or "userinfo", depending on
+   *   where the specific claims are intended to be put in.
+   * @param scope - the intended scope, while oidc-provider will mask
+   *   claims depending on the scope automatically you might want to skip
+   *   loading some claims from external resources etc. based on this detail
+   *   or not return them in id tokens but only userinfo and so on.
+   */
+  async claims(use, scope) { // eslint-disable-line no-unused-vars
+    if (this.profile) {
+      return {
+        sub: this.accountId, // it is essential to always return a sub claim
+        email: this.profile.email,
+        email_verified: this.profile.email_verified,
+        family_name: this.profile.family_name,
+        given_name: this.profile.given_name,
+        locale: this.profile.locale,
+        name: this.profile.name,
+      };
+    }
+
+    return {
+      sub: this.accountId, // it is essential to always return a sub claim
+
+      address: {
+        country: '000',
+        formatted: '000',
+        locality: '000',
+        postal_code: '000',
+        region: '000',
+        street_address: '000',
+      },
+      birthdate: '1987-10-16',
+      email: 'johndoe@example.com',
+      email_verified: false,
+      family_name: 'Doe',
+      gender: 'male',
+      given_name: 'John',
+      locale: 'en-US',
+      middle_name: 'Middle',
+      name: 'John Doe',
+      nickname: 'Johny',
+      phone_number: '+49 000 000000',
+      phone_number_verified: false,
+      picture: 'http://lorempixel.com/400/200/',
+      preferred_username: 'johnny',
+      profile: 'https://johnswebsite.com',
+      updated_at: 1454704946,
+      website: 'http://example.com',
+      zoneinfo: 'Europe/Berlin',
+    };
+  }
+
+  static async findByFederated(provider, claims) {
+    const id = `${provider}.${claims.sub}`;
+    if (!logins.get(id)) {
+      logins.set(id, new Account(id, claims));
+    }
+    return logins.get(id);
+  }
+
+  static async findByLogin(login) {
+    if (!logins.get(login)) {
+      logins.set(login, new Account(login));
+    }
+
+    return logins.get(login);
+  }
+
+  static async findAccount(ctx, id, token) { // eslint-disable-line no-unused-vars
+    // token is a reference to the token used for which a given account is being loaded,
+    //   it is undefined in scenarios where account claims are returned from authorization endpoint
+    // ctx is the koa request context
+    if (!store.get(id)) new Account(id); // eslint-disable-line no-new
+    return store.get(id);
+  }
+}
+
+module.exports = Account;

+ 80 - 0
server/support/configuration.js

@@ -0,0 +1,80 @@
+const { interactionPolicy: { Prompt, base: policy } } =  require('oidc-provider');
+
+// copies the default policy, already has login and consent prompt policies
+const interactions = policy();
+
+// create a requestable prompt with no implicit checks
+const selectAccount = new Prompt({
+  name: 'select_account',
+  requestable: true,
+});
+
+// add to index 0, order goes select_account > login > consent
+interactions.add(selectAccount, 0);
+
+module.exports = {
+  clients: [
+      {
+          client_id: 'seec-portal-1919810-114514',
+          response_types: ['id_token'],
+          grant_types: ['implicit'],
+          token_endpoint_auth_method: 'none',
+      },
+  ],
+  interactions: {
+     // policy: interactions,
+    url: (ctx, interaction) => { // eslint-disable-line no-unused-vars
+      console.log("redirect now!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+      return `/api/authenticate`;
+    },
+  },
+  cookies: {
+    long: { signed: true, maxAge: (1 * 24 * 60 * 60) * 1000}, // 1 day in ms
+    short: { signed: true , sameSite: 'none'},
+    keys: ['some secret key', 'and also the old rotated away some time ago', 'and one more'],
+  },
+  claims: {
+    address: ['address'],
+    email: ['email', 'email_verified'],
+    phone: ['phone_number', 'phone_number_verified'],
+    profile: ['birthdate', 'family_name', 'gender', 'given_name', 'locale', 'middle_name', 'name',
+      'nickname', 'picture', 'preferred_username', 'profile', 'updated_at', 'website', 'zoneinfo'],
+  },
+  features: {
+    devInteractions: { enabled: false }, // defaults to true
+
+    deviceFlow: { enabled: true }, // defaults to false
+    introspection: { enabled: true }, // defaults to false
+    revocation: { enabled: true }, // defaults to false
+  },
+  jwks: {
+    keys: [
+      {
+        d: 'VEZOsY07JTFzGTqv6cC2Y32vsfChind2I_TTuvV225_-0zrSej3XLRg8iE_u0-3GSgiGi4WImmTwmEgLo4Qp3uEcxCYbt4NMJC7fwT2i3dfRZjtZ4yJwFl0SIj8TgfQ8ptwZbFZUlcHGXZIr4nL8GXyQT0CK8wy4COfmymHrrUoyfZA154ql_OsoiupSUCRcKVvZj2JHL2KILsq_sh_l7g2dqAN8D7jYfJ58MkqlknBMa2-zi5I0-1JUOwztVNml_zGrp27UbEU60RqV3GHjoqwI6m01U7K0a8Q_SQAKYGqgepbAYOA-P4_TLl5KC4-WWBZu_rVfwgSENwWNEhw8oQ',
+        dp: 'E1Y-SN4bQqX7kP-bNgZ_gEv-pixJ5F_EGocHKfS56jtzRqQdTurrk4jIVpI-ZITA88lWAHxjD-OaoJUh9Jupd_lwD5Si80PyVxOMI2xaGQiF0lbKJfD38Sh8frRpgelZVaK_gm834B6SLfxKdNsP04DsJqGKktODF_fZeaGFPH0',
+        dq: 'F90JPxevQYOlAgEH0TUt1-3_hyxY6cfPRU2HQBaahyWrtCWpaOzenKZnvGFZdg-BuLVKjCchq3G_70OLE-XDP_ol0UTJmDTT-WyuJQdEMpt_WFF9yJGoeIu8yohfeLatU-67ukjghJ0s9CBzNE_LrGEV6Cup3FXywpSYZAV3iqc',
+        e: 'AQAB',
+        kty: 'RSA',
+        n: 'xwQ72P9z9OYshiQ-ntDYaPnnfwG6u9JAdLMZ5o0dmjlcyrvwQRdoFIKPnO65Q8mh6F_LDSxjxa2Yzo_wdjhbPZLjfUJXgCzm54cClXzT5twzo7lzoAfaJlkTsoZc2HFWqmcri0BuzmTFLZx2Q7wYBm0pXHmQKF0V-C1O6NWfd4mfBhbM-I1tHYSpAMgarSm22WDMDx-WWI7TEzy2QhaBVaENW9BKaKkJklocAZCxk18WhR0fckIGiWiSM5FcU1PY2jfGsTmX505Ub7P5Dz75Ygqrutd5tFrcqyPAtPTFDk8X1InxkkUwpP3nFU5o50DGhwQolGYKPGtQ-ZtmbOfcWQ',
+        p: '5wC6nY6Ev5FqcLPCqn9fC6R9KUuBej6NaAVOKW7GXiOJAq2WrileGKfMc9kIny20zW3uWkRLm-O-3Yzze1zFpxmqvsvCxZ5ERVZ6leiNXSu3tez71ZZwp0O9gys4knjrI-9w46l_vFuRtjL6XEeFfHEZFaNJpz-lcnb3w0okrbM',
+        q: '3I1qeEDslZFB8iNfpKAdWtz_Wzm6-jayT_V6aIvhvMj5mnU-Xpj75zLPQSGa9wunMlOoZW9w1wDO1FVuDhwzeOJaTm-Ds0MezeC4U6nVGyyDHb4CUA3ml2tzt4yLrqGYMT7XbADSvuWYADHw79OFjEi4T3s3tJymhaBvy1ulv8M',
+        qi: 'wSbXte9PcPtr788e713KHQ4waE26CzoXx-JNOgN0iqJMN6C4_XJEX-cSvCZDf4rh7xpXN6SGLVd5ibIyDJi7bbi5EQ5AXjazPbLBjRthcGXsIuZ3AtQyR0CEWNSdM7EyM5TRdyZQ9kftfz9nI03guW3iKKASETqX2vh0Z8XRjyU',
+        use: 'sig',
+      }, {
+        crv: 'P-256',
+        d: 'K9xfPv773dZR22TVUB80xouzdF7qCg5cWjPjkHyv7Ws',
+        kty: 'EC',
+        use: 'sig',
+        x: 'FWZ9rSkLt6Dx9E3pxLybhdM6xgR5obGsj5_pqmnz5J4',
+        y: '_n8G69C-A2Xl4xUW2lF0i8ZGZnk_KPYrhv4GbTGu5G4',
+      },
+    ],
+  },
+  ttl: {
+    AccessToken: 1 * 60 * 60, // 1 hour in seconds
+    AuthorizationCode: 10 * 60, // 10 minutes in seconds
+    IdToken: 1 * 60 * 60, // 1 hour in seconds
+    DeviceCode: 10 * 60, // 10 minutes in seconds
+    RefreshToken: 1 * 24 * 60 * 60, // 1 day in seconds
+  },
+};

+ 27 - 0
server/test/course.test.js

@@ -0,0 +1,27 @@
+const supertest = require('supertest');
+const chai = require('chai');
+const util      = require('util');
+const app = require('./../app');
+
+const expect = chai.expect;
+const request = supertest(app.listen());
+
+describe('开始测试课程功能', () => {
+    let response;
+    it('测试 /api/course/getHotCourses 请求', done => {
+        request
+            .get('/api/course/getHotCourses')
+            .send({})
+            .expect(200)
+            .end((err, res) => {
+                response = res;
+                expect(res.body).to.be.an('object');
+                expect(res.body.code).to.eql(0);
+                done()
+            })
+    })
+
+    afterEach(function(){
+            console.log("    Response body: " + util.inspect(response.body,{depth: null, colors: true}) + "\n");
+    })
+});

+ 67 - 0
server/test/user.test.js

@@ -0,0 +1,67 @@
+const supertest = require('supertest');
+const chai = require('chai');
+const util      = require('util');
+const app = require('./../app');
+
+const expect = chai.expect;
+const request = supertest(app.listen());
+
+describe('开始测试用户功能', () => {
+    let response;
+    it('测试 /api/user/register 请求', done => {
+        request
+            .post('/api/user/register')
+            .send({name: 'test', password: '123456', phone: "12345678912"})
+            .expect(200)
+            .end((err, res) => {
+                response = res;
+                expect(res.body).to.be.an('object');
+                expect(res.body.code).to.eql(-1);
+                expect(res.body.message).to.eql('此手机号已注册');
+                done()
+            })
+    })
+    it('测试 /api/user/login 请求,正常登陆情况', done => {
+        request
+            .post('/api/user/login')
+            .send({password: '123456', phone: "12345678912"})
+            .expect(200)
+            .end((err, res) => {
+                response = res;
+                expect(res.body).to.be.an('object');
+                expect(res.body.code).to.eql(0);
+                expect(res.body.data).to.be.an('object');
+                done()
+            });
+    })
+    it('测试 /api/user/login 请求,密码错误情况', done => {
+        request
+            .post('/api/user/login')
+            .send({password: '12345', phone: "12345678912"})
+            .expect(200)
+            .end((err, res) => {
+                response = res;
+                expect(res.body).to.be.an('object');
+                expect(res.body.code).to.eql(-1);
+                expect(res.body.message).to.eql('密码错误,请重试');
+                done()
+            });
+    })
+    it('测试 /api/user/login 请求,账户不存在情况', done => {
+        request
+            .post('/api/user/login')
+            .send({password: '123456', phone: "12345678913"})
+            .expect(200)
+            .end((err, res) => {
+                response = res;
+                expect(res.body).to.be.an('object');
+                expect(res.body.code).to.eql(-1);
+                expect(res.body.message).to.eql('此账号不存在');
+                done()
+            });
+    })
+    //
+    // afterEach(function(){
+    //         console.log("    Response body: " + util.inspect(response.body,{depth: null, colors: true}) + "\n");
+    // })
+});

+ 13 - 0
server/util/request.js

@@ -0,0 +1,13 @@
+const request = require('request');
+const util = require('util');
+const getPromise = util.promisify(request.get);
+
+module.exports = async function (url, data) {
+    let result = await getPromise({url:url, headers: {
+            'Authorization': 'Basic cm9vdDpOSlU2N3NvZnR3YXJl'
+        }
+    });
+    // 可以加入 try catch 捕获异常  也可以加 .catch()
+    console.log("result" , result);
+    return result;
+};

+ 11 - 0
server/util/response.js

@@ -0,0 +1,11 @@
+class Response {
+    static success(data){
+        return {code: 0, data: data};
+    }
+
+    static failed(message){
+        return {code: -1, message: message};
+    }
+}
+
+module.exports = Response;

+ 44 - 0
server/util/time-processer.js

@@ -0,0 +1,44 @@
+module.exports = function getNowFormatDate() {
+    let date = new Date();
+
+    let year = date.getFullYear();        //年 ,从 Date 对象以四位数字返回年份
+    let month = date.getMonth() + 1;      //月 ,从 Date 对象返回月份 (0 ~ 11) ,date.getMonth()比实际月份少 1 个月
+    let day = date.getDate();             //日 ,从 Date 对象返回一个月中的某一天 (1 ~ 31)
+
+    let hours = date.getHours();          //小时 ,返回 Date 对象的小时 (0 ~ 23)
+    let minutes = date.getMinutes();      //分钟 ,返回 Date 对象的分钟 (0 ~ 59)
+    let seconds = date.getSeconds();      //秒 ,返回 Date 对象的秒数 (0 ~ 59)
+
+
+//修改月份格式
+    if (month >= 1 && month <= 9) {
+        month = "0" + month;
+    }
+
+//修改日期格式
+    if (day >= 0 && day <= 9) {
+        day = "0" + day;
+    }
+
+//修改小时格式
+    if (hours >= 0 && hours <= 9) {
+        hours = "0" + hours;
+    }
+
+//修改分钟格式
+    if (minutes >= 0 && minutes <= 9) {
+        minutes = "0" + minutes;
+    }
+
+//修改秒格式
+    if (seconds >= 0 && seconds <= 9) {
+        seconds = "0" + seconds;
+    }
+
+//获取当前系统时间  格式(yyyy-mm-dd hh:mm:ss)
+    let currentFormatDate = year + "-" + month + "-" + day + " " + hours + ":" + minutes + ":" + seconds;
+
+    return currentFormatDate;
+}
+
+

Datei-Diff unterdrückt, da er zu groß ist
+ 523 - 31
yarn.lock


Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.