2019-12-30 14:27:49 +00:00
import pg from 'pg'
import pg _error from 'pg-error-constants'
import bcrypt from 'bcrypt'
import assert from 'assert'
function objectFlip ( obj ) {
2020-08-26 16:53:55 +00:00
const ret = { } ;
Object . keys ( obj ) . forEach ( key => {
ret [ obj [ key ] ] = key ;
} ) ;
return ret ;
2019-12-30 14:27:49 +00:00
}
let pg _error _inv = objectFlip ( pg _error )
export let pool ;
2020-05-13 19:00:38 +00:00
export const FACTION _MAP = {
2020-08-26 16:53:55 +00:00
0 : [ "Terran Republic" , "TR" ] ,
1 : [ "New Conglomerate" , "NC" ] ,
2 : [ "Vanu Sovereignty" , "VS" ] ,
3 : [ "Neutral" , "NL" ] ,
2019-12-30 14:27:49 +00:00
}
2020-05-13 19:00:38 +00:00
export const FACTION _MAP _INV = objectFlip ( FACTION _MAP )
2019-12-30 14:27:49 +00:00
const BCRYPT _ROUNDS = 4 ;
export const SQL _ORDER = Object . freeze ( {
2020-08-26 16:53:55 +00:00
ASCENDING : Symbol ( "ASC" ) ,
DESCENDING : Symbol ( "DESC" ) ,
2019-12-30 14:27:49 +00:00
} ) ;
export const ACCOUNT = Object . freeze ( {
2020-08-26 16:53:55 +00:00
THIS : Symbol ( "account" ) ,
ID : Symbol ( "id" ) ,
USER : Symbol ( "username" ) ,
PASSWORD : Symbol ( "passhash" ) ,
CREATED : Symbol ( "created" ) ,
MODIFIED : Symbol ( "last_modified" ) ,
BANNED : Symbol ( "inactive" ) ,
ADMIN : Symbol ( "gm" ) ,
2019-12-31 20:25:35 +00:00
// A derived table column
2020-08-26 16:53:55 +00:00
LAST _LOGIN : Symbol ( "last_login" ) ,
2019-12-30 14:27:49 +00:00
} ) ;
export const CHARACTER = Object . freeze ( {
2020-08-26 16:53:55 +00:00
THIS : Symbol ( "avatar" ) ,
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 14:27:49 +00:00
} ) ;
2019-12-30 18:20:50 +00:00
export const LOGIN = Object . freeze ( {
2020-08-26 16:53:55 +00:00
THIS : Symbol ( "login" ) ,
ID : Symbol ( "id" ) ,
ACCOUNT _ID : Symbol ( "account_id" ) ,
2019-12-30 18:20:50 +00:00
} ) ;
2019-12-30 14:27:49 +00:00
function to _sql ( symbol ) {
2019-12-31 20:25:35 +00:00
assert ( typeof symbol == 'symbol' ,
` symbol expected got ${ typeof symbol } ` )
2020-08-26 16:53:55 +00:00
return String ( symbol ) . slice ( 7 , - 1 ) ;
2019-12-30 14:27:49 +00:00
}
2020-08-26 16:53:55 +00:00
function to _sql _kv ( fields , idx = 1 ) {
2019-12-30 18:20:50 +00:00
let SQL = [ ] ;
let values = [ ] ;
// This will ONLY get Symbols in the field dict
2019-12-31 20:25:35 +00:00
if ( ! fields || Object . getOwnPropertySymbols ( fields ) . length == 0 ) {
2020-08-26 16:53:55 +00:00
return { sql : [ ] , next _idx : idx , values : [ ] }
2019-12-31 20:25:35 +00:00
}
2019-12-30 18:20:50 +00:00
Object . getOwnPropertySymbols ( fields ) . forEach ( key => {
assert ( typeof key == 'symbol' )
2020-08-26 16:53:55 +00:00
SQL . push ( to _sql ( key ) + "=$" + idx ++ ) ;
2019-12-30 18:20:50 +00:00
values . push ( fields [ key ] ) ;
} ) ;
return {
sql : SQL ,
next _idx : idx ,
values : values ,
}
}
2020-08-26 16:53:55 +00:00
function build _SET ( fields , idx = 1 ) {
2019-12-30 18:20:50 +00:00
const kv = to _sql _kv ( fields , idx ) ;
2019-12-31 20:25:35 +00:00
assert ( kv . sql . length > 0 , "SET MUST have at least one kv pair" )
kv . sql = Symbol ( ` SET ${ kv . sql . join ( ", " ) } ` ) ;
2019-12-30 18:20:50 +00:00
return kv ;
}
2020-08-26 16:53:55 +00:00
function build _WHERE ( filter , idx = 1 ) {
2019-12-31 20:25:35 +00:00
const kv = to _sql _kv ( filter . fields , idx ) ;
if ( kv . sql . length > 1 ) {
assert ( filter . binop !== undefined ,
"binary operand required in WHERE with multiple terms" )
const binop = filter . binop == "AND" ? Symbol ( "AND" ) : Symbol ( "OR" ) ;
kv . sql = Symbol ( ` WHERE ${ kv . sql . join ( ` ${ to _sql ( binop ) } ` ) } ` ) ;
} else if ( kv . sql . length == 1 )
kv . sql = Symbol ( ` WHERE ${ kv . sql [ 0 ] } ` ) ;
else
kv . sql = Symbol ( "" )
2019-12-30 18:20:50 +00:00
return kv ;
}
2019-12-31 20:25:35 +00:00
function build _ORDER _BY ( sort ) {
// This will ONLY get Symbols in the sort dict
if ( ! sort || Object . getOwnPropertySymbols ( sort ) . length == 0 ) {
return Symbol ( "" ) ;
}
let SQL = [ ]
Object . getOwnPropertySymbols ( sort ) . forEach ( key => {
const value = sort [ key ] ;
assert ( typeof key == 'symbol' )
assert ( typeof value == 'symbol' )
SQL . push ( ` ${ to _sql ( key ) } ${ to _sql ( value ) } ` ) ;
} ) ;
return Symbol ( ` ORDER BY ${ SQL . join ( ", " ) } ` ) ;
}
2020-08-26 16:53:55 +00:00
function build _OFFSET ( offset , limit , idx = 1 ) {
2019-12-31 20:25:35 +00:00
assert ( typeof offset == 'number' , "offset must be a number" ) ;
assert ( typeof limit == 'number' , "limit must be a number" ) ;
return {
sql : Symbol ( ` OFFSET $ ${ idx ++ } LIMIT $ ${ idx ++ } ` ) ,
next _idx : idx ,
values : [ offset , limit ] ,
} ;
}
2020-08-26 16:53:55 +00:00
async function get _row _count ( table , filter = undefined ) {
2019-12-30 18:20:50 +00:00
let resp ;
if ( filter ) {
const where = build _WHERE ( filter ) ;
2019-12-31 20:25:35 +00:00
resp = await pool . query ( ` SELECT COUNT(*) FROM ${ to _sql ( table ) } ${ to _sql ( where . sql ) } ` ,
2020-08-26 16:53:55 +00:00
where . values ) ;
2019-12-30 18:20:50 +00:00
} else {
resp = await pool . query ( ` SELECT COUNT(*) FROM ${ to _sql ( table ) } ` ) ;
}
2019-12-30 14:27:49 +00: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 18:20:50 +00: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 14:27:49 +00: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 {
2020-08-26 16:53:55 +00:00
const account = await pool . query ( 'SELECT * FROM account WHERE id=$1' , [ id ] ) ;
2019-12-30 14:27:49 +00:00
2019-12-30 18:20:50 +00:00
if ( account . rows . length == 0 ) {
return undefined ;
}
const account _obj = account . rows [ 0 ] ;
2019-12-30 14:27:49 +00:00
delete account _obj . passhash ;
2019-12-30 18:20:50 +00:00
2019-12-30 14:27:49 +00:00
return account _obj ;
} catch ( e ) {
throw e ;
}
}
export async function get _accounts ( pagination , sort , order ) {
2020-08-26 16:53:55 +00:00
const start _id = ( pagination . page - 1 ) * pagination . items _per _page ;
2019-12-30 14:27:49 +00:00
const values = [ start _id , pagination . items _per _page ] ;
try {
const account _count = await get _row _count ( ACCOUNT . THIS ) ;
2020-08-26 16:53:55 +00:00
const accounts = await pool . query ( ` SELECT id, username, created, last_modified, gm, inactive FROM account ORDER BY ${ to _sql ( sort ) } ${ to _sql ( order ) } OFFSET $ 1 LIMIT $ 2 ` , values ) ;
2019-12-30 14:27:49 +00:00
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 ;
}
}
2019-12-31 20:25:35 +00:00
export async function get _accounts _login _info ( pagination , sort , filter ) {
2020-08-26 16:53:55 +00:00
const start _id = ( pagination . page - 1 ) * pagination . items _per _page ;
2019-12-30 14:27:49 +00:00
const values = [ start _id , pagination . items _per _page ] ;
try {
2019-12-31 20:25:35 +00:00
const account _count = await get _row _count ( ACCOUNT . THIS , filter ) ;
const where = build _WHERE ( filter ) ;
const offset = build _OFFSET ( start _id , pagination . items _per _page , where . next _idx ) ;
const values = [ ] . concat ( where . values , offset . values ) ;
const order = build _ORDER _BY ( sort ) ;
2019-12-30 14:27:49 +00:00
// this was a really hard query to get right...
// https://www.gab.lc/articles/better_faster_subqueries_postgresql/
const accounts = await pool . query (
2020-08-26 16:53:55 +00:00
'SELECT account.*, COALESCE(l.lastLogin, TIMESTAMP \'epoch\') as last_login, l2.ip_address, l2.canonical_hostname FROM account' +
' LEFT OUTER JOIN (' +
' SELECT MAX(id) as loginId, account_id, MAX(login_time) as lastLogin' +
' FROM login' +
' GROUP BY account_id' +
' ) l ON l.account_id = account.id' +
' LEFT OUTER JOIN login l2' +
' ON l2.id = l.loginId' +
` ${ to _sql ( where . sql ) } ` +
` ${ to _sql ( order ) } ` +
` ${ to _sql ( offset . sql ) } ` , values ) ;
2019-12-30 14:27:49 +00:00
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 ;
2019-12-31 20:25:35 +00:00
if ( r . ip _address !== null ) {
2019-12-30 14:27:49 +00:00
r . last _login = {
2020-08-26 16:53:55 +00:00
time : r . last _login ,
hostname : r . canonical _hostname ,
ip : r . ip _address ,
2019-12-30 14:27:49 +00:00
}
} else {
r . last _login = { }
}
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 ) {
2020-08-26 16:53:55 +00:00
const start _id = ( pagination . page - 1 ) * pagination . items _per _page ;
2019-12-30 14:27:49 +00:00
const values = [ start _id , pagination . items _per _page ] ;
try {
const char _count = await get _row _count ( CHARACTER . THIS ) ;
2020-08-26 16:53:55 +00:00
const chars = await pool . query ( ` SELECT id, account_id, name, faction_id, created, last_login FROM avatar ORDER BY ${ to _sql ( sort ) } ${ to _sql ( order ) } OFFSET $ 1 LIMIT $ 2 ` , values ) ;
2019-12-30 14:27:49 +00:00
pagination . item _count = char _count ;
pagination . page _count = Math . ceil ( pagination . item _count / pagination . items _per _page ) ;
return chars . rows ;
} catch ( e ) {
if ( e . code )
e . code = pg _error _inv [ e . code ]
throw e ;
}
}
2020-09-20 09:15:17 +00:00
// Database action added for the sake of reporting avatar information out to a publicly exposed API route for leader-boards and other components.
export async function get _character _batch _for _stats ( batch , sort , order ) {
const values = [ batch ] ;
try {
const char _count = await get _row _count ( CHARACTER . THIS ) ;
const chars = await pool . query ( ` SELECT id, name, faction_id, bep, cep FROM avatar ORDER BY ${ to _sql ( sort ) } ${ to _sql ( order ) } OFFSET $ 1*1000 LIMIT 1000 ` , values ) ;
return chars . rows ;
} catch ( e ) {
if ( e . code )
e . code = pg _error _inv [ e . code ]
throw e ;
}
}
2019-12-30 14:27:49 +00:00
export async function get _characters _by _account ( account _id ) {
try {
2020-08-26 16:53:55 +00:00
const characters = await pool . query ( 'SELECT * FROM avatar WHERE account_id=$1 AND deleted=false' , [ account _id ] )
2019-12-30 14:27:49 +00:00
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 {
2020-08-26 16:53:55 +00:00
const account = await pool . query ( 'SELECT * FROM account WHERE username=$1' , [ name ] ) ;
2019-12-30 14:27:49 +00:00
return account . rows [ 0 ] ;
} catch ( e ) {
if ( e . code )
e . code = pg _error _inv [ e . code ]
throw e ;
}
}
2020-05-12 21:06:22 +00:00
export async function get _character _by _name ( name ) {
try {
2020-08-26 16:53:55 +00:00
const account = await pool . query ( 'SELECT id, account_id, name, faction_id, created, last_login FROM avatar WHERE name=$1 AND deleted=false' , [ name ] ) ;
2020-05-12 21:06:22 +00:00
return account . rows [ 0 ] ;
} catch ( e ) {
if ( e . code )
e . code = pg _error _inv [ e . code ]
throw e ;
}
}
2019-12-30 14:27:49 +00:00
export async function create _account ( username , password ) {
try {
const passhash = await bcrypt . hash ( password , BCRYPT _ROUNDS ) ;
2020-08-26 16:53:55 +00:00
const account _id = await pool . query ( 'INSERT INTO account(username, passhash) VALUES($1, $2) RETURNING id' , [ username , passhash ] ) ;
2019-12-30 14:27:49 +00:00
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 18:20:50 +00:00
const set = build _SET ( fields ) ;
set . values . push ( account _id )
2019-12-30 14:27:49 +00:00
try {
2020-08-26 16:53:55 +00:00
const update _result = await pool . query ( ` UPDATE account ${ to _sql ( set . sql ) } WHERE id= $ ${ set . next _idx } ` , set . values ) ;
2019-12-30 14:27:49 +00:00
return update _result . rowCount ;
} catch ( e ) {
if ( e . code )
e . code = pg _error _inv [ e . code ]
throw e ;
}
}
2020-05-13 19:00:38 +00:00
export async function get _empire _stats ( ) {
try {
2020-08-26 16:53:55 +00:00
const query = await pool . query ( 'SELECT faction_id, COUNT(*) FROM avatar GROUP BY faction_id' ) ;
2020-05-13 19:00:38 +00:00
const empires = { } ;
query . rows . forEach ( ( r ) => {
empires [ FACTION _MAP [ r . faction _id ] [ 1 ] ] = parseInt ( r . count ) ;
} ) ;
return empires ;
} catch ( e ) {
if ( e . code )
e . code = pg _error _inv [ e . code ]
throw e ;
}
return stats ;
}
2019-12-30 14:27:49 +00:00
export async function get _stats ( ) {
try {
const account _count = await get _row _count ( ACCOUNT . THIS ) ;
const character _count = await get _row _count ( CHARACTER . THIS ) ;
2020-08-26 16:53:55 +00:00
const last _character = await pool . query ( 'SELECT id, account_id, name, faction_id, created FROM avatar ORDER BY id DESC LIMIT 1' ) ;
2019-12-30 14:27:49 +00:00
const stats = { }
2020-05-13 19:00:38 +00:00
2019-12-30 14:27:49 +00:00
stats . accounts = account _count ;
stats . characters = character _count ;
stats . last = { } ;
stats . last . character = last _character . rows [ 0 ] ;
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 ) {
2020-08-26 16:53:55 +00:00
const start _id = ( pagination . page - 1 ) * pagination . items _per _page ;
2019-12-30 14:27:49 +00:00
const values = [ account _id , start _id , pagination . items _per _page ] ;
try {
2019-12-31 20:25:35 +00:00
const login _count = await get _row _count ( LOGIN . THIS , {
2020-08-26 16:53:55 +00:00
fields : {
[ LOGIN . ACCOUNT _ID ] : account _id ,
2019-12-31 20:25:35 +00:00
}
} ) ;
2020-08-26 16:53:55 +00:00
const logins = await pool . query ( 'SELECT * FROM login WHERE account_id=$1 ORDER by login_time DESC ' + ` OFFSET $ 2 LIMIT $ 3 ` , values ) ;
2019-12-30 18:20:50 +00:00
pagination . item _count = login _count ;
2019-12-30 14:27:49 +00: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 ) {
2020-08-26 16:53:55 +00:00
const start _id = ( pagination . page - 1 ) * pagination . items _per _page ;
2019-12-30 21:21:55 +00:00
term = term . replace ( /%/g , "" ) ;
if ( term . length < 3 ) {
return [ ] ;
}
2020-08-26 16:53:55 +00:00
const values = [ '%' + term . toUpperCase ( ) + '%' , start _id , pagination . items _per _page ] ;
2019-12-30 14:27:49 +00:00
try {
2020-08-26 16:53:55 +00:00
const accounts = await pool . query ( 'SELECT id, username, gm, inactive FROM account ' +
'WHERE upper(username) LIKE $1 ' +
` ORDER BY username OFFSET $ 2 LIMIT $ 3 ` , values ) ;
const characters = await pool . query ( 'SELECT id, name, account_id, faction_id FROM avatar ' +
'WHERE upper(name) LIKE $1 ' +
` ORDER BY name OFFSET $ 2 LIMIT $ 3 ` , values ) ;
2019-12-30 14:27:49 +00:00
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
2020-08-26 16:53:55 +00:00
results . sort ( function ( a , b ) {
2019-12-30 14:27:49 +00:00
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 {
2020-08-26 16:53:55 +00:00
const data = await pool . query ( 'SELECT id, passhash FROM account WHERE username=$1' , [ username ] ) ;
2019-12-30 14:27:49 +00:00
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 {
2019-12-30 18:50:45 +00:00
return undefined ;
2019-12-30 14:27:49 +00:00
}
}
} catch ( e ) {
throw e ;
}
}