import admin from 'firebase-admin' ; import FirebaseFirestore from '@google-cloud/firestore' ; import { Plugin } from '../../Plugin' ; import { Database } from '../../Plugin/Database' ; import { Dimension , DatabaseType , NanoID } from '../../Dimension' ; import { Match } from '../../Match' ; import { DeepPartial } from '../../utils/DeepPartial' ; import { pickMatch } from '../../Station/routes/api/dimensions/match' ; import bcrypt from 'bcryptjs' ; import { generateToken , verify } from '../../Plugin/Database/utils' ; import { Player , Tournament } from '../../Tournament' ; import { nanoid } from '../..' ; import { TournamentStatus } from '../../Tournament/TournamentStatus' ; import { deepMerge } from '../../utils/DeepMerge' ; import { deepCopy } from '../../utils/DeepCopy' ; require ( 'dotenv' ) . config ( ) ; const salt = bcrypt . genSaltSync ( ) ; type TournamentConfigsCollectionData = { status : Tournament . Status ; configs : Tournament . TournamentConfigsBase ; modificationDate : Date ; } ; type MatchCollectionData = Pick < Match , 'creationDate' | 'id' | 'mapAgentIDtoTournamentID' | 'matchStatus' | 'name' | 'finishDate' | 'results' | 'replayFileKey' | 'replayFile' > & { governID : string ; } ; export class FireStore extends Database { public name = 'FireStore' ; public type = Plugin . Type . DATABASE ; public db : FirebaseFirestore . Firestore ; public userCollection : FirebaseFirestore . CollectionReference < Database . User > ; public matchesCollection : FirebaseFirestore . CollectionReference < MatchCollectionData > ; public tournamentConfigsCollection : FirebaseFirestore . CollectionReference < TournamentConfigsCollectionData > ; constructor ( fireStoreConfigs , configs = { } ) { super ( configs ) ; admin . initializeApp ( { credential : admin . credential . cert ( fireStoreConfigs . keyFile ) , } ) ; } /**
     * Connects to the firestore database and returns the db object
     */ public async connect ( ) { this . db = admin . firestore ( ) ; this . db . settings ( { ignoreUndefinedProperties : true } ) ; return this . db ; } public async initialize ( ) { await this . connect ( ) ; this . userCollection = < FirebaseFirestore . CollectionReference < Database . User > > this . db . collection ( FireStore . Collections . USERS ) ; this . matchesCollection = < FirebaseFirestore . CollectionReference < MatchCollectionData > > this . db . collection ( FireStore . Collections . MATCHES ) ; this . tournamentConfigsCollection = < FirebaseFirestore . CollectionReference < TournamentConfigsCollectionData > > this . db . collection ( FireStore . Collections . TOURNAMENT_CONFIGS ) ; const existingUser = await this . getUser ( 'admin' ) ; if ( ! existingUser ) { await this . registerUser ( 'admin' , process . env . ADMIN_PASSWORD ) ; } return ; } public async storeMatch ( match , governID ) { const data = { ... pickMatch ( match ) , governID : governID } ; delete data . configs ; const plainAgentIDtoTournamentID = { } ; data . mapAgentIDtoTournamentID . forEach ( ( val , key ) => { plainAgentIDtoTournamentID [ key ] = val ; } ) ; data . mapAgentIDtoTournamentID = plainAgentIDtoTournamentID as Map < number , Tournament . ID > ; return this . matchesCollection . doc ( ) . set ( data ) ; } public async getMatch ( id ) { const snapshot = await this . matchesCollection . where ( 'matchID' , '==' , id ) . get ( ) ; return snapshot . docs [ 0 ] . data ( ) ; } public async getPlayerMatches ( playerID , governID , offset = 0 , limit = 10 , order = - 1 ) { let sortOrder = 'desc' ; if ( order > 0 ) { sortOrder = 'asc' ; } let query = this . matchesCollection . where ( 'governID' , '==' , governID ) . orderBy ( 'creationDate' , sortOrder ) . offset ( offset ) ; if ( limit == 0 ) { return [ ] ; } if ( limit > 0 ) { query = query . limit ( limit ) ; } const snapshot = await query . get ( ) ; return snapshot . docs . map ( ( d ) => d . data ( ) ) ; } public async registerUser ( username , password , userData ? ) { const hash = bcrypt . hashSync ( password , salt ) ; return this . userCollection . doc ( username ) . set ( { username : username , passwordHash : hash , statistics : { } , playerID : Player . generatePlayerID ( ) , meta : { ... userData , } , } ) ; } param usernameOrID public async getUser ( usernameOrID , publicView = true ) { const doc = await this . getUserDoc ( usernameOrID ) ; if ( doc === null ) { return null ; } const user = doc . data ( ) ; if ( user && publicView ) { user . passwordHash = '' ; } return user ; } private async getUserDoc ( usernameOrID ) { let snapshot = await this . userCollection . where ( 'playerID' , '==' , usernameOrID ) . get ( ) ; if ( snapshot . empty ) { snapshot = await this . userCollection . where ( 'username' , '==' , usernameOrID ) . get ( ) ; } if ( snapshot . empty ) { return null ; } return snapshot . docs [ 0 ] ; } public async loginUser ( username , password ) { const snapshot = await this . userCollection . where ( 'username' , '==' , username ) . get ( ) ; if ( snapshot . empty ) { throw new Error ( 'Not a valid user' ) ; } const user = snapshot . docs [ 0 ] . data ( ) ; if ( bcrypt . compareSync ( password , user . passwordHash ) ) { return generateToken ( user ) ; } else { throw new Error ( 'Invalid password' ) ; } } public async updateUser ( usernameOrID , update ) { const doc = await this . getUserDoc ( usernameOrID ) ; if ( doc === null ) { throw new Error ( 'Not a valid user' ) ; } await doc . ref . update ( update ) ; return deepMerge ( deepCopy ( doc . data ( ) ) , deepCopy ( update ) ) ; } public async deleteUser ( usernameOrID ) { const doc = await this . getUserDoc ( usernameOrID ) ; await doc . ref . delete ( ) ; } public async verifyToken ( jwt ) { return verify ( jwt ) ; } public isAdmin ( user ) { if ( user . username === 'admin' ) return true ; return false ; } public async getUsersInTournament ( tournamentKey , offset = 0 , limit = - 1 ) { const key = `statistics.${ tournamentKey }` ; if ( limit == 0 ) { return [ ] ; } let query = this . userCollection . where ( `${ key }.matchesPlayed` , '>=' , 0 ) . offset ( offset ) ; if ( limit > 0 ) { query = query . limit ( limit ) ; } const snapshot = await query . get ( ) ; return snapshot . docs . map ( ( d ) => d . data ( ) ) ; } public async manipulate ( dimension ) { dimension . configs . backingDatabase = DatabaseType . FIRESTORE ; return ; } public async storeTournamentConfigs ( tournamentID , tournamentConfigs , status ) { await this . tournamentConfigsCollection . doc ( tournamentID ) . set ( { status : status , configs : tournamentConfigs , modificationDate : new Date ( ) , } ) ; return ; } public async getTournamentConfigsModificationDate ( tournamentID ) { const snapshot = await this . tournamentConfigsCollection . doc ( tournamentID ) . get ( ) ; if ( snapshot . exists ) { return new Date ( snapshot . data ( ) . modificationDate ) ; } return null ; } public async getTournamentConfigs ( tournamentID ) { const snapshot = await this . tournamentConfigsCollection . doc ( tournamentID ) . get ( ) ; return snapshot . data ( ) ; } } export namespace FireStore { export interface Configs { /** Key file used for authentication */ keyFile : string ; } export enum Collections { MATCHES = 'd_matches' , USERS = 'd_users' , TOURNAMENT_CONFIGS = 'd_tourney_configs' } }