2019-12-30 09:27:49 -05:00
import pg from 'pg'
import pg _error from 'pg-error-constants'
import bcrypt from 'bcrypt'
import assert from 'assert'
function objectFlip ( obj ) {
const ret = { } ;
Object . keys ( obj ) . forEach ( key => {
ret [ obj [ key ] ] = key ;
} ) ;
return ret ;
}
let pg _error _inv = objectFlip ( pg _error )
export let pool ;
const FACTION _MAP = {
0 : [ "Terran Republic" , "TR" ] ,
1 : [ "New Conglomerate" , "NC" ] ,
2 : [ "Vanu Sovereignty" , "VS" ] ,
3 : [ "Neutral" , "NL" ] ,
}
const FACTION _MAP _INV = objectFlip ( FACTION _MAP )
const BCRYPT _ROUNDS = 4 ;
export const SQL _ORDER = Object . freeze ( {
ASCENDING : Symbol ( "ASC" ) ,
DESCENDING : Symbol ( "DESC" ) ,
} ) ;
export const ACCOUNT = Object . freeze ( {
THIS : Symbol ( "accounts" ) ,
ID : Symbol ( "id" ) ,
USER : Symbol ( "username" ) ,
PASSWORD : Symbol ( "passhash" ) ,
CREATED : Symbol ( "created" ) ,
MODIFIED : Symbol ( "last_modified" ) ,
BANNED : Symbol ( "inactive" ) ,
ADMIN : Symbol ( "gm" ) ,
} ) ;
export const CHARACTER = Object . freeze ( {
THIS : Symbol ( "characters" ) ,
ID : Symbol ( "id" ) ,
NAME : Symbol ( "name" ) ,
ACCOUNT _ID : Symbol ( "account_id" ) ,
FACTION : Symbol ( "faction_id" ) ,
GENDER : Symbol ( "gender_id" ) ,
HEAD : Symbol ( "head_id" ) ,
VOICE : Symbol ( "void_id" ) ,
CREATED : Symbol ( "created" ) ,
LAST _LOGIN : Symbol ( "last_login" ) ,
LAST _MODIFIED : Symbol ( "last_modified" ) ,
DELETED : Symbol ( "deleted" ) ,
} ) ;
2019-12-30 13:20:50 -05:00
export const LOGIN = Object . freeze ( {
THIS : Symbol ( "logins" ) ,
ID : Symbol ( "id" ) ,
ACCOUNT _ID : Symbol ( "account_id" ) ,
} ) ;
2019-12-30 09:27:49 -05:00
function to _sql ( symbol ) {
assert ( typeof symbol == 'symbol' )
return String ( symbol ) . slice ( 7 , - 1 ) ;
}
2019-12-30 13:20:50 -05:00
function to _sql _kv ( fields , idx = 1 ) {
let SQL = [ ] ;
let values = [ ] ;
// This will ONLY get Symbols in the field dict
assert ( Object . getOwnPropertySymbols ( fields ) . length > 0 , "to_sql_kv must have at least one field" )
Object . getOwnPropertySymbols ( fields ) . forEach ( key => {
assert ( typeof key == 'symbol' )
SQL . push ( to _sql ( key ) + "=$" + idx ++ ) ;
values . push ( fields [ key ] ) ;
} ) ;
return {
sql : SQL ,
next _idx : idx ,
values : values ,
}
}
function build _SET ( fields , idx = 1 ) {
const kv = to _sql _kv ( fields , idx ) ;
kv . sql = Symbol ( kv . sql . join ( ", " ) ) ;
return kv ;
}
function build _WHERE ( fields , idx = 1 ) {
const kv = to _sql _kv ( fields , idx ) ;
kv . sql = Symbol ( kv . sql . join ( " AND " ) ) ;
return kv ;
}
async function get _row _count ( table , filter = undefined ) {
let resp ;
if ( filter ) {
const where = build _WHERE ( filter ) ;
resp = await pool . query ( ` SELECT COUNT(*) FROM ${ to _sql ( table ) } WHERE ${ to _sql ( where . sql ) } ` ,
where . values ) ;
} else {
resp = await pool . query ( ` SELECT COUNT(*) FROM ${ to _sql ( table ) } ` ) ;
}
2019-12-30 09:27:49 -05:00
return parseInt ( resp . rows [ 0 ] . count ) ;
}
export async function connect _to _db ( ) {
pool = new pg . Pool ( )
try {
const res = await pool . query ( 'SELECT NOW()' )
2019-12-30 13:20:50 -05:00
// Quick hack for query debugging (throws exception)
const _query = pool . query ;
pool . query _log = ( q , v ) => {
console . log ( "QUERY LOG: " , q , v ) ;
return _query ( q , v ) ;
}
2019-12-30 09:27:49 -05:00
console . log ( ` Connected to the psql database at ${ process . env . PGHOST } ` )
} catch ( e ) {
console . log ( "Unable to connect to the database: " + e . message ) ;
process . exit ( 1 ) ;
}
}
export async function get _account _by _id ( id ) {
try {
const account = await pool . query ( 'SELECT * FROM accounts WHERE id=$1' , [ id ] ) ;
2019-12-30 13:20:50 -05:00
if ( account . rows . length == 0 ) {
return undefined ;
}
const account _obj = account . rows [ 0 ] ;
2019-12-30 09:27:49 -05:00
delete account _obj . passhash ;
2019-12-30 13:20:50 -05:00
2019-12-30 09:27:49 -05:00
return account _obj ;
} catch ( e ) {
throw e ;
}
}
export async function get _accounts ( pagination , sort , order ) {
const start _id = ( pagination . page - 1 ) * pagination . items _per _page ;
const values = [ start _id , pagination . items _per _page ] ;
try {
const account _count = await get _row _count ( ACCOUNT . THIS ) ;
const accounts = await pool . query ( ` SELECT id, username, created, last_modified, gm, inactive FROM accounts ORDER BY ${ to _sql ( sort ) } ${ to _sql ( order ) } OFFSET $ 1 LIMIT $ 2 ` , values ) ;
pagination . item _count = account _count ;
pagination . page _count = Math . ceil ( pagination . item _count / pagination . items _per _page ) ;
accounts . rows . forEach ( ( r ) => {
r . name = r . username ;
r . admin = r . gm ;
delete r . username ;
delete r . gm ;
} ) ;
return accounts . rows ;
} catch ( e ) {
if ( e . code )
e . code = pg _error _inv [ e . code ]
throw e ;
}
}
export async function get _accounts _login _info ( pagination , sort , order ) {
const start _id = ( pagination . page - 1 ) * pagination . items _per _page ;
const values = [ start _id , pagination . items _per _page ] ;
try {
const account _count = await get _row _count ( ACCOUNT . THIS ) ;
// this was a really hard query to get right...
// https://www.gab.lc/articles/better_faster_subqueries_postgresql/
const accounts = await pool . query (
'SELECT accounts.*, lastLogin as login_time, l2.ip_address, l2.canonical_hostname FROM accounts' +
' LEFT OUTER JOIN (' +
' SELECT MAX(id) as loginId, account_id, MAX(login_time) as lastLogin' +
' FROM logins' +
' GROUP BY account_id' +
' ) l ON l.account_id = accounts.id' +
' LEFT OUTER JOIN logins l2' +
' ON l2.id = l.loginId' +
` ORDER BY COALESCE(l.lastLogin, TIMESTAMP \' epoch \' ) ${ to _sql ( order ) } OFFSET $ 1 LIMIT $ 2 ` , values ) ;
pagination . item _count = account _count ;
pagination . page _count = Math . ceil ( pagination . item _count / pagination . items _per _page ) ;
accounts . rows . forEach ( ( r ) => {
r . name = r . username ;
r . admin = r . gm ;
if ( r . login _time !== null ) {
r . last _login = {
time : r . login _time ,
hostname : r . canonical _hostname ,
ip : r . ip _address ,
}
} else {
r . last _login = { }
}
delete r . login _time ;
delete r . canonical _hostname ;
delete r . ip _address ;
delete r . passhash ;
delete r . username ;
delete r . gm ;
} ) ;
return accounts . rows ;
} catch ( e ) {
if ( e . code )
e . code = pg _error _inv [ e . code ]
throw e ;
}
}
export async function get _characters ( pagination , sort , order ) {
const start _id = ( pagination . page - 1 ) * pagination . items _per _page ;
const values = [ start _id , pagination . items _per _page ] ;
try {
const char _count = await get _row _count ( CHARACTER . THIS ) ;
const chars = await pool . query ( ` SELECT id, name, faction_id, created, last_login FROM characters ORDER BY ${ to _sql ( sort ) } ${ to _sql ( order ) } OFFSET $ 1 LIMIT $ 2 ` , values ) ;
pagination . item _count = char _count ;
pagination . page _count = Math . ceil ( pagination . item _count / pagination . items _per _page ) ;
chars . rows . forEach ( ( r ) => {
delete r . account _id ;
} ) ;
return chars . rows ;
} catch ( e ) {
if ( e . code )
e . code = pg _error _inv [ e . code ]
throw e ;
}
}
export async function get _characters _by _account ( account _id ) {
try {
const characters = await pool . query ( 'SELECT * FROM characters WHERE account_id=$1 AND deleted=false' , [ account _id ] )
return characters . rows ;
} catch ( e ) {
if ( e . code )
e . code = pg _error _inv [ e . code ]
throw e ;
}
}
export async function get _account _by _name ( name ) {
try {
const account = await pool . query ( 'SELECT * FROM accounts WHERE username=$1' , [ name ] ) ;
return account . rows [ 0 ] ;
} catch ( e ) {
if ( e . code )
e . code = pg _error _inv [ e . code ]
throw e ;
}
}
export async function create _account ( username , password ) {
try {
const passhash = await bcrypt . hash ( password , BCRYPT _ROUNDS ) ;
const account _id = await pool . query ( 'INSERT INTO accounts(username, passhash) VALUES($1, $2) RETURNING id' , [ username , passhash ] ) ;
return account _id . rows [ 0 ] . id ;
} catch ( e ) {
if ( e . code )
e . code = pg _error _inv [ e . code ]
throw e ;
}
}
export async function update _account ( account _id , fields ) {
if ( fields === { } ) {
return
}
2019-12-30 13:20:50 -05:00
const set = build _SET ( fields ) ;
set . values . push ( account _id )
2019-12-30 09:27:49 -05:00
try {
2019-12-30 13:20:50 -05:00
const update _result = await pool . query ( ` UPDATE accounts SET ${ to _sql ( set . sql ) } WHERE id= $ ${ set . next _idx } ` , set . values ) ;
2019-12-30 09:27:49 -05:00
return update _result . rowCount ;
} catch ( e ) {
if ( e . code )
e . code = pg _error _inv [ e . code ]
throw e ;
}
}
export async function get _stats ( ) {
try {
const account _count = await get _row _count ( ACCOUNT . THIS ) ;
const character _count = await get _row _count ( CHARACTER . THIS ) ;
const last _account = await pool . query ( 'SELECT id, username, created FROM accounts ORDER BY id DESC LIMIT 1' ) ;
const last _character = await pool . query ( 'SELECT id, name, faction_id, created FROM characters ORDER BY id DESC LIMIT 1' ) ;
const empires = await pool . query ( 'SELECT faction_id, COUNT(*) FROM characters GROUP BY faction_id' ) ;
const stats = { }
stats . accounts = account _count ;
stats . characters = character _count ;
stats . last = { } ;
stats . last . character = last _character . rows [ 0 ] ;
stats . last . account = last _account . rows [ 0 ] ;
stats . last . account . name = stats . last . account . username
delete stats . last . account . username ;
stats . empires = { } ;
empires . rows . forEach ( ( r ) =>
stats . empires [ FACTION _MAP [ r . faction _id ] [ 1 ] ] = parseInt ( r . count )
) ;
return stats ;
} catch ( e ) {
if ( e . code )
e . code = pg _error _inv [ e . code ]
throw e ;
}
}
export async function get _account _logins ( account _id , pagination ) {
const start _id = ( pagination . page - 1 ) * pagination . items _per _page ;
const values = [ account _id , start _id , pagination . items _per _page ] ;
try {
2019-12-30 13:20:50 -05:00
const login _count = await get _row _count ( LOGIN . THIS , { [ LOGIN . ACCOUNT _ID ] : account _id } ) ;
const logins = await pool . query ( 'SELECT * FROM logins WHERE account_id=$1 ORDER by login_time DESC ' + ` OFFSET $ 2 LIMIT $ 3 ` , values ) ;
pagination . item _count = login _count ;
2019-12-30 09:27:49 -05:00
pagination . page _count = Math . ceil ( pagination . item _count / pagination . items _per _page ) ;
return logins . rows ;
} catch ( e ) {
if ( e . code )
e . code = pg _error _inv [ e . code ]
throw e ;
}
}
export async function search ( term , pagination ) {
const start _id = ( pagination . page - 1 ) * pagination . items _per _page ;
const values = [ '%' + term . toUpperCase ( ) + '%' , start _id , pagination . items _per _page ] ;
try {
const accounts = await pool . query ( 'SELECT id, username, gm FROM accounts ' +
'WHERE upper(username) LIKE $1 ' +
` ORDER BY username OFFSET $ 2 LIMIT $ 3 ` , values ) ;
const characters = await pool . query ( 'SELECT id, name, faction_id FROM characters ' +
'WHERE name LIKE $1 ' +
` ORDER BY upper(name) OFFSET $ 2 LIMIT $ 3 ` , values ) ;
pagination . item _count = 100 ;
pagination . page _count = Math . ceil ( pagination . item _count / pagination . items _per _page ) ;
const results = [ ]
accounts . rows . forEach ( ( r ) => {
r . type = "account" ;
r . name = r . username ;
r . admin = r . gm ;
delete r . username ;
delete r . gm ;
results . push ( r )
} ) ;
characters . rows . forEach ( ( r ) => {
r . type = "character" ;
results . push ( r )
} ) ;
// sort by name
results . sort ( function ( a , b ) {
var nameA = a . name . toUpperCase ( ) ; // ignore upper and lowercase
var nameB = b . name . toUpperCase ( ) ; // ignore upper and lowercase
if ( nameA < nameB ) {
return - 1 ;
}
if ( nameA > nameB ) {
return 1 ;
}
// names must be equal
return 0 ;
} ) ;
return results ;
} catch ( e ) {
if ( e . code )
e . code = pg _error _inv [ e . code ]
throw e ;
}
}
export async function validate _account ( username , password ) {
try {
const data = await pool . query ( 'SELECT id, passhash FROM accounts WHERE username=$1' , [ username ] ) ;
if ( data . rows . length === 0 ) {
return undefined ;
} else {
const creds = data . rows [ 0 ] ;
if ( await bcrypt . compare ( password , creds . passhash ) === true ) {
return creds . id ;
} else {
return creds . id ;
}
}
} catch ( e ) {
throw e ;
}
}