sardines-built-in-services
Version:
A built-in service provider for sardines.js
241 lines (219 loc) • 8.25 kB
text/typescript
/**
* @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})
}
}