소스 검색

feat: 新增登录日志功能

lanjianrong 4 년 전
부모
커밋
62f564a6b2
6개의 변경된 파일200개의 추가작업 그리고 1개의 파일을 삭제
  1. 6 0
      app/const/setting.js
  2. 5 0
      app/controller/profile_controller.js
  3. 132 0
      app/service/login_logging.js
  4. 7 1
      app/service/project_account.js
  5. 49 0
      app/view/profile/safe.ejs
  6. 1 0
      package.json

+ 6 - 0
app/const/setting.js

@@ -23,6 +23,11 @@ const listPath = [
     { label_name: '金额概况', path: '/list/info', is_default: false }, // 金额概况
 ];
 
+// 登录方式
+const loginWay = {
+    normalPsw: 0, // 正常登录
+    extraPsw: 1, // 副密码登录
+};
 
 module.exports = {
     cType: {
@@ -30,4 +35,5 @@ module.exports = {
         text: cTypeStr,
     },
     listPath,
+    loginWay,
 };

+ 5 - 0
app/controller/profile_controller.js

@@ -14,6 +14,7 @@ const smsTypeConst = require('../const/sms_type');
 const qr = require('qr-image');
 const path = require('path');
 const sendToWormhole = require('stream-wormhole');
+const loginWay = require('../const/setting').loginWay;
 
 module.exports = app => {
 
@@ -359,9 +360,13 @@ module.exports = app => {
             const passwordRule = ctx.service.projectAccount.rule('modifyPassword');
             const passwordJsValidator = await this.jsValidator.convert(passwordRule).setSelector('#password-form').build();
 
+            // 获取登录日志
+            const loginLogging = await ctx.service.loginLogging.getLoginLogs(ctx.session.sessionProject.id, ctx.session.sessionUser.accountId);
             const renderData = {
                 accountData,
                 passwordJsValidator,
+                loginLogging,
+                loginWay,
             };
             await this.layout('profile/safe.ejs', renderData);
         }

+ 132 - 0
app/service/login_logging.js

@@ -0,0 +1,132 @@
+'use strict';
+
+/**
+ * 登录日志-数据模型
+ *
+ * @author lanjianrong
+ * @date 2020/8/31
+ * @version
+ */
+const UAParser = require('ua-parser-js');
+
+module.exports = app => {
+    class LoginLogging extends app.BaseService {
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'login_logging';
+        }
+
+        /**
+         * 创建记录
+         * @param {Object} payload - 载荷
+         */
+        async createLog(payload) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                transaction.insert(this.tableName, payload);
+                await transaction.commit();
+            } catch (error) {
+                await transaction.rollback();
+                throw error;
+            }
+        }
+
+        /**
+         * 创建登录日志
+         * @return {Boolean} 日志是否创建成功
+         */
+        async addLoginLog() {
+            const { ctx } = this;
+            const ip = ctx.header['x-real-ip'] ? ctx.header['x-real-ip'] : '';
+            const ipInfo = await this.getIpInfoFromApi(ip);
+            const parser = new UAParser(ctx.header['user-agent']);
+            const osInfo = parser.getOS();
+            const cpuInfo = parser.getCPU();
+            const browserInfo = parser.getBrowser();
+            const payload = {
+                os: `${osInfo.name} ${osInfo.version} ${cpuInfo.architecture}`,
+                browser: `${browserInfo.name} ${browserInfo.version}`,
+                ip,
+                address: ipInfo,
+                uid: ctx.session.sessionUser.accountId,
+                pid: ctx.session.sessionProject.id,
+            };
+            return await this.createLog(payload);
+        }
+
+        /**
+         * 根据ip请求获取详细地址
+         * @param {String} a_ip - ip地址
+         * @return {String} 详细地址
+         */
+        async getIpInfoFromApi(a_ip = '') {
+            if (!a_ip) return '';
+            if (a_ip === '127.0.0.1') return '服务器本机访问';
+            const { ip = '', region = '', city = '', isp = '' } = await this.sendRequest(a_ip);
+            let address = '';
+            region && (address += region + '省');
+            city && (address += city + '市 ');
+            isp && (address += isp + ' ');
+            ip && (address += `(${ip})`);
+            return address;
+        }
+
+        /**
+         * 发送请求获取详细地址
+         * @param {String} ip - ip地址
+         * @param {Number} max - 最大重试次数
+         * @return {Object} the result of request
+         * @private
+         */
+        async sendRequest(ip, max = 3) {
+            return new Promise(resolve => {
+                const start = () => {
+                    if (max <= 0) {
+                        resolve(); // 已达到最大重试次数,返回空的执行承若
+                    }
+                    max--;
+                    this.ctx.curl(`https://api01.aliyun.venuscn.com/ip?ip=${ip}`, {
+                        dateType: 'json',
+                        encoding: 'utf8',
+                        timeout: 2000,
+                        headers: {
+                            Authorization: 'APPCODE 85c64bffe70445c4af9df7ae31c7bfcc',
+                        },
+                    }).then(({ status, data }) => {
+                        if (status === 200) {
+                            const result = JSON.parse(data.toString()).data;
+                            if (!result.ip) {
+                                start();
+                            } else {
+                                max++;
+                                resolve(result);
+                            }
+                        } else {
+                            max--;
+                            start();
+                        }
+                    }).catch(() => {
+                        start();
+                    });
+                };
+                start();
+            });
+        }
+
+        /**
+         * 获取登录日志
+         * @param {Number} pid - 项目id
+         * @param {Number} uid - 用户id
+         * @return {Promise<Array>} 日志数组
+         */
+        async getLoginLogs(pid, uid) {
+            return this.db.select(this.tableName, {
+                where: { pid, uid },
+                orders: [['create_time', 'desc']],
+                columns: ['browser', 'create_time', 'ip', 'os', 'address'],
+                limit: 10, offset: 0,
+            });
+        }
+    }
+    return LoginLogging;
+};

+ 7 - 1
app/service/project_account.js

@@ -14,6 +14,7 @@ const SSO = require('../lib/sso');
 const SMS = require('../lib/sms');
 const SmsAliConst = require('../const/sms_alitemplate');
 const thirdPartyConst = require('../const/third_party');
+const loginWay = require('../const/setting').loginWay;
 
 module.exports = app => {
 
@@ -235,13 +236,18 @@ module.exports = app => {
                         projectInfo.gxby_status = thirdParty.gxby_option && thirdParty.gxby_option.status
                             ? thirdParty.gxby_option.status : thirdPartyConst.gxby;
 
-                        thirdParty.dagl_option = thirdParty.dagl_option ? JSON.parse(thirdParty.dagl_option): null;
+                        thirdParty.dagl_option = thirdParty.dagl_option ? JSON.parse(thirdParty.dagl_option) : null;
                         projectInfo.dagl = thirdParty.dagl;
                         projectInfo.dagl_status = thirdParty.dagl_option && thirdParty.dagl_option.status
                             ? thirdParty.dagl_option.status : thirdPartyConst.dagl;
                     }
                     this.ctx.session.sessionProject = projectInfo;
                     this.ctx.session.sessionProjectList = projectList;
+                    if (loginStatus === loginWay.normalPsw) {
+                        // 正常登录-记录登录日志
+                        await this.ctx.service.loginLogging.addLoginLog();
+                    }
+
                 }
             } catch (error) {
                 console.log(error);

+ 49 - 0
app/view/profile/safe.ejs

@@ -22,6 +22,55 @@
                                 <p>SSO用户请到<a href="#">此处</a>修改密码</p>
                             <% } %>
                         </form>
+                        <!-- 访问日志 -->
+                        <% if(ctx.session.sessionUser.loginStatus === loginWay.normalPsw) { %>
+                            <div class="col-12 mt-5">
+                                <h4>访问日志</h4>
+                                <table class="table table-hover">
+                                    <thead>
+                                        <tr>
+                                            <th></th>
+                                            <th>系统</th>
+                                            <th>浏览器</th>
+                                            <th>登录时间</th>
+                                            <th>登录地址</th>
+                                        </tr>
+                                    </thead>
+                                    <tbody>
+                                        <% loginLogging.forEach((item, idx) => { %>
+                                            <tr>
+                                                <td><%- idx + 1 %></td>
+                                                <td><%- item.os %></td>
+                                                <td><%- item.browser %></td>
+                                                <td><%- ctx.helper.formatFullDate(item.create_time) %></td>
+                                                <td><%- item.address %></td>
+                                            </tr>
+                                        <% }) %>
+                                        <!-- <tr>
+                                            <td>1</td>
+                                            <td>Windows NT 10.0 64-bit</td>
+                                            <td>Chrome 55.0.2883.87 m (64-bit)</td>
+                                            <td>2017-01-12 09:09</td>
+                                            <td>广东省珠海市 电信(116.19.86.133)</td>
+                                        </tr>
+                                        <tr>
+                                            <td>2</td>
+                                            <td>Windows NT 10.0 64-bit</td>
+                                            <td>Chrome 55.0.2883.87 m (64-bit)</td>
+                                            <td>2017-01-12 09:09</td>
+                                            <td>广东省珠海市 电信(116.19.86.133)</td>
+                                        </tr>
+                                        <tr>
+                                            <td>3</td>
+                                            <td>Windows NT 10.0 64-bit</td>
+                                            <td>Chrome 55.0.2883.87 m (64-bit)</td>
+                                            <td>2017-01-12 09:09</td>
+                                            <td>广东省珠海市 电信(116.19.86.133)</td>
+                                        </tr> -->
+                                    </tbody>
+                                </table>
+                            </div>
+                        <% } %>
                     </div>
                 </div>
             </div>

+ 1 - 0
package.json

@@ -37,6 +37,7 @@
     "qr-image": "^3.2.0",
     "stream-to-array": "^2.3.0",
     "stream-wormhole": "^1.1.0",
+    "ua-parser-js": "^0.7.21",
     "ueditor": "^1.2.3",
     "uglify-es": "^3.3.9",
     "uglify-js": "^3.3.27",