diff --git a/build/nodes/firebase-config.html b/build/nodes/firebase-config.html
index 9fb65d3..63d2616 100644
--- a/build/nodes/firebase-config.html
+++ b/build/nodes/firebase-config.html
@@ -80,6 +80,63 @@
// deprecated
$("#node-config-input-json").typedInput({ default: "json", types: ["json"] });
+ const timeMap = { small: 10, medium: 30, large: 60 };
+ const sizeLimitInput = $("#node-config-input-defaultWriteSizeLimit");
+ sizeLimitInput.typedInput({
+ type: "size",
+ types: [
+ {
+ value: "size", // TODO: i18n
+ options: ["small", "medium", "large", "unlimited"].map((o) => ({
+ value: o,
+ label: o + (timeMap[o] ? ` (${timeMap[o]}s)` : ""),
+ })),
+ },
+ ],
+ });
+
+ let popover = null;
+ const sizeLimitButton = $("#node-config-button-defaultWriteSizeLimit");
+ sizeLimitButton.on("click", function (event) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ if ($(this).attr("disabled")) return;
+ $(this).attr("disabled", true);
+
+ const that = this;
+ $.post(`firebase/config-node/rtdb/settings/${this.id}`, { writeSizeLimit: sizeLimitInput.val() })
+ .then(function () {
+ RED.notify("Successfully updated RTDB settings", "success");
+ })
+ .fail(function (error) {
+ console.warn(error.statusText, error.responseText);
+ RED.notify("Failed to update RTDB settings", "error");
+ })
+ .always(function () {
+ $(that).attr("disabled", false);
+ });
+ });
+
+ const msg = "Only available with PrivateKey authentication";
+ authType.on("change", function () {
+ if (authType.val() === "privateKey") {
+ sizeLimitButton.attr("disabled", false);
+ popover?.delete();
+ popover = null;
+
+ $.getJSON(`firebase/config-node/rtdb/settings/${this.id}`, function (response) {
+ sizeLimitInput.typedInput("value", response.defaultWriteSizeLimit);
+ });
+ } else if (popover) {
+ sizeLimitButton.attr("disabled", true);
+ popover.setContent(msg);
+ } else {
+ sizeLimitButton.attr("disabled", true);
+ popover = RED.popover.tooltip(sizeLimitButton, msg);
+ }
+ });
+
const tabs = RED.tabs.create({
id: "node-config-firebase-tabs",
onchange: function (tab) {
@@ -103,6 +160,11 @@
label: this._("firebase-config.tabs.databases"),
});
+ tabs.addTab({
+ id: "firebase-tab-settings",
+ label: this._("firebase-config.tabs.settings"),
+ });
+
tabs.activateTab("firebase-tab-connection");
},
oneditsave: function () {
@@ -270,5 +332,21 @@
+
+
+
diff --git a/build/nodes/locales/en-US/firebase-config.json b/build/nodes/locales/en-US/firebase-config.json
index 68908ae..530bf34 100644
--- a/build/nodes/locales/en-US/firebase-config.json
+++ b/build/nodes/locales/en-US/firebase-config.json
@@ -13,12 +13,14 @@
"projectId": "Project ID",
"storageBucket": "Bucket",
"uid": "UID",
- "url": "URL"
+ "url": "URL",
+ "writeSizeLimit": "Write size limit"
},
"tabs": {
"connection": "Connection",
"security": "Security",
- "databases": "Databases"
+ "databases": "Databases",
+ "settings": "Settings"
},
"divider": {
"authMethod": "Authentication Method",
@@ -51,6 +53,7 @@
"clickDragHere": "Click or drag the file here.",
"dropFileHere": "Drop File Here",
"nothing2complete": "Nothing to complete",
+ "saveSetting": "Save setting",
"tooltips": {
"apiKey": "API Key can be found in the project settings.",
"clientEmail": "Client Email can be found in the JSON file, it is the client_email field.",
diff --git a/build/nodes/locales/fr/firebase-config.json b/build/nodes/locales/fr/firebase-config.json
index 4c514d9..8e9c664 100644
--- a/build/nodes/locales/fr/firebase-config.json
+++ b/build/nodes/locales/fr/firebase-config.json
@@ -13,12 +13,14 @@
"projectId": "ID Projet",
"storageBucket": "Bucket",
"uid": "UID",
- "url": "URL"
+ "url": "URL",
+ "writeSizeLimit": "Taille max d'écriture"
},
"tabs": {
"connection": "Connexion",
"security": "Sécurité",
- "databases": "Base de données"
+ "databases": "Base de données",
+ "settings": "Paramètres"
},
"divider": {
"authMethod": "Méthode d'authentification",
@@ -51,6 +53,7 @@
"clickDragHere": "Cliquer ou faites glisser le fichier ici.",
"dropFileHere": "Lachez le fichier ici",
"nothing2complete": "Rien à compléter",
+ "saveSetting": "Enregistrer le paramètre",
"tooltips": {
"apiKey": "La Clé API se trouve dans les paramètres du projet.",
"clientEmail": "L'Email Client peut être trouvée dans la console Firebase, dans la section Paramètres du projet, cliquez sur Comptes de service puis sur Clés de compte de service. L'adresse e-mail est affichée dans la colonne Adresse e-mail.",
diff --git a/src/lib/firebase/client/client.ts b/src/lib/firebase/client/client.ts
index 83027eb..398d7f3 100644
--- a/src/lib/firebase/client/client.ts
+++ b/src/lib/firebase/client/client.ts
@@ -26,18 +26,20 @@ import {
signInWithEmailAndPassword,
signOut,
} from "@firebase/auth";
-import { App as FirebaseAdminApp, AppOptions, cert } from "firebase-admin/app";
+import { App as FirebaseAdminApp, AppOptions, cert, GoogleOAuthAccessToken, ServiceAccount } from "firebase-admin/app";
+import { Auth as AdminAuth, getAuth as adminGetAuth } from "firebase-admin/auth";
import { TypedEmitter } from "tiny-typed-emitter";
import { ClientError } from "./error";
import { AppConfig, ClientEvents, Credentials, SignInFn, SignState } from "./types";
-import { checkJSONCredential, createCustomToken } from "./utils";
+import { checkJSONCredential, createCustomToken, getAccessToken } from "./utils";
import { AdminApp, App } from "../app";
import { LogCallback, LogFn } from "../logger";
export class Client extends TypedEmitter {
private _app?: AdminApp | App;
- private _auth?: Auth;
+ private _auth?: AdminAuth | Auth;
private _clientInitialised: boolean = false;
+ private _serviceAccount?: ServiceAccount;
private _signState: SignState = SignState.NOT_YET;
private warn: LogFn | null = null;
@@ -56,6 +58,10 @@ export class Client extends TypedEmitter {
return this._app?.app;
}
+ public get auth(): AdminAuth | Auth | undefined {
+ return this._auth;
+ }
+
public get clientDeleted(): boolean | undefined {
return this._app?.deleted;
}
@@ -89,6 +95,13 @@ export class Client extends TypedEmitter {
return this._app.deleteApp();
}
+ public getAccessToken(): Promise {
+ if (!this._clientInitialised) throw new ClientError("getAccessToken called before signIn call");
+ if (!this._app || !this._app.admin) throw new ClientError("getAccessToken is only allowed for Admin client");
+ if (!this._serviceAccount) throw new ClientError("ServiceAccount missing to call getAccessToken");
+ return getAccessToken(this._serviceAccount);
+ }
+
public signInAnonymously(): Promise {
return this.wrapSignIn(() => signInAnonymously(this._auth as Auth));
}
@@ -137,7 +150,8 @@ export class Client extends TypedEmitter {
}
public signInWithPrivateKey(projectId: string, clientEmail: string, privateKey: string): Promise {
- const credential = { credential: cert(checkJSONCredential({ clientEmail, privateKey, projectId })) };
+ this._serviceAccount = checkJSONCredential({ clientEmail, privateKey, projectId });
+ const credential = { credential: cert(this._serviceAccount) };
return this.wrapSignIn({ ...this.appConfig, ...credential });
}
@@ -160,7 +174,7 @@ export class Client extends TypedEmitter {
this.emit("sign-out");
// Admin SDK has no signOut method - internally called during deleteApp
- if (!this.admin && this._auth) await signOut(this._auth);
+ if (!this.admin && this._auth) await signOut(this._auth as Auth);
return this.deleteClient();
}
@@ -203,6 +217,8 @@ export class Client extends TypedEmitter {
// Initialize the app and sign in
this._app = new AdminApp(config, this.appName);
+ this._auth = adminGetAuth(this._app.app);
+
// The client is ready to init DB
this._clientInitialised = true;
diff --git a/src/lib/firebase/client/utils.ts b/src/lib/firebase/client/utils.ts
index e7840d1..ede17b7 100644
--- a/src/lib/firebase/client/utils.ts
+++ b/src/lib/firebase/client/utils.ts
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-import { ServiceAccount, cert } from "firebase-admin/app";
+import { GoogleOAuthAccessToken, ServiceAccount, cert } from "firebase-admin/app";
import { getAuth } from "firebase-admin/auth";
import { claimsNotAllowed } from "./constants";
import { Credentials, ServiceAccountId } from "./types";
@@ -56,4 +56,8 @@ async function createCustomToken(cred: Credentials, uid: string, claims?: object
return token;
}
-export { checkJSONCredential, createCustomToken };
+function getAccessToken(serviceAccount: ServiceAccount): Promise {
+ return cert(checkJSONCredential(serviceAccount)).getAccessToken();
+}
+
+export { checkJSONCredential, createCustomToken, getAccessToken };
diff --git a/src/lib/nodes/rtdb-settings.ts b/src/lib/nodes/rtdb-settings.ts
new file mode 100644
index 0000000..4237427
--- /dev/null
+++ b/src/lib/nodes/rtdb-settings.ts
@@ -0,0 +1,93 @@
+/**
+ * Copyright 2023 Gauthier Dandele
+ *
+ * Licensed under the MIT License,
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/MIT.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Request, Response } from "express";
+import { ConfigNode } from "./types";
+import { NodeAPI } from "node-red";
+import { isFirebaseConfigNode } from "../firebase/utils";
+
+async function getDatabaseSettings(RED: NodeAPI, got: typeof import("got").got, req: Request, res: Response) {
+ try {
+ const id = req.params.id;
+
+ if (!id) {
+ res.status(400).send("The config-node ID is missing!");
+ return;
+ }
+
+ const node = RED.nodes.getNode(id) as ConfigNode | null;
+ if (!node || !isFirebaseConfigNode(node) || !node.client) {
+ res.json({});
+ return;
+ }
+
+ const databaseURL = node.credentials.url;
+ const token = await node.client.getAccessToken();
+
+ const path = encodeURI(req.body.path || "defaultWriteSizeLimit");
+ const url = `${databaseURL}.settings/${path}.json`;
+
+ const response = await got.get(url, {
+ headers: { Authorization: `Bearer ${token?.access_token}` },
+ responseType: "json",
+ });
+
+ res.json({ defaultWriteSizeLimit: response.body });
+ } catch (error) {
+ res.status(500).send({ message: String(error) });
+ RED.log.error("An error occured while getting RTDB settings: ");
+ RED.log.error(error);
+ }
+}
+
+async function updateDatabaseSettings(RED: NodeAPI, got: typeof import("got").got, req: Request, res: Response) {
+ try {
+ const id = req.params.id;
+
+ if (!id) {
+ res.status(400).send("The config-node ID is missing!");
+ return;
+ }
+
+ const node = RED.nodes.getNode(id) as ConfigNode | null;
+ if (!node || !isFirebaseConfigNode(node) || !node.client) {
+ res.status(400).send("Config Node disabled or not yet deployed");
+ return;
+ }
+
+ const databaseURL = node.credentials.url;
+ const token = await node.client.getAccessToken();
+
+ const path = encodeURI(req.body.path || "defaultWriteSizeLimit");
+ const url = `${databaseURL}.settings/${path}.json`;
+ const writeSizeLimit = req.body.writeSizeLimit;
+
+ const response = await got.post(url, {
+ headers: { Authorization: `Bearer ${token?.access_token}` },
+ body: JSON.stringify(writeSizeLimit),
+ responseType: "json",
+ });
+
+ res.status(200);
+ console.log(response.body);
+ } catch (error) {
+ res.status(500).send({ message: String(error) });
+ RED.log.error("An error occured while setting RTDB settings: ");
+ RED.log.error(error);
+ }
+}
+
+export { getDatabaseSettings, updateDatabaseSettings };
diff --git a/src/nodes/firebase-config.ts b/src/nodes/firebase-config.ts
index a1f943a..e34dbd1 100644
--- a/src/nodes/firebase-config.ts
+++ b/src/nodes/firebase-config.ts
@@ -15,12 +15,13 @@
*/
import { NodeAPI } from "node-red";
+import { getDatabaseSettings, updateDatabaseSettings } from "../lib/nodes/rtdb-settings";
import { FirebaseClient } from "../lib/nodes/firebase-client";
import { Config, ConfigNode } from "../lib/nodes/types";
const VERSION = "0.2.1";
-export default function (RED: NodeAPI) {
+export default async function (RED: NodeAPI) {
/**
* Firebase Configuration Node
*
@@ -63,4 +64,21 @@ export default function (RED: NodeAPI) {
url: { type: "text" },
},
});
+
+ // Got need a dynamic import due to ESM
+ // Dynamic imports are transpiled when module is CommonJS
+ // https://github.com/microsoft/TypeScript/issues/43329
+ const got = (await new Function("return import('got')")()).got;
+
+ RED.httpAdmin.get(
+ "/firebase/config-node/rtdb/settings/:id",
+ RED.auth.needsPermission("firebase-config.write"),
+ (req, res) => getDatabaseSettings(RED, got, req, res)
+ );
+
+ RED.httpAdmin.post(
+ "/firebase/config-node/rtdb/settings/:id",
+ RED.auth.needsPermission("firebase-config.write"),
+ (req, res) => updateDatabaseSettings(RED, got, req, res)
+ );
}