UNPKG

sardines-built-in-services

Version:
241 lines (219 loc) 8.25 kB
/** * @author Robin Sun * @email robin@naturewake.com * @create date 2019-06-24 12:09:45 * @modify date 2019-06-24 12:09:45 * @desc [description] */ import * as bcrypt from 'bcryptjs'; import { DatabaseStructure, ServerSettings, Database } from '../storage/postgresql' const cryptPassword = async (password: string): Promise<string> => { return new Promise((resolve, reject) => { bcrypt.genSalt(10, function(err, salt) { if (err) reject(err) bcrypt.hash(password, salt, function(err, hash) { if (err) reject(err) else resolve(hash) }); }); }) }; const comparePassword = (plainPass:string, hashword:string): Promise<boolean> => { return new Promise((resolve, reject) => { bcrypt.compare(plainPass, hashword, function(err, isPasswordMatch) { if (err) reject(err) else resolve(isPasswordMatch) }) }) }; const tokenAlphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' const genToken = (length: number) => { let token = '' for (let i = 0; i<length; i++) { token += tokenAlphabet[Math.round(Math.random()*(tokenAlphabet.length - 1))] } return token } export const basePostgresDBStruct: DatabaseStructure = { account: { id: 'UUID PRIMARY KEY DEFAULT uuid_generate_v4()', create_on: 'TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP', last_access_on: 'TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP', name: 'VARCHAR(30) UNIQUE NOT NULL', password: 'VARCHAR(64) NOT NULL', password_expire_on: 'TIMESTAMP(3)', email: 'VARCHAR(100) UNIQUE', is_email_varified: 'Boolean NOT NULL DEFAULT false', mobile: 'VARCHAR(20) UNIQUE', is_mobile_varified: 'Boolean NOT NULL DEFAULT false', can_login: 'Boolean NOT NULL DEFAULT true', can_manage_accounts: 'Boolean NOT NULL DEFAULT false' }, token: { account_id: 'UUID', token: 'VARCHAR(100) UNIQUE NOT NULL', expire_on: 'TIMESTAMP(3)' } } export interface Account { id?: string name?: any email?: any mobile?: any can_login?: boolean can_manage_accounts?: boolean password?: string } export interface Token { account_id: string token: string expire_on?: number } export class TempleteAccount { protected db: Database|null = null protected isInited: boolean = false protected tokenCache: Map<string, Token> protected dbStruct: DatabaseStructure = basePostgresDBStruct protected tokenExpireInSeconds: number = 1800 constructor() { this.tokenCache = new Map() } protected async setupDB(serverSettings: ServerSettings, dbStruct?: DatabaseStructure) { if (dbStruct) { this.dbStruct = Database.mergeDbStructs(basePostgresDBStruct, dbStruct) } this.db = new Database(serverSettings, this.dbStruct) return this.db } protected async touch(tableName: string, obj: any) { if (!tableName || !this.dbStruct[tableName]) return if (Array.isArray(obj)) { for (let o of obj) { await this.touch(tableName, o) } } else { let objId: {id: string|null}= {id: null} if (typeof obj === 'string') { objId.id = obj } else if (typeof obj === 'object' && obj && obj.id) { objId.id = obj.id } if (objId.id) { try { const colType = this.db!.getColumnType(tableName, 'last_access_on') if (colType) { await this.db!.set(tableName, {last_access_on: 'CURRENT_TIMESTAMP'}, objId) } } catch (e) { if (e) return } } } } protected async queryAccount(account: Account) { let accountForQuery: Account = Object.assign({}, account) if (accountForQuery.password) delete accountForQuery.password return await this.db!.get('account', accountForQuery) } protected async createOrUpdateAccount(account: Account) { if (account.password) account.password = await cryptPassword(account.password) else if (!account.id) throw 'password is required to create account' if (!account.id) return await this.db!.set('account', account) else return this.db!.set('account', account, {id: account.id}) } // Account protected validatePassword(password: string) { if (!password) { throw 'Password can not be empty' } else if (password.length < 6) { throw 'Password shall be longer than 6' } } async signUp(account: Account, password: string) { // Validate account and password this.validatePassword(password) // Check whether account exists let accountInst = await this.queryAccount(account) if (accountInst) { throw 'Account already exists' } else { let accountObj = Object.assign({}, account) accountObj.password = password accountInst = await this.createOrUpdateAccount(accountObj) const tokenObj = await this.createToken(accountInst.id) return tokenObj.token } } protected async createToken(accountId: string, expireInSeconds: number = this.tokenExpireInSeconds): Promise<Token> { let tokenQuery = { account_id: accountId, token: genToken(30) } let tokenInDb = await this.db!.get('token', tokenQuery) while (tokenInDb) { tokenQuery.token = genToken(32) tokenInDb = await this.db!.get('token', tokenQuery) } let tokenObj: Token = tokenQuery if (expireInSeconds > 0) { tokenObj.expire_on = Date.now() + 1000 * expireInSeconds } else { tokenObj.expire_on = Number.MAX_SAFE_INTEGER } await this.db!.set('token', tokenObj) this.tokenCache.set(tokenObj.token, tokenObj) return tokenObj } protected async validateToken(token: string, update: boolean = false): Promise<Token> { if (!token) 'token is empty' let tokenObj: Token|null = null if (this.tokenCache.has(token)) { tokenObj = this.tokenCache.get(token)! } else { tokenObj = await this.db!.get('token', {token}) } if (!tokenObj) { throw 'Invalid token' } else if (tokenObj.expire_on && tokenObj.expire_on - Date.now() < 0) { throw 'token expired' } else { if (update) { tokenObj.expire_on = Date.now() + 1000 * this.tokenExpireInSeconds await this.db!.set('token', tokenObj, {account_id: tokenObj.account_id, token: tokenObj.token}) this.tokenCache.set(token, tokenObj) } return tokenObj } } async signIn(account: Account, password: string): Promise<string> { this.validatePassword(password) let accountInst = await this.queryAccount(account) if (accountInst) { if (await comparePassword(password, accountInst.password)) { // Find a still alive token let tokenObj = await this.db!.get('token', {account_id: accountInst.id}, {expire_on: -1}) if (!tokenObj || tokenObj.expire_on.getTime() <= Date.now() + 30000 ) { // Get token tokenObj = await this.createToken(accountInst.id) } await this.touch('account', {id: accountInst.id}) return tokenObj.token } else { throw 'Password is invalid' } } else { throw 'Account does not exist' } } async signOut(token: string) { let tokenObj = await this.validateToken(token) if (this.tokenCache.has(token)) { this.tokenCache.delete(token) } await this.db!.set('token', null, {account_id: tokenObj.account_id}) } }