diff --git a/.gitignore b/.gitignore index e067215..2f3cfd3 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ public/* # Access access/gsheets.json access/jwt_secret.json +access/db.json # Misc .DS_Store diff --git a/src/server/checkAccess.ts b/src/server/checkAccess.ts new file mode 100755 index 0000000..e255f9a --- /dev/null +++ b/src/server/checkAccess.ts @@ -0,0 +1,5 @@ +import { checkGSheetsAccess } from "./gsheets/accessors" + +export default async function checkAccess(): Promise { + checkGSheetsAccess() +} diff --git a/src/server/gsheets/accessors.ts b/src/server/gsheets/accessors.ts index f0e3439..00bc3b8 100644 --- a/src/server/gsheets/accessors.ts +++ b/src/server/gsheets/accessors.ts @@ -1,16 +1,21 @@ // eslint-disable-next-line max-classes-per-file import path from "path" import _ from "lodash" -import { promises as fs } from "fs" +import { promises as fs, constants } from "fs" import { GoogleSpreadsheet, GoogleSpreadsheetWorksheet } from "google-spreadsheet" // Test write attack with: wget --header='Content-Type:application/json' --post-data='{"prenom":"Pierre","nom":"SCELLES","email":"test@gmail.com","telephone":"0601010101","dejaBenevole":false,"commentaire":""}' http://localhost:3000/PreVolunteerAdd const CRED_PATH = path.resolve(process.cwd(), "access/gsheets.json") +const DB_PATH = path.resolve(process.cwd(), "access/db.json") const REMOTE_UPDATE_DELAY = 40000 const DELAY_AFTER_QUERY = 2000 +let creds: string | undefined | null +// eslint-disable-next-line @typescript-eslint/ban-types +let localDb: { [sheetName in keyof SheetNames]?: object[] | undefined } = {} + export type ElementWithId = { id: number } & ElementNoId export class SheetNames { @@ -28,6 +33,26 @@ export const sheetNames = new SheetNames() type SheetList = { [sheetName in keyof SheetNames]?: Sheet> } const sheetList: SheetList = {} +let hasGSheetsAccessReturn: boolean | undefined +export async function hasGSheetsAccess(): Promise { + if (hasGSheetsAccessReturn !== undefined) { + return hasGSheetsAccessReturn + } + try { + // eslint-disable-next-line no-bitwise + await fs.access(CRED_PATH, constants.R_OK | constants.W_OK) + hasGSheetsAccessReturn = true + } catch { + hasGSheetsAccessReturn = false + } + return hasGSheetsAccessReturn +} + +export async function checkGSheetsAccess(): Promise { + if (!(await hasGSheetsAccess())) { + console.error(`Google Sheets: no creds found, loading local database ${DB_PATH} instead`) + } +} export function getSheet< // eslint-disable-next-line @typescript-eslint/ban-types ElementNoId extends object, @@ -83,7 +108,7 @@ export class Sheet< (englishProp: string) => (specimen as any)[englishProp] ) as Element - setTimeout(() => this.dbLoad(), 100 * Object.values(sheetList).length) + setTimeout(() => this.dbFirstLoad(), 100 * Object.values(sheetList).length) } async getList(): Promise { @@ -91,9 +116,10 @@ export class Sheet< return JSON.parse(JSON.stringify(this._state)) } - setList(newState: Element[] | undefined): void { + async setList(newState: Element[] | undefined): Promise { this._state = JSON.parse(JSON.stringify(newState)) this.modifiedSinceSave = true + this.localDbSave() } async nextId(): Promise { @@ -147,6 +173,15 @@ export class Sheet< } else { this.dbLoad() } + if (__DEV__) { + this.localDbSave() + } + } + + async localDbSave(): Promise { + localDb[this.name] = this._state + const jsonDB = __DEV__ ? JSON.stringify(localDb, null, 2) : JSON.stringify(localDb) + await fs.writeFile(DB_PATH, jsonDB) } dbSave(): void { @@ -174,12 +209,36 @@ export class Sheet< } } + async dbFirstLoad(): Promise { + if (await hasGSheetsAccess()) { + this.dbLoad() + } else if (_.isEmpty(localDb)) { + let stringifiedDb + try { + stringifiedDb = await fs.readFile(DB_PATH) + } catch { + console.error(`Error: Found no DB file at ${DB_PATH}`) + } + if (stringifiedDb) { + localDb = JSON.parse(stringifiedDb.toString()) + if (localDb[this.name]) { + this._state = localDb[this.name] as Element[] + } + } + this.dbLoad() + } + } + private async dbSaveAsync(): Promise { if (!this._state) { return } const sheet = await this.getGSheet() + if (!sheet) { + return + } + // Load sheet into an array of objects const rows = await sheet.getRows() await delayDBAccess() @@ -245,6 +304,10 @@ export class Sheet< type StringifiedElement = Record const sheet = await this.getGSheet() + if (!sheet) { + return + } + // Load sheet into an array of objects const rows = (await sheet.getRows()) as StringifiedElement[] await delayDBAccess() @@ -275,11 +338,21 @@ export class Sheet< this._state = elements } - private async getGSheet(): Promise { - const doc = new GoogleSpreadsheet("1pMMKcYx6NXLOqNn6pLHJTPMTOLRYZmSNg2QQcAu7-Pw") - const creds = await fs.readFile(CRED_PATH) + private async getGSheet(): Promise { + if (creds === undefined) { + if (await hasGSheetsAccess()) { + const credsBuffer = await fs.readFile(CRED_PATH) + creds = credsBuffer?.toString() || null + } else { + creds = null + } + } + if (creds === null) { + return null + } // Authentication - await doc.useServiceAccountAuth(JSON.parse(creds.toString())) + const doc = new GoogleSpreadsheet("1pMMKcYx6NXLOqNn6pLHJTPMTOLRYZmSNg2QQcAu7-Pw") + await doc.useServiceAccountAuth(JSON.parse(creds)) await doc.loadInfo() return doc.sheetsByTitle[this.sheetName] } diff --git a/src/server/index.ts b/src/server/index.ts index d9cadec..0cdccc0 100755 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -28,6 +28,9 @@ import { } from "./gsheets/volunteers" import config from "../config" import notificationsSubscribe from "./notificationsSubscribe" +import checkAccess from "./checkAccess" + +checkAccess() const app = express()