UNPKG

9.25 kBJavaScriptView Raw
1/*! firebase-admin v12.0.0 */
2"use strict";
3/*!
4 * Copyright 2020 Google Inc.
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18Object.defineProperty(exports, "__esModule", { value: true });
19exports.DatabaseService = void 0;
20const url_1 = require("url");
21const path = require("path");
22const error_1 = require("../utils/error");
23const validator = require("../utils/validator");
24const api_request_1 = require("../utils/api-request");
25const index_1 = require("../utils/index");
26const TOKEN_REFRESH_THRESHOLD_MILLIS = 5 * 60 * 1000;
27class DatabaseService {
28 constructor(app) {
29 this.databases = {};
30 if (!validator.isNonNullObject(app) || !('options' in app)) {
31 throw new error_1.FirebaseDatabaseError({
32 code: 'invalid-argument',
33 message: 'First argument passed to admin.database() must be a valid Firebase app instance.',
34 });
35 }
36 this.appInternal = app;
37 }
38 get firebaseApp() {
39 return this.app;
40 }
41 /**
42 * @internal
43 */
44 delete() {
45 if (this.tokenListener) {
46 this.firebaseApp.INTERNAL.removeAuthTokenListener(this.tokenListener);
47 clearTimeout(this.tokenRefreshTimeout);
48 }
49 const promises = [];
50 for (const dbUrl of Object.keys(this.databases)) {
51 const db = this.databases[dbUrl];
52 promises.push(db.INTERNAL.delete());
53 }
54 return Promise.all(promises).then(() => {
55 this.databases = {};
56 });
57 }
58 /**
59 * Returns the app associated with this DatabaseService instance.
60 *
61 * @returns The app associated with this DatabaseService instance.
62 */
63 get app() {
64 return this.appInternal;
65 }
66 getDatabase(url) {
67 const dbUrl = this.ensureUrl(url);
68 if (!validator.isNonEmptyString(dbUrl)) {
69 throw new error_1.FirebaseDatabaseError({
70 code: 'invalid-argument',
71 message: 'Database URL must be a valid, non-empty URL string.',
72 });
73 }
74 let db = this.databases[dbUrl];
75 if (typeof db === 'undefined') {
76 // eslint-disable-next-line @typescript-eslint/no-var-requires
77 const rtdb = require('@firebase/database-compat/standalone');
78 db = rtdb.initStandalone(this.appInternal, dbUrl, (0, index_1.getSdkVersion)()).instance;
79 const rulesClient = new DatabaseRulesClient(this.app, dbUrl);
80 db.getRules = () => {
81 return rulesClient.getRules();
82 };
83 db.getRulesJSON = () => {
84 return rulesClient.getRulesJSON();
85 };
86 db.setRules = (source) => {
87 return rulesClient.setRules(source);
88 };
89 this.databases[dbUrl] = db;
90 }
91 if (!this.tokenListener) {
92 this.tokenListener = this.onTokenChange.bind(this);
93 this.firebaseApp.INTERNAL.addAuthTokenListener(this.tokenListener);
94 }
95 return db;
96 }
97 // eslint-disable-next-line @typescript-eslint/no-unused-vars
98 onTokenChange(_) {
99 const token = this.firebaseApp.INTERNAL.getCachedToken();
100 if (token) {
101 const delayMillis = token.expirationTime - TOKEN_REFRESH_THRESHOLD_MILLIS - Date.now();
102 // If the new token is set to expire soon (unlikely), do nothing. Somebody will eventually
103 // notice and refresh the token, at which point this callback will fire again.
104 if (delayMillis > 0) {
105 this.scheduleTokenRefresh(delayMillis);
106 }
107 }
108 }
109 scheduleTokenRefresh(delayMillis) {
110 clearTimeout(this.tokenRefreshTimeout);
111 this.tokenRefreshTimeout = setTimeout(() => {
112 this.firebaseApp.INTERNAL.getToken(/*forceRefresh=*/ true)
113 .catch(() => {
114 // Ignore the error since this might just be an intermittent failure. If we really cannot
115 // refresh the token, an error will be logged once the existing token expires and we try
116 // to fetch a fresh one.
117 });
118 }, delayMillis);
119 }
120 ensureUrl(url) {
121 if (typeof url !== 'undefined') {
122 return url;
123 }
124 else if (typeof this.appInternal.options.databaseURL !== 'undefined') {
125 return this.appInternal.options.databaseURL;
126 }
127 throw new error_1.FirebaseDatabaseError({
128 code: 'invalid-argument',
129 message: 'Can\'t determine Firebase Database URL.',
130 });
131 }
132}
133exports.DatabaseService = DatabaseService;
134const RULES_URL_PATH = '.settings/rules.json';
135/**
136 * A helper client for managing RTDB security rules.
137 */
138class DatabaseRulesClient {
139 constructor(app, dbUrl) {
140 let parsedUrl = new url_1.URL(dbUrl);
141 const emulatorHost = process.env.FIREBASE_DATABASE_EMULATOR_HOST;
142 if (emulatorHost) {
143 const namespace = extractNamespace(parsedUrl);
144 parsedUrl = new url_1.URL(`http://${emulatorHost}?ns=${namespace}`);
145 }
146 parsedUrl.pathname = path.join(parsedUrl.pathname, RULES_URL_PATH);
147 this.dbUrl = parsedUrl.toString();
148 this.httpClient = new api_request_1.AuthorizedHttpClient(app);
149 }
150 /**
151 * Gets the currently applied security rules as a string. The return value consists of
152 * the rules source including comments.
153 *
154 * @returns A promise fulfilled with the rules as a raw string.
155 */
156 getRules() {
157 const req = {
158 method: 'GET',
159 url: this.dbUrl,
160 };
161 return this.httpClient.send(req)
162 .then((resp) => {
163 if (!resp.text) {
164 throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INTERNAL_ERROR, 'HTTP response missing data.');
165 }
166 return resp.text;
167 })
168 .catch((err) => {
169 throw this.handleError(err);
170 });
171 }
172 /**
173 * Gets the currently applied security rules as a parsed JSON object. Any comments in
174 * the original source are stripped away.
175 *
176 * @returns {Promise<object>} A promise fulfilled with the parsed rules source.
177 */
178 getRulesJSON() {
179 const req = {
180 method: 'GET',
181 url: this.dbUrl,
182 data: { format: 'strict' },
183 };
184 return this.httpClient.send(req)
185 .then((resp) => {
186 return resp.data;
187 })
188 .catch((err) => {
189 throw this.handleError(err);
190 });
191 }
192 /**
193 * Sets the specified rules on the Firebase Database instance. If the rules source is
194 * specified as a string or a Buffer, it may include comments.
195 *
196 * @param {string|Buffer|object} source Source of the rules to apply. Must not be `null`
197 * or empty.
198 * @returns {Promise<void>} Resolves when the rules are set on the Database.
199 */
200 setRules(source) {
201 if (!validator.isNonEmptyString(source) &&
202 !validator.isBuffer(source) &&
203 !validator.isNonNullObject(source)) {
204 const error = new error_1.FirebaseDatabaseError({
205 code: 'invalid-argument',
206 message: 'Source must be a non-empty string, Buffer or an object.',
207 });
208 return Promise.reject(error);
209 }
210 const req = {
211 method: 'PUT',
212 url: this.dbUrl,
213 data: source,
214 headers: {
215 'content-type': 'application/json; charset=utf-8',
216 },
217 };
218 return this.httpClient.send(req)
219 .then(() => {
220 return;
221 })
222 .catch((err) => {
223 throw this.handleError(err);
224 });
225 }
226 handleError(err) {
227 if (err instanceof api_request_1.HttpError) {
228 return new error_1.FirebaseDatabaseError({
229 code: error_1.AppErrorCodes.INTERNAL_ERROR,
230 message: this.getErrorMessage(err),
231 });
232 }
233 return err;
234 }
235 getErrorMessage(err) {
236 const intro = 'Error while accessing security rules';
237 try {
238 const body = err.response.data;
239 if (body && body.error) {
240 return `${intro}: ${body.error.trim()}`;
241 }
242 }
243 catch {
244 // Ignore parsing errors
245 }
246 return `${intro}: ${err.response.text}`;
247 }
248}
249function extractNamespace(parsedUrl) {
250 const ns = parsedUrl.searchParams.get('ns');
251 if (ns) {
252 return ns;
253 }
254 const hostname = parsedUrl.hostname;
255 const dotIndex = hostname.indexOf('.');
256 return hostname.substring(0, dotIndex).toLowerCase();
257}