WWebSso.mjs

import path from 'path'
import fs from 'fs'
import get from 'lodash-es/get.js'
import iseobj from 'wsemi/src/iseobj.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import ispint from 'wsemi/src/ispint.mjs'
import isearr from 'wsemi/src/isearr.mjs'
import ispnum from 'wsemi/src/ispnum.mjs'
import isfun from 'wsemi/src/isfun.mjs'
import cint from 'wsemi/src/cint.mjs'
import strleft from 'wsemi/src/strleft.mjs'
import strright from 'wsemi/src/strright.mjs'
import strdelright from 'wsemi/src/strdelright.mjs'
import pm2resolve from 'wsemi/src/pm2resolve.mjs'
import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
import fsIsFile from 'wsemi/src/fsIsFile.mjs'
import replace from 'wsemi/src/replace.mjs'
import WServHapiServer from 'w-serv-hapi/src/WServHapiServer.mjs'
import WServOrm from 'w-serv-orm/src/WServOrm.mjs'
import ds from '../src/schema/index.mjs'
import procCore from './procCore.mjs'
import procSettings from './procSettings.mjs'


/**
 * 權限伺服器
 *
 * @class
 * @param {Function} WOrm 輸入資料庫ORM函數
 * @param {String} url 輸入資料庫連線字串,例如w-orm-lmdb為'./db',或w-orm-mongodb為'mongodb://sername:password@$127.0.0.1:27017'
 * @param {String} db 輸入資料庫名稱字串
 * @param {Function} getUserByToken 輸入處理函數,函數會傳入使用者token,通過此函數處理後並回傳使用者資訊物件,並至少須提供'id'、'email'、'name'、'isAdmin'欄位,且'isAdmin'限輸入'y'或'n',且輸入'y'時會複寫權限系統該使用者之'isAdmin'欄位值
 * @param {Function} verifyClientUser 輸入驗證瀏覽使用者身份之處理函數,函數會傳入使用者資訊物件,通過此函數識別後回傳布林值,允許使用者回傳true,反之回傳false
 * @param {Function} verifyAppUser 輸入驗證應用程序使用者身份之處理函數,函數會傳入使用者資訊物件,通過此函數識別後回傳布林值,允許使用者回傳true,反之回傳false
 * @param {String} [pathSettings='./settings'] 輸入設定檔案路徑字串,預設'./settings'
 * @returns {Object} 回傳物件,其內server為hapi伺服器實體,wsrv為w-converhp的伺服器事件物件,wsds為w-serv-webdata的伺服器事件物件,可監聽error事件
 * @example
 *
 *
 *
 */
function WWebSso(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser, pathSettings) {
    let instWServHapiServer = null


    //check WOrm
    if (!isfun(WOrm)) {
        console.log('invalid WOrm', WOrm)
        throw new Error('invalid WOrm')
    }


    //check url
    if (!isestr(url)) {
        console.log('invalid url', url)
        throw new Error('invalid url')
    }


    //check db
    if (!isestr(db)) {
        console.log('invalid db', db)
        throw new Error('invalid db')
    }


    //check getUserByToken
    if (!isfun(getUserByToken)) {
        console.log('invalid getUserByToken', getUserByToken)
        throw new Error('invalid getUserByToken')
    }


    //check verifyClientUser
    if (!isfun(verifyClientUser)) {
        console.log('invalid verifyClientUser', verifyClientUser)
        throw new Error('invalid verifyClientUser')
    }


    //check verifyAppUser
    if (!isfun(verifyAppUser)) {
        console.log('invalid verifyAppUser', verifyAppUser)
        throw new Error('invalid verifyAppUser')
    }


    //check pathSettings
    if (!fsIsFile(pathSettings)) {
        pathSettings = './settings.json'
    }


    //setFilepath
    procSettings.setFilepath(pathSettings)


    //getSettings
    let opt = procSettings.getSettings()
    // * @param {Object|String} [opt={}] 輸入設定物件或設定檔檔案路徑字串,若給予檔案路徑則會預設檔案為json格式且讀入成為opt物件,預設{}
    // * @param {Integer} [opt.serverPort=11007] 輸入伺服器通訊port,預設11006
    // * @param {Boolean} [opt.useCheckUser=false] 輸入是否檢查使用者資訊布林值,預設false
    // * @param {Function} [opt.getUserById=null] 輸入當useCheckUser=true時依照使用者ID取得使用者資訊物件函數,預設null
    // * @param {Boolean} [opt.useExcludeWhenNotAdmin=false] 輸入使用ORM的select方法時是否自動刪除數據內isActive欄位之布林值,預設false
    // * @param {Object} [opt.webName={}] 輸入站台名稱物件,至少包含語系eng與cht鍵的名稱,預設{}
    // * @param {Object} [opt.webDescription={}] 輸入站台描述物件,至少包含語系eng與cht鍵的名稱,預設{}
    // * @param {String} [opt.webLogo=''] 輸入站台logo字串,採base64格式,預設''
    // * @param {String} [opt.subfolder=''] 輸入站台所在子目錄字串,提供站台位於內網採反向代理進行服務時,故需支援位於子目錄情形,預設''
    // * @param {String} [opt.mappingBy='email'] 輸入外部系統識別使用者token後所提供之資料物件,與權限系統之使用者資料物件,兩者間查找之對應欄位,可選'id'、'email'、'name',預設'email'


    //serverPort
    let serverPort = get(opt, 'serverPort')
    if (!ispint(serverPort)) {
        serverPort = 11007
    }
    serverPort = cint(serverPort)


    //useCheckUser
    let useCheckUser = get(opt, 'useCheckUser', false)


    //getUserById
    let getUserById = get(opt, 'getUserById', null)


    //useExcludeWhenNotAdmin
    let useExcludeWhenNotAdmin = get(opt, 'useExcludeWhenNotAdmin', false)


    //webName
    let webName = get(opt, 'webName', {})


    //webDescription
    let webDescription = get(opt, 'webDescription', {})


    //webLogo
    let webLogo = get(opt, 'webLogo', '')


    //webBackgoundGradientColors
    let webBackgoundGradientColors = get(opt, 'webBackgoundGradientColors', [])
    if (!isearr(webBackgoundGradientColors)) {
        webBackgoundGradientColors = ['#FFE0B2', '#FFCC80', '#FFF59D']
    }


    //webKey
    let webKey = get(opt, 'webKey', '')


    //salt
    let salt = get(opt, 'salt', '')


    //timeExpired
    let timeExpired = get(opt, 'timeExpired', '')
    if (ispnum(timeExpired)) {
        timeExpired = 30
    }

    //subfolder
    let subfolder = get(opt, 'subfolder', '')
    if (isestr(subfolder)) {
        if (strright(subfolder, 1) === '/') { //右邊不需要給「/」
            subfolder = strdelright(subfolder, 1)
        }
        if (strleft(subfolder, 1) !== '/') { //左邊需要給「/」
            subfolder = `/${subfolder}`
        }
    }


    //params
    let showLanguage = get(opt, 'showLanguage', 'n')
    let language = get(opt, 'language', 'eng')
    let showModeEditUsers = get(opt, 'showModeEditUsers', 'n')
    let modeEditUsers = get(opt, 'modeEditUsers', 'n')

    //是否可於登入頁提供申請註冊使用者按鈕 bbb
    //是否使用圖形驗證碼模組

    // - 使用者成功登入後產生token之有效(過期)時間m(30)分鐘
    // - 使用者n(10)分鐘內最大登入失敗次數m(3),若觸發則進入封鎖狀態p(30)分鐘
    // - 使用者n(10)分鐘內最大調用API次數m(1000),若觸發則進入封鎖狀態p(30)分鐘
    // - IP使用者n(10)分鐘內最大調用API次數m(10000),若觸發則IP進入封鎖狀態p(30)分鐘
    // - 清除(移動至備份表)紀錄使用者與IP連線log機制,偵測時間n(1)天,保留最新資料時間m(60)天

    //是否啟用要求使用者定期更換密碼
    // 若是,預設6個月更換密碼,是否允許使用同樣密碼

    //email模板
    // 註冊驗證信模板
    // 變更使用者密碼驗證信模板
    // 註冊請求通知(給SSO系統管理員)信模板
    // 註冊核可成功通知信模板
    // 變更使用者資訊通知信模板


    //mappingBy
    let mappingBy = get(opt, 'mappingBy', '')
    if (mappingBy !== 'id' && mappingBy !== 'email' && mappingBy !== 'name') {
        mappingBy = 'email'
    }
    // console.log('mappingBy', mappingBy)


    //kpLangExt
    let kpLangExt = get(opt, 'kpLangExt', null)


    //WServOrm
    let optWServOrm = {
        useCheckUser,
        getUserById,
        useExcludeWhenNotAdmin,
    }
    let wp = {}
    try {
        wp = WServOrm(ds, WOrm, url, db, optWServOrm)
    }
    catch (err) {
        console.log(err)
    }
    let { woItems, procOrm } = wp


    //getWebInfor
    let getWebInfor = (_t) => {
        return {

            webName,
            webDescription,
            webLogo,
            webBackgoundGradientColors,
            webKey,

            showLanguage,
            language,

            showModeEditUsers,
            modeEditUsers,

            kpLangExt,

        }
    }


    //procCore
    let p = procCore(woItems, procOrm, { salt, timeExpired })


    //detect
    let stCleanTokens = 'none'
    setInterval(() => {
        if (stCleanTokens === 'doing') {
            return
        }
        stCleanTokens = 'doing'
        p.cleanTokens()
            .finally(() => {
                stCleanTokens = 'none'
            })
    }, 1000)


    // //parseToken
    // let parseToken = (req) => {

    //     //token
    //     let token = get(req, 'query.token', '')
    //     // console.log('token', token)

    //     return token
    // }


    // //getTokenUser
    // let getTokenUser = async(token) => {

    //     //userSelf
    //     let userSelf = null
    //     if (isestr(token)) {
    //         userSelf = getUserByToken(token)
    //         if (ispm(userSelf)) {
    //             userSelf = await userSelf
    //         }
    //     }
    //     // console.log('userSelf', userSelf)

    //     //check
    //     if (!iseobj(userSelf)) {
    //         console.log(`token`, token)
    //         console.log(`can not find the user from token`)
    //         return Promise.reject(`can not find the user from token`)
    //     }

    //     //check userSelf
    //     if (true) {
    //         let id = get(userSelf, 'id', '')
    //         if (!isestr(id)) {
    //             console.log('userSelf', userSelf)
    //             console.log('can not get the userId')
    //             return Promise.reject(`can not get the userId`)
    //         }
    //         let email = get(userSelf, 'email', '')
    //         if (!isestr(email)) {
    //             console.log('userSelf', userSelf)
    //             console.log('can not get the email of user')
    //             return Promise.reject(`can not get the email of user`)
    //         }
    //         let name = get(userSelf, 'name', '')
    //         if (!isestr(name)) {
    //             console.log('userSelf', userSelf)
    //             console.log('can not get userName')
    //             return Promise.reject(`can not get userName`)
    //         }
    //         let isAdmin = get(userSelf, 'isAdmin', '')
    //         if (isAdmin !== 'y' && isAdmin !== 'n') {
    //             console.log('userSelf', userSelf)
    //             console.log('userSelf.isAdmin is not y or n', userSelf.isAdmin)
    //             console.log('can not get the role of user')
    //             return Promise.reject(`can not get the role of user`)
    //         }
    //     }

    //     //須反查sso內users, 提供正規化屬性
    //     let vSelf = get(userSelf, mappingBy, '')
    //     // console.log('vSelf', vSelf)

    //     //check
    //     if (!isestr(vSelf)) {
    //         console.log('userSelf', userSelf)
    //         console.log('mappingBy', mappingBy)
    //         console.log('can not get the prop of user by mappingBy')
    //         return Promise.reject(`can not get the prop of user by mappingBy`)
    //     }

    //     //userFind
    //     let userFind = await woItems.users.select({ [mappingBy]: vSelf, isActive: 'y' })
    //     userFind = get(userFind, 0, null)
    //     // console.log('userFind', userFind)

    //     //check
    //     if (!iseobj(userFind)) {
    //         console.log('userSelf', userSelf)
    //         console.log('mappingBy', mappingBy)
    //         console.log('can not get the user from sso')
    //         return Promise.reject(`can not get the user from sso`)
    //     }

    //     //複寫isAdmin
    //     let isAdminSrc = get(userSelf, 'isAdmin', '')
    //     let isAdminSelf = get(userFind, 'isAdmin', '')
    //     if (isAdminSrc !== isAdminSelf) {
    //         userFind.isAdmin = isAdminSelf
    //     }
    //     // console.log('userFind(isAdmin)', userFind)

    //     return userFind
    // }


    // //getAndVerifyBrowserTokenUser
    // let getAndVerifyBrowserTokenUser = async (token, caller = '') => {

    //     //getTokenUser
    //     let userSelf = await getTokenUser(token)

    //     //verifyClientUser
    //     let b = verifyClientUser(userSelf, caller)
    //     if (ispm(b)) {
    //         b = await b
    //     }

    //     //check
    //     if (!b) {
    //         console.log('userSelf', userSelf)
    //         console.log(`user does not have permission`)
    //         return Promise.reject(`user does not have permission`)
    //     }

    //     return userSelf
    // }


    // //getAndVerifyAppTokenUser
    // let getAndVerifyAppTokenUser = async (token, caller = '') => {

    //     //getTokenUser
    //     let userSelf = await getTokenUser(token)

    //     //verifyAppUser
    //     let b = verifyAppUser(userSelf, caller)
    //     if (ispm(b)) {
    //         b = await b
    //     }

    //     //check
    //     if (!b) {
    //         console.log('userSelf', userSelf)
    //         console.log(`user does not have permission`)
    //         return Promise.reject(`user does not have permission`)
    //     }

    //     return userSelf
    // }


    // //parsePayload
    // let parsePayload = async (req, key) => {

    //     //inp
    //     let inp = get(req, 'payload')

    //     //to obj
    //     if (isestr(inp)) {
    //         inp = j2o(inp)
    //     }

    //     //check
    //     if (!iseobj(inp)) {
    //         console.log('inp', inp)
    //         console.log('invalid inp from req')
    //         return Promise.reject(`invalid inp from req`)
    //     }

    //     //from
    //     let from = get(inp, 'from', '')

    //     //check
    //     if (!isestr(from)) {
    //         console.log('inp', inp)
    //         console.log('from', from)
    //         console.log('invalid from from inp')
    //         return Promise.reject(`invalid from from inp`)
    //     }


    //     //vs
    //     let vs = get(inp, key, [])

    //     //check
    //     if (!isearr(vs)) {
    //         console.log('inp', inp)
    //         console.log(key, vs)
    //         console.log(`invalid ${key} from inp`)
    //         return Promise.reject(`invalid ${key} from inp`)
    //     }

    //     //resave
    //     inp = {
    //         from,
    //         [key]: vs,
    //     }

    //     return inp
    // }


    // //syncAndReplaceRows
    // let syncAndReplaceRows = async (params, key) => {

    //     //from
    //     let from = get(params, 'from', '')
    //     // console.log('from', from)

    //     //check
    //     if (!isestr(from)) {
    //         console.log('params', params)
    //         console.log('from', from)
    //         console.log(`invalid from`)
    //         return Promise.reject(`invalid from`)
    //     }

    //     //vs
    //     let vs = get(params, key, [])
    //     // console.log(key, vs)

    //     //check
    //     if (!isearr(vs)) {
    //         console.log('params', params)
    //         console.log(key, vs)
    //         console.log(`invalid ${key}`)
    //         return Promise.reject(`invalid ${key}`)
    //     }

    //     //save from
    //     each(vs, (v, k) => {
    //         vs[k].from = from
    //     })

    //     //delAll from
    //     await woItems[key].delAll({ from })

    //     //insert
    //     let r = await woItems[key].insert(vs)

    //     return r
    // }


    //pathStaticFiles
    let pathStaticFiles = 'dist'
    let npmPathStaticFiles = './node_modules/w-web-sso/dist'
    if (fsIsFolder(npmPathStaticFiles)) {
        pathStaticFiles = npmPathStaticFiles
    }
    // console.log('pathStaticFiles', pathStaticFiles)


    //subfolder
    let fnEntryIn = 'index.tmp'
    let fnEntryOut = 'index.html'
    try {
        let fpEntryIn = path.resolve(pathStaticFiles, fnEntryIn)
        if (!fsIsFile(fpEntryIn)) {
            fpEntryIn = path.resolve(pathStaticFiles, fnEntryOut) //本機開發另使用html替代tmp
        }
        if (!fsIsFile(fpEntryIn)) {
            console.log('fpEntryIn', fpEntryIn)
            throw new Error(`invalid fpEntryIn`)
        }
        let fpEntryOut = path.resolve(pathStaticFiles, fnEntryOut)
        let c = fs.readFileSync(fpEntryIn, 'utf8')
        c = replace(c, '/msso/', '{sfd}/') //方法同genEntry
        c = replace(c, '{sfd}', subfolder)
        c = replace(c, '{language}', language)
        fs.writeFileSync(fpEntryOut, c, 'utf8')
    }
    catch (err) {
        console.log(err)
        console.log(`can not generate ${fnEntryOut}`)
    }


    //apis
    let apis = [
        // {
        //     method: 'GET',
        //     path: '/api/someAPI',
        //     handler: async function (req, res) {

        //         // //token
        //         // let token = get(req, 'query.token', '')

        //         return 'someAPI'
        //     },
        // },

        // {
        //     method: 'GET',
        //     path: '/getWebInfor',
        //     handler: async function (req, res) {
        //         return getWebInfor() //bbb 應該已不用, 前端載入通訊器後由$fapi.getWebInfor呼叫
        //     },
        // },

        {
            method: 'GET',
            path: '/api/logoutSsoUser',
            handler: async function (req, res) {
                // console.log('logoutSsoUser', req)

                async function core() {

                    //token
                    let token = get(req, 'query.token', '')
                    // console.log('token', token)

                    //logOutByToken
                    let b = await p.logOutByToken(token)
                    // console.log('b', b)

                    return b
                }

                //pm2resolve core
                let r = await pm2resolve(core)()
                // console.log('logoutSsoUser', r)

                return r
            },
        },

        {
            method: 'GET',
            path: '/api/checkToken',
            handler: async function (req, res) {
                // console.log('checkToken', req)

                async function core() {

                    //token
                    let token = get(req, 'query.token', '')
                    // console.log('token', token)

                    //checkToken
                    let b = await p.checkToken(token)
                    // console.log('b', b)

                    return b
                }

                //pm2resolve core
                let r = await pm2resolve(core)()
                // console.log('checkToken', r)

                return r
            },
        },

        {
            method: 'GET',
            path: '/api/getSsoUsersList',
            handler: async function (req, res) {
                // console.log('getSsoUsersList', req)

                async function core() {

                    //token
                    let token = get(req, 'query.token', '')
                    // console.log('token', token)

                    //getUsersList
                    let us = await p.getUsersList(token)
                    // console.log('us', us)

                    return us
                }

                //pm2resolve core
                let r = await pm2resolve(core)()
                // console.log('getSsoUsersList', r)

                return r
            },
        },

        {
            method: 'GET',
            path: '/api/getSsoUserInfor',
            handler: async function (req, res) {
                // console.log('getSsoUserInfor', req)

                async function core() {

                    //token
                    let token = get(req, 'query.token', '')
                    // console.log('token', token)

                    //key
                    let key = get(req, 'query.key', '')
                    // console.log('key', key)

                    //value
                    let value = get(req, 'query.value', '')
                    // console.log('value', value)

                    //userTarget
                    let userTarget = null
                    if (key === 'token') {
                        userTarget = await p.getUserByToken(value)
                        // console.log('getUserByToken userTarget', userTarget)
                    }
                    else {
                        userTarget = await p.getUserInfor(token, key, value)
                        // console.log('getUserInfor userTarget', userTarget)
                    }

                    //check
                    if (!iseobj(userTarget)) {
                        console.log('token', token)
                        console.log('key', key, 'value', value)
                        console.log(`token does not have permission`)
                        return Promise.reject(`token does not have permission`)
                    }

                    return userTarget
                }

                //pm2resolve core
                let r = await pm2resolve(core)()
                // console.log('getSsoUserInfor', r)

                return r
            },
        },

    ]


    // //tableNamesExec, tableNamesSync
    // let tableNamesExec = [] //keys(ds)
    // // let tableNamesSync = filter(tableNamesExec, (v) => {
    // //     return strright(v, 5) !== 'Items'//不同步數據
    // // })
    // let tableNamesSync = []


    //WServHapiServer
    instWServHapiServer = new WServHapiServer({
        port: opt.serverPort,
        pathStaticFiles,
        apis,
        verifyConn: () => {
            return true
        },
        getUserIdByToken: async (token) => { //可使用async或sync函數
            return ''
        },
        corsOrigins: ['*'],
        useDbOrm: false,
        // kpOrm: woItems,
        // operOrm: procOrm, //procOrm的輸入為: userId, tableName, methodName, input
        tableNamesExec: [],
        methodsExec: [], //['select', 'insert', 'save', 'del', 'delAll'],
        tableNamesSync: [],
        kpFunExt: { //接收參數第1個為userId, 之後才是前端給予參數
            getWebInfor,
            getUserByToken: (_t, token) => {
                return p.getUserByToken(token)
            },
            loginByAccountAndPassword: (_t, account, password) => {
                return p.loginByAccountAndPassword(account, password)
            },
            checkToken: (_t, token) => {
                return p.checkToken(token)
            },
            refreshToken: (_t, token) => {
                return p.refreshToken(token)
            },
            logOutByToken: (_t, token) => {
                return p.logOutByToken(token)
            },
            getUsersList: (_t, token) => {
                return p.getUsersList(token)
            },
            updateUsersList: (_t, token, rows) => {
                return p.updateUsersList(token, rows)
            },
            getSettings: async (_t, token,) => {

                //checkToken
                await p.checkToken(token)

                return procSettings.getSettings()
            },
            updateSettings: async (_t, token, st) => {

                //checkToken
                await p.checkToken(token)

                return procSettings.setSettings(st)
            },
            //...
        },
        fnTableTags: 'tableTags-web-sso.json',
    })


    return instWServHapiServer
}


export default WWebSso