var utils = require('./src/utils'); var crossCtl = require('./src/crossCtl'); var compression = require('compression'); var express = require('express'); // var cors = require('cors') var path = require('path'); var morgan = require('morgan'); var rfs = require('rotating-file-stream'); // version 2.x var favicon = require('serve-favicon'); var cookieParser = require('cookie-parser'); var bodyParser = require('body-parser'); var passport = require('passport'); var LocalStrategy = require('passport-local').Strategy; var FacebookStrategy = require('passport-facebook').Strategy; var GoogleStrategy = require('passport-google-oauth20').Strategy; var KakaoStrategy = require('passport-kakao').Strategy; var AppleStrategy = require('passport-apple'); var CustomStrategy = require('passport-custom').Strategy; var mysql = require('mysql'); var moment = require('moment'); var app = express(); app.use(express.json({ limit: '50mb' })); app.use(express.urlencoded({ limit: '50mb', extended: false })); app.set('trust proxy', true); var session = require('express-session'); var RedisStore = require('connect-redis')(session); var sessionDomain = 'safekiso.com'; // // console.log('huk sessionDomain = ', sessionDomain) var redisOptions = {}; if (crossCtl.sConfig.redis) { redisOptions = { ...crossCtl.config.options.redis, ...crossCtl.sConfig.redis, }; } else { redisOptions = { ...crossCtl.config.options.redis }; } // console.log('huk redisOptions = ', redisOptions) var cookieOptions = {}; if (crossCtl.sConfig.cookie) { cookieOptions = crossCtl.sConfig.cookie; } else { cookieOptions = { path: '/', // sameSite: 'strict', // domain: sessionDomain, // !!!!!!! // maxAge: 1000 * 60 * 60 * 24 // 24 hours maxAge: 1000 * 60 * 60 * 1, // 1 hours }; } // console.log('huk cookieOptions = ', cookieOptions) // redirect HTTP to HTTPS app.all('*', (req, res, next) => { let protocol = req.headers['x-forwarded-proto'] || req.protocol; // console.log('protocol=', protocol) if (protocol == 'https') { next(); } else { if (crossCtl.sConfig.forceHttps === true) { let from = `${protocol}://${req.hostname}${req.url}`; let to = `https://${req.hostname}${req.url}`; // log and redirect console.log(`[${req.method}]: ${from} -> ${to}`); res.redirect(to); } else { next(); } } }); app.use( session({ store: new RedisStore(redisOptions), secret: 'in got we trust!!!', resave: true, saveUninitialized: true, // ttl: 60 * 1, rolling: true, cookie: cookieOptions, }) ); app.use(function (req, res, next) { const cleanup = () => { res.removeListener('finish', logStatsics); res.removeListener('close', abortFn); res.removeListener('error', errorFn); }; const logStatsics = () => { cleanup(); // console.log('logStatsics()') // console.info(`${req.method} ${req.originalUrl}`) // console.info(`${req.sessionID}`) // crossCtl.redis.logHit(req.originalUrl) if (req.sessionID) { crossCtl.redis.logSession(req.sessionID); } else { // console.log('no session request!!!') } }; const abortFn = () => { console.warn('Request aborted by the client'); cleanup(); }; const errorFn = (err) => { console.error(`Request pipeline error: ${err}`); cleanup(); }; res.on('finish', logStatsics); // successful pipeline (regardless of its response) res.on('close', abortFn); // aborted pipeline res.on('error', errorFn); // pipeline internal error next(); }); app.use(compression()); // app.use(cors()) // view engine setup app.set('view engine', 'ejs'); // var i18n = require('i18n') crossCtl.i18n.configure({ locales: ['ko', 'en'], defaultLocale: 'ko', autoReload: true, // watch for changes in json files to reload locale on updates - defaults to false updateFiles: true, // whether to write new locale information to disk - defaults to true syncFiles: true, // sync locale information across all files - defaults to false directory: path.join(__dirname, 'modules', crossCtl.sConfig.type, 'locales'), }); // i18n.setLocale('ko'); app.use(crossCtl.i18n.init); // uncomment after placing your favicon in /public var morganOptions = { skip: function (req, res) { return req.url === '/ping'; }, }; // app.use(logger('dev', morganOptions)) // morgan // create a rotating write stream var accessLogStream = rfs.createStream(utils.generator, { // interval: '1M', // rotate monthly // interval: '1m', // rotate minitly // immutable: true, // rotate: 1, // size: "30K", interval: '1M', // rotate minitly path: path.join(__dirname, crossCtl.sConfig.log), }); accessLogStream.on('open', (filename) => { // console.log('accessLogStream on open with ', filename) }); accessLogStream.on('rotation', () => { // console.log('accessLogStream on rotation') }); accessLogStream.on('rotated', (filename) => { // console.log('accessLogStream on rotated to ', filename) }); accessLogStream.on('warning', (error) => { // console.log('accessLogStream on warning with ', error ) }); accessLogStream.on('error', (error) => { // console.log('accessLogStream on error with ', error ) }); // setup the logger // morgan.token('trueIp', req => utils.getIPFromReq(req)); /* morgan.token('remote-addr', function (req) { return utils.getIPFromReq(req) }); */ app.use(morgan('combined', { stream: accessLogStream })); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); // app.use(bodyParser.json({limit: '10mb', extended: true})) // app.use(bodyParser.urlencoded({limit: '10mb', extended: true})) app.use(cookieParser()); app.set('views', [ path.join(__dirname, 'modules', 'base', 'views'), path.join(__dirname, 'modules', crossCtl.sConfig.type, 'views'), ]); // app.set("views", path.join(__dirname, "modules", "base", "views")); // app.set("views",path.join(__dirname, "modules", crossCtl.sConfig.type, "views")); /* app.use( favicon( path.join( __dirname, 'modules', crossCtl.sConfig.type, 'wwwroot', 'favicon.ico' ) ) ); */ // if (crossCtl.sConfig.clientDistInfo != undefined) { app.use( express.static( path.join( __dirname, 'modules', crossCtl.sConfig.type, 'client', // crossCtl.sConfig.clientDistInfo.fromPath, '.output', 'public' ) ) ); } app.use( express.static( path.join(__dirname, 'modules', crossCtl.sConfig.type, 'wwwroot') ) ); app.use(express.static(path.join(__dirname, 'modules', 'base', 'wwwroot'))); app.use( express.static(path.join(__dirname, 'modules', 'base', 'wwwroot', 'admindek')) ); const fs = require('fs'); /* if (crossCtl.sConfig.type === 'inspond') { var wwwrootPath = path.join(__dirname, '..', 'wwwroot'); // console.log('wwwrootPath=', wwwrootPath) if (fs.existsSync(wwwrootPath)) { app.use(express.static(wwwrootPath)); } } */ /* crossCtl.text2png.generatePngFromText('졸리\n만세!', {}, 'modules/linkcare/wwwroot/out.png', (err) => { console.log('after generatePngFromText(), err=', err) }) */ app.use(passport.initialize()); app.use(passport.session()); var passportOptions = crossCtl.sConfig.passport; // crossCtl.config.options.passport function doAuth(type, key, token, info, cb) { // var qry = 'SELECT * FROM ' + crossCtl.db.options.database + '.tbl_account_users WHERE auth_type = ' + type + ' AND auth_key = ' + mysql.escape(key) + ' AND status = 0' var qry = 'SELECT * FROM ' + crossCtl.db.options.database + '.tbl_account_users WHERE auth_type = ' + type + ' AND auth_key = ' + mysql.escape(key) + ' AND site_id = ' + mysql.escape(crossCtl.sConfig.type); if (crossCtl.sConfig.type == 'usm') { var roleTag = info.roleTag ? info.roleTag : 'student'; qry = 'SELECT * FROM ' + crossCtl.db.options.database + '.tbl_account_users WHERE auth_type = ' + type + ' AND auth_key = ' + mysql.escape(key) + ' AND site_id = ' + mysql.escape(crossCtl.sConfig.type) + ' AND role_tag = ' + mysql.escape(roleTag); } var prefDataJson = {}; var resultUser = { auth_flag: false, auth_type: type, reason: 'undefined', code: 0, }; crossCtl.db.doEasyQuery(qry, function (err, results) { if (err) { cb(err, null); } else { if (results.length === 0) { if (type === 0) { // local인 경우 로그인 못한다고 하세요. resultUser.reason = 'no user found'; resultUser.code = -1; cb(new Error(resultUser.reason), resultUser); // crossCtl.logUserAction('anonym', '404', 'bad login id, type = ' + type + ', key = ' + key + ', token = ' + token) } else { // 외부인 경우 레코드를 생성하고 리턴 합니다. prefDataJson.account_info = info; // console.log('info =', info) // console.log('info.displayName =', info.displayName) var post = { site_id: crossCtl.sConfig.type, name: info.name, auth_type: type, auth_key: key, auth_token: token, pref_data: JSON.stringify(prefDataJson), }; qry = 'INSERT INTO ' + crossCtl.db.options.database + '.tbl_account_users SET ? '; crossCtl.db.doEasyQueryPost(qry, post, function (err, results) { if (err) { resultUser.reason = err.message; resultUser.code = utils.__line(); cb(err, resultUser); } else { var serial = results.insertId; var uid = utils.uuid('uid_' + serial); crossCtl.logUserAction(uid, 'new', JSON.stringify(info)); qry = 'UPDATE ' + crossCtl.db.options.database + '.tbl_account_users SET uid = ' + mysql.escape(uid) + ' WHERE serial = ' + serial; crossCtl.db.doEasyQuery(qry, function (err, results) { if (err) { cb(err, null); } else { var user = { site_id: crossCtl.sConfig.type, uid: uid, name: post.name, auth_flag: true, user_info: info, account_info: post, prefDataJson: prefDataJson, newAccountFlag: true, created: moment.utc().format(), }; crossCtl.passEventToLocalHandler( 'addUser', user, function (error) { if (error) { utils.log( 'error', 'on crossCtl.passEventToLocalHandler(), error =', error ); } if (crossCtl.sConfig.allowSignup === false) { resultUser.reason = 'no signup allowded'; resultUser.code = -10; cb(new Error(resultUser.reason), resultUser); } else { crossCtl.loadProfile(user, cb); // cb(error, user) } } ); // crossCtl.logUserAction(uid, 'login', 'login ok') } }); } }); } } else if (results.length > 1) { resultUser.reason = 'too many target'; resultUser.code = utils.__line(); cb(new Error('too many target account found'), resultUser); } else { // 1개의 레코드가 있군요. 비교하세요. if (type === 0) { if (results[0].status !== 0) { resultUser.reason = 'bad status : ' + results[0].status; resultUser.code = -2; cb(new Error(resultUser.reason), resultUser); } else if (results[0].bad_pass_count >= 5) { resultUser.reason = 'wrong password count limit exceeded : ' + results[0].bad_pass_count; resultUser.code = -2; cb(new Error(resultUser.reason), resultUser); } else { utils.compareHash( token, results[0].auth_token, function (err, result) { if (err) { resultUser.reason = err.message; resultUser.code = utils.__line(); cb(err, resultUser); } else { if (result === false) { resultUser.reason = 'bad password'; resultUser.code = -2; cb(new Error(resultUser.reason), resultUser); qry = 'UPDATE ' + crossCtl.db.options.database + '.tbl_account_users SET bad_pass_count = bad_pass_count + 1 WHERE auth_type = ' + type + ' AND auth_key = ' + mysql.escape(key) + ' AND site_id = ' + mysql.escape(crossCtl.sConfig.type); crossCtl.db.doEasyQuery(qry, function (err, results) {}); } else { var prefDataJson = utils.safeJSON(results[0].pref_data); qry = 'UPDATE ' + crossCtl.db.options.database + '.tbl_account_users SET bad_pass_count = 0 WHERE auth_type = ' + type + ' AND auth_key = ' + mysql.escape(key) + ' AND site_id = ' + mysql.escape(crossCtl.sConfig.type); crossCtl.db.doEasyQuery(qry, function (err, results) {}); if (prefDataJson.newAccountFlag === true) { // console.log('huk new user!!!') prefDataJson.newAccountFlag = undefined; var newPrefDataJson = utils.safeJSON( JSON.stringify(prefDataJson) ); crossCtl.saveUserPrefData( results[0].uid, newPrefDataJson, function (error, json) { var user = { site_id: crossCtl.sConfig.type, uid: results[0].uid, name: results[0].name, auth_flag: true, user_info: json.account_info, account_info: results[0], prefDataJson: json, newAccountFlag: true, created: results[0].created, }; crossCtl.loadProfile(user, cb); //cb(null, user) } ); } else { var user = { site_id: crossCtl.sConfig.type, uid: results[0].uid, name: results[0].name, auth_flag: true, user_info: prefDataJson.account_info, account_info: results[0], prefDataJson: prefDataJson, created: results[0].created, }; crossCtl.loadProfile(user, cb); //cb(null, user) } // crossCtl.logUserAction(results[0].uid, 'login', 'login ok') } } } ); } } else { // 그냥 업데이트 해 주고 다음으로 이동... prefDataJson = utils.safeJSON(results[0].pref_data); if (type == 4) { info = prefDataJson.account_info; } else { prefDataJson.account_info = info; } if (token === results[0].auth_token) { // utils.log('external auth token match!') } else { // utils.log('external auth token not match!') // utils.log('token = ' + token) // utils.log('results[0].auth_token = ' + results[0].auth_token) qry = 'UPDATE ' + crossCtl.db.options.database + '.tbl_account_users SET auth_token = ' + mysql.escape(token) + ', pref_data = ' + mysql.escape(JSON.stringify(prefDataJson)) + ' WHERE serial = ' + results[0].serial; crossCtl.db.doEasyQuery(qry, function (err, results) { if (err) { utils.log('error', 'doAuth(), update error. ' + err); } }); } var user = { site_id: crossCtl.sConfig.type, uid: results[0].uid, name: results[0].name, auth_flag: true, user_info: info, account_info: results[0], prefDataJson: prefDataJson, created: results[0].created, }; crossCtl.loadProfile(user, cb); // cb(null, user) // crossCtl.logUserAction(results[0].uid, 'login', 'login ok') } } } }); } passport.use( new LocalStrategy(passportOptions.local, function ( req, email, password, done ) { var normalizeUserInfo = crossCtl.normalizeUserInfo('id/password', { email, password, }); console.log('normalizeUserInfo=', normalizeUserInfo); doAuth(0, email, password, normalizeUserInfo, function (error, user) { if (error) { utils.log('error', 'doAuth for local error. ' + error); } return done(error, user); }); }) ); passport.use( 'api-login', new CustomStrategy(function (req, done) { if (req.body.info == undefined) { req.body.info = {}; } var type = parseInt(req.body.type); var id = req.body.id; var token = req.body.token; var profile = req.body.info['provider'] != undefined ? req.body.info : utils.safeJSON(req.body.info); doAuth(type, id, token, profile, function (error, user) { if (error) { utils.log('error', 'doAuth for local error. ' + error); } return done(error, user); }); }) ); var request = require('request'); passport.serializeUser(function (user, done) { done(null, user); }); passport.deserializeUser(function (user, done) { done(null, user); }); var addRequestId = require('express-request-id')(); app.use(addRequestId); app.use(function (req, res, next) { if (req.url !== '/ping') { req.workTag = utils.getWorkTag(req, res); req.workTag.responsePacket = { header: crossCtl.config.header.responseTag, responseCode: 0, responseMessage: 'ok', }; } next(); }); app.use(function (req, res, next) { req.uid = req.isAuthenticated() ? req.user.uid : utils.uuid('uid_' + req.sessionID); var userNick = ''; var userName = ''; var userEmail = ''; var provider = 'anoymous'; // facebook, google, anoymous var adminFlag = false; var profileUrl = ''; var messageCount = 0; if (req.isAuthenticated() && req.user.auth_flag) { var params = req.user; // console.log('req.user=', req.user) adminFlag = params.account_info.user_level === 5 ? true : false; if (params.user_info == undefined) { params.user_info = {}; } provider = params.user_info.provider ? params.user_info.provider : 'id/password'; // facebook, google if (params.user_info.provider === 'facebook') { userNick = 'f ' + params.user_info.name; userName = '' + params.user_info.name; userEmail = ''; } else if (params.user_info.provider === 'google') { userNick = 'g+ ' + params.account_info.name; userName = '' + params.account_info.name; userEmail = params.user_info.email; profileUrl = params.user_info.photo; } else if (params.user_info.provider === 'kakao') { userNick = 'k ' + params.user_info.name; userName = '' + params.user_info.name; userEmail = '' + params.user_info.email; profileUrl = params.user_info.photo; } else { userNick = params.user_info.name; userName = params.user_info.name; userEmail = params.user_info.name; } } else { userNick = 'anonym'; userName = 'anonym'; userEmail = ''; provider = 'anonymous'; } var rawLocAry = req.originalUrl.toLowerCase().split('?'); var rawLoc = req.originalUrl.toLowerCase(); var loc = rawLocAry[0]; if (rawLoc.endsWith('/') && rawLoc !== '/') { rawLoc = rawLoc.substring(0, rawLoc.length - 1); } if (loc.endsWith('/') && loc !== '/') { loc = loc.substring(0, loc.length - 1); } var query = req.query; var finalLang = 'ko'; finalLang = req.session ? (req.session.lang ? req.session.lang : 'ko') : 'ko'; finalLang = req.query.lang ? req.query.lang : finalLang; var profile = req.isAuthenticated() ? req.user.profile ? req.user.profile : crossCtl.makeEmptyProfile() : crossCtl.makeEmptyProfile(); // req.infos.userInfo.userNick req.infos = { appInfo: { name: crossCtl.sConfig.name, type: crossCtl.sConfig.type, wsUrl: crossCtl.sConfig.wsUrl, }, hostName: utils.hostName, fullUrl: req.protocol + '://' + req.get('host') + req.originalUrl.toLowerCase(), rawLoc: rawLoc, loc: loc, query: query, lang: finalLang, userInfo: { loginFlag: req.isAuthenticated() && req.user.auth_flag, userNick: userNick, userName: profile.display_name, profileUrl: profile.photo_url, messageCount: messageCount, adminFlag: adminFlag, provider: provider, fcmTokenFlag: req.session.fcmInfo == undefined ? false : true, profile: profile, }, localPath: '../../../../' + crossCtl.sConfig.type + '/views/' + crossCtl.sConfig.type, basePath: '../../../../base/views/main', }; // console.log('req.infos=', req.infos) // req.setLocale('ko'); // console.log('req.getLocale()=', req.getLocale()) crossCtl.i18n.setLocale(req, finalLang); var ip = utils.getIPFromReq(req); // console.log('ip=', ip) // console.log('req.headers=', req.headers) if (req.headers['x-forwarded-for']) { if (req.headers['x-forwarded-by'] === 'bouncy') { if (req.session) { req.session.bouncyIP = ip; } } else { if (req.session) { req.session.bouncyIP = undefined; } } } /* if (req.session.bouncyIP !== undefined) { req.session.bouncyIP = undefined } */ var ip = utils.getIPFromReq(req); next(); }); app.use(function (req, res, next) { var err = null; try { decodeURIComponent(req.path); } catch (e) { err = e; } if (err) { // console.log(err, req.url); utils.log( 'error', 'URI malformed. url = ' + req.url + ', req = ' + utils.dumpReq(req) ); if (req.infos.loc.startsWith('/api/')) { return req.workTag .res() .status(500) .send({ ...req.workTag.responsePacket, responseCode: 500, responseMessage: 'Server error : URI malformed, make sure that your filename have no % character', }); } else { return res.status(404).render('main/pages/next', { infos: req.infos, bigTitle: 404, smallTitle: 'URI malformed, make sure that your filename have no % character', nextTitle: 'Goto home', nextUrl: '/', }); } } next(); }); app.use(function (req, res, next) { res.header('Access-Control-Allow-Credentials', true); // res.header('Access-Control-Allow-Origin', req.headers.origin) res.header('Access-Control-Allow-Origin', req.headers.origin || '*'); // res.header('Access-Control-Allow-Origin', '*') res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS'); res.header( 'Access-Control-Allow-Headers', 'Origin, x-api-key, x-access-token, X-Requested-With, X-HTTP-Method-Override, Authorization, Content-Length, Content-Type, Accept, Cache-Control, DomainKey' ); // intercepts OPTIONS method if (req.method === 'OPTIONS') { // respond with 200 res.sendStatus(200); } else { // move on next(); } }); app.use(function (req, res, next) { var maintenanceInfo = crossCtl.getMaintenanceInfo(); if (maintenanceInfo !== null) { var ip = utils.getIPFromReq(req); if (crossCtl.isItStaffIP(ip)) { next(); } else { if (req.infos.loc.startsWith('/api/')) { req.workTag .res() .status(503) .send({ ...req.workTag.responsePacket, responseCode: 503, responseMessage: 'Service Temporarily Unavailiable', }); } else { res.status(503).render('main/pages/next', { infos: req.infos, bigTitle: '점검중', smallTitle: '준비되는대로 다시 찾아와 주세요.', nextUrl: 'javascript:alert("꼭이요! ^o^/~")', nextTitle: '확인', maintenanceInfo: maintenanceInfo, }); } } } else { next(); } }); app.options('/*', function (req, res, next) { res.header('Access-Control-Allow-Credentials', true); //res.header("Access-Control-Allow-Origin", req.get("Origin")); res.header('Access-Control-Allow-Origin', req.get('Origin') || '*'); // res.header("Access-Control-Allow-Origin", req.headers.origin || "*"); // res.header('Access-Control-Allow-Origin', '*') res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS'); res.header( 'Access-Control-Allow-Headers', 'Origin, x-api-key, x-access-token, X-Requested-With, X-HTTP-Method-Override, Authorization, Content-Length, Content-Type, Accept, Cache-Control, DomainKey' ); res.send(200); }); app.use( '/api/v1', require('./modules/' + crossCtl.sConfig.type + '/routes/api/v1') ); app.use( '/api', require('./modules/' + crossCtl.sConfig.type + '/routes/api/index') ); app.use('/api', require('./modules/base/routes/api')); app.use( '/admin', require('./modules/' + crossCtl.sConfig.type + '/routes/admin') ); app.use('/admin', require('./modules/base/routes/admin')); app.use('/ck', require('./modules/base/routes/ck')); app.use('/', require('./modules/base/routes/main')); app.use('/', require('./modules/' + crossCtl.sConfig.type + '/routes/main')); // catch 404 and forward to error handler app.use(function (req, res, next) { // var err = new Error('Not Found') // err.status = 404 // next(err) if (req.infos.loc.startsWith('/api/')) { req.workTag .res() .status(404) .send({ ...req.workTag.responsePacket, responseCode: 404, responseMessage: 'api not found', }); } else { res.status(404).render('main/pages/next', { infos: req.infos, bigTitle: 404, smallTitle: 'Page not found', nextTitle: 'Goto home', nextUrl: '/', }); } }); app.use(function (err, req, res, next) { utils.log('error', 'app trap error ' + err.stack); utils.log('error', 'exit process after 3 sec '); crossCtl.boom.flushAll(function (count) { console.log(count, 'boom flushed...'); setTimeout(function () { process.exit(1); }, 1000); }); }); process.on('uncaughtException', function (err) { utils.log('error', 'uncaughtException, error ' + err.stack); utils.log('error', 'exit process after 3 sec '); crossCtl.boom.flushAll(function (count) { console.log(count, 'boom flushed...'); setTimeout(function () { process.exit(1); }, 1000); }); }); process.on('SIGINT', function () { console.log('Caught interrupt signal'); utils.log('error', 'exit process after 3 sec '); crossCtl.boom.flushAll(function (count) { console.log(count, 'boom flushed...'); setTimeout(function () { process.exit(1); }, 1000); }); }); process.on('SIGTERM', function () { console.log('Caught term signal'); utils.log('error', 'exit process after 3 sec '); crossCtl.boom.flushAll(function (count) { console.log(count, 'boom flushed...'); setTimeout(function () { process.exit(1); }, 1000); }); }); // error handler /* app.use(function (err, req, res, next) { // set locals, only providing error in development res.locals.message = err.message res.locals.error = req.app.get('env') === 'development' ? err : {} // render the error page res.status(err.status || 500) res.render('error') }) */ module.exports = app;