Skip to content

Commit a91fd02

Browse files
- several adjustments and refactoring
- cache removed - serialize-firestore.ts removed
1 parent d01561e commit a91fd02

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1632
-3622
lines changed

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2023 Rodrigo João Bertotti <rodrigo@wisetap.com>
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+107-124
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,6 @@ The main aspects of this sample are:
88
- Access Control: Restricting routes access with custom claims and checking nuances
99
- Reject a request outside the controller easily by throwing `new HttpResponseError(status, codeString, message)`
1010
- Logs: **winston** module is preconfigured to write `.log` files
11-
- Serialization of objects, so you can easily perform write operations on Firestore of custom prototypes objects
12-
- Caching: A wrapper function which helps to avoid identical queries on the Firestore Database
13-
14-
:iphone: **Check also the [Flutter side](https://github.com/RodrigoBertotti/flutter_client_for_api_example)
15-
example which interacts with this API example.**
1611

1712
## Summary
1813

@@ -21,13 +16,20 @@ example which interacts with this API example.**
2116
3. [Access Control](#access-control-custom-claims)
2217
4. [Errors and permissions](#errors-and-permissions)
2318
5. [Logs](#logs)
24-
6. [Serialization of objects](#serialization-of-objects)
25-
7. [Caching Firestore results](#caching-firestore-results)
26-
8. [Reference](#reference)
19+
6. [Reference](#reference)
2720

2821
## Getting Started
2922

30-
### Step 1 - Generate your `firebase-credentials.json` file
23+
# Configure the Firebase Console
24+
25+
In the Firebase Console
26+
27+
Go to Build > Authentication > Get Started > Sign-in method > Email/Password and enable Email/Password and save it.
28+
29+
Also go to Build > Firestore Database > Create database. You can choose the option `Start in test mode`.
30+
31+
32+
### Step 2 - Generate your `firebase-credentials.json` file
3133

3234
1) Go to your Firebase Project
3335
2) Click on the engine icon (on right of "Project Overview")
@@ -40,11 +42,11 @@ example which interacts with this API example.**
4042
⚠️ Keep `firebase-credentials.json` and `environment.ts` local,
4143
don't commit these files, keep both on `.gitignore`
4244

43-
As any Firebase server, the API has adminstrative priviliges,
45+
As any Firebase server, the API has administrative privileges,
4446
that means the API has full permission to perform changes on the Firestore Database (and
45-
other Firebase Resources) regardless how the Firestore Security Rules is configured.
47+
other Firebase Resources) regardless of how the Firestore Security Rules are configured.
4648

47-
### Step 2 - To test your server locally:
49+
### Step 3 - To test your server locally:
4850

4951
This command will start and restart your server as code changes are made,
5052
do not use on production
@@ -54,7 +56,7 @@ do not use on production
5456
Let's run `npm install` to install the dependencies and `npm run dev`
5557
to start your server locally on port 3000.
5658

57-
#### Other commands for production environment
59+
#### Other commands for the production environment
5860

5961
#### To build your server:
6062

@@ -64,45 +66,61 @@ to start your server locally on port 3000.
6466

6567
npm run start
6668

67-
### Step 3 - Interact with your server
69+
### Step 4 - Use Postman to test it
70+
71+
1. In the Firebase Console > Go to Project Overview and Click on the **Web** platform to Add a new Platform
72+
73+
2. Add a Nickname like "Postman" and click on Register App
74+
75+
3. Copy the **apiKey** field
76+
77+
4. Download the <a id="raw-url" download href="https://raw.githubusercontent.com/RodrigoBertotti/api-example-firebase-nodejs/dev/postman_collection.json">Postman Collection</a> and import to your Postman
6878

69-
:iphone: Check this [Flutter project](https://github.com/RodrigoBertotti/flutter_client_for_api_example)
70-
to interact with the server,
71-
you can also create your own client that uses the Firebase Authentication
72-
library, like React, Angular, Vue, etcetera.
79+
5. Test creating an account first, after that, go to the Login request
80+
example and pass the `apiKey` as query parameter
7381

74-
### Step 4 - Your time!
82+
6. Copy the `idToken` and pass it, and pass it as header of the other requests, the header name is also `idToken`.
7583

76-
We are done! Customize this API on your way!
84+
### 🚫 Permission errors
7785

78-
---
86+
- #### "Only storeOwner can access"
87+
Means you are not logged with a `buyer` claim rather
88+
than with a user that contains the `storeOwner` claim.
89+
90+
- #### "You aren't the correct storeOwner"
91+
Means you are logged with the correct claim, but you are trying to read others storeOwner's data.
92+
93+
- #### "Requires authentication"
7994

8095
## Authentication
8196

8297
Firebase Authentication is used to verify
8398
if the client is authenticated on Firebase Authentication,
84-
to do so, the client side should inform the `Authorization` header:
99+
to do so, the client side should inform the `isToken` header:
85100

86-
### `Authorization` Header
101+
### `idToken` Header
87102

88-
The client's token on Firebase Authentication in the format `Bearer <token>`,
89-
it can be obtained in the client side after the authentication is performed with the
90-
Firebase Authentication library for the client side.
103+
The client's token on Firebase Authentication,
104+
it can be obtained on the client side after the authentication is performed with the
105+
Firebase Authentication library for the client side,
91106

92-
### Flutter Client Example on how to get the `Authorization`:
107+
The `idToken` can be generated by the client side only.
93108

94-
final dioLoggedIn = Dio(BaseOptions(
95-
baseUrl: 'https://myapi.example.com',
96-
headers: {
97-
"Authorization": "Bearer ${(await FirebaseAuth.instance.currentUser!.getIdToken())}"
98-
}
99-
));
100-
// dioLoggedIn.get('/user').then(...);
109+
#### Option 1: Generating `idToken` with Postman:
101110

102-
:iphone: [Click here](https://github.com/RodrigoBertotti/flutter_client_for_api_example)
103-
to check a Flutter client example for this API
111+
Follow the previous instructions on [Step 4 - Use Postman to test it](#step-4---use-postman-to-test-it)
104112

105-
---
113+
#### Option 2: Generating `idToken` with a Flutter Client:
114+
```dart
115+
final idToken = await FirebaseAuth.instance.currentUser!.getIdToken();
116+
// use idToken as header
117+
```
118+
119+
#### Option 3: Generating `idToken` with a Web Client:
120+
```javascript
121+
const idToken = await getAuth(firebaseApp).currentUser?.getIdToken();
122+
// use idToken as header
123+
```
106124
107125
## Access Control (custom claims)
108126
@@ -111,54 +129,58 @@ define which routes the users have access to.
111129
112130
### Define custom claims to a user
113131
114-
This can be done in the server like bellow:
115-
116-
await admin.auth().setCustomUserClaims(user.uid, {
117-
storeOwner: true,
118-
buyer: false
119-
});
120-
132+
This can be done in the server like below:
133+
```javascript
134+
await admin.auth().setCustomUserClaims(user.uid, {
135+
storeOwner: true,
136+
buyer: false
137+
});
138+
```
121139
### Configuring the routes
122140
123141
You can set a param (array of strings) on the `httpServer.<method>`
124142
function, like:
125143
126-
httpServer.get ('/product/:productId/full-details',
127-
this.getProductByIdFull.bind(this), ['storeOwner']);
144+
```javascript
145+
httpServer.get (
146+
'/product/:productId/full-details',
147+
this.getProductByIdFull.bind(this), ['storeOwner']
148+
);
149+
```
128150
129151
In the example above, only users with the `storeOwner` custom claim will
130152
have access to the `/product/:productId/full-details` path.
131153
132154
Is this enough? Not always, so let's check the next section [Errors and permissions](#errors-and-permissions).
133155
134-
---
135-
136156
## Errors and permissions
137157
138158
You can easily send an HTTP response with code between 400 and 500 to the client
139159
by simply throwing a `new HttpResponseError(...)` on your controller, service or repository,
140160
for example:
141161
142-
throw new HttpResponseError(400, 'BAD_REQUEST', "Missing 'name'");
162+
```javascript
163+
throw new HttpResponseError(400, 'BAD_REQUEST', "Missing 'name' field on the body");
164+
```
143165

144-
Sometimes defining roles isn't enough to assure that a user can't
166+
Sometimes defining roles isn't enough to ensure that a user can't
145167
access or modify a specific data,
146-
let's imagine if a storeOwner tries to get full details
147-
of a product he is not selling, like a product of another storeOwner,
168+
let's imagine if a store owner tries to get full details
169+
of a product he is not selling, like a product of another store owner,
148170
he still has access to the route because of his `storeOwner` custom claim,
149171
but an additional verification is needed.
150172

151-
if (product.storeOwnerUid != req.auth!.uid) {
152-
throw new HttpResponseError(
153-
403,
154-
'FORBIDDEN',
155-
`Even though you are a storeOwner,
156-
you are a owner of another store,
157-
so you can't see full details of this product`
158-
);
159-
}
160-
161-
---
173+
```javascript
174+
if (product.storeOwnerUid != req.auth!.uid) {
175+
throw new HttpResponseError(
176+
403,
177+
'FORBIDDEN',
178+
`Even though you are a store owner,
179+
you are a owner of another store,
180+
so you can't see full details of this product`
181+
);
182+
}
183+
```
162184

163185
## Authentication fields
164186

@@ -168,8 +190,8 @@ express request handler:
168190
### `req.authenticated`
169191
type: `boolean`
170192

171-
Is true only if the client is authenticated, that means, the client
172-
informed `Authorization` on the headers, and these
193+
Is true only if the client is authenticated, which means, the client
194+
informed `idToken` on the headers, and these
173195
values were successfully validated.
174196

175197
### `req.auth`
@@ -182,19 +204,17 @@ type: [DecodedIdToken](https://firebase.google.com/docs/reference/admin/node/fir
182204

183205
If authenticated: Contains token data of Firebase Authentication.
184206

185-
---
186-
187207
## Logs
188208

189209
You can save logs into a file by importing these functions of the `src/utils/logger.ts` file
190210
and using like:
191-
192-
log("this is a info", "info");
193-
logDebug("this is a debug");
194-
logInfo("this is a info");
195-
logWarn("this is a warn");
196-
logError("this is a error");
197-
211+
```javascript
212+
log("this is a info", "info");
213+
logDebug("this is a debug");
214+
logInfo("this is a info");
215+
logWarn("this is a warn");
216+
logError("this is a error");
217+
```
198218
By default, a `logs` folder will be generated
199219
aside this project folder, in this structure:
200220

@@ -213,69 +233,32 @@ aside this project folder, in this structure:
213233
Each `.log` file contains the logs of the respective day.
214234

215235
You can also go to `src/utils/logger.ts` and check `logsFilename` and `logsPathAndFilename` fields
216-
to change the default path and filename so the logs can be saved with a different file name and
236+
to change the default path and filename so the logs can be saved with a different filename and
217237
in a different location.
218238

219239
By default, regardless of the log level, all logs will be saved in the same file,
220240
you can also change this behavior on the `winston.createLogger(transports: ...)` line of
221241
the `src/utils/logger.ts` file.
222242

223-
---
224-
225-
## Serialization of objects
226-
227-
You will get an error if you create your own class, instantiate an object of it
228-
and try to save directly on Firestore:
229-
230-
231-
await db().collection('products').doc().set(new ProductEntity(...));
232-
233-
Error: Value for argument "data" is not a valid Firestore document.
234-
Couldn't serialize object of type "ProductEntity".
235-
Firestore doesn't support JavaScript objects with custom prototypes
236-
(i.e. objects that were created via the "new" operator).
237-
238-
To fix this problem, this project has a function called `serializeFS(object)` which
239-
accepts an object as param.
243+
## Getting in touch
240244

241-
await db().collection('products').doc().set(serializeFS(new ProductEntity(...)));
245+
Feel free to open a GitHub issue about:
242246

243-
---
247+
- :grey_question: questions
244248

245-
## Caching Firestore results
249+
- :bulb: suggestions
246250

247-
Sometimes you need to fetch for the same data on the database in two or more
248-
functions, you may need to fetch for `product` in the database
249-
to check if the client has permission to read it, and if so, you may want to
250-
return the exact same `product` as response.
251+
- :ant: potential bugs
251252

252-
The commom solution is to pass the cached data as param on different functions.
253-
This project also offers an alternative way of caching:
253+
## License
254254

255-
You can use `req.cacheOf(cacheId, function)` in the request handler to wrap a function into
256-
a new function that will cache the result, in this way, a cache will be created in the
257-
first time this function is called and will be used as result when this function is called
258-
again.
259-
260-
// If there's a cache: it will use the cache, otherwise: it will wait for the getProductById result and cache it
261-
const getProductByIdCached = req
262-
.cacheOf(req.params['productId'], productsRepository.getProductById); // <-- Wrapped with a cache function
263-
const product = await getProductByIdCached(req.params['productId']);
264-
265-
The cache will be valid for a single request handler, so you will not have
266-
problems of inconsistent cache on different requests, because
267-
each request has its own cache.
268-
269-
But if the data changes, and you want to invalidate the cache on that
270-
request handler, you can call `req.invalidateCache(cacheId)`, for example:
271-
272-
req.invalidateCache(req.params['productId']);
273-
274-
Calling `req.invalidateCache` will not affect the other requests.
275-
276-
---
255+
[MIT](LICENSE)
277256

278257
## Reference
279258

280-
This project based part of the structure of the GitHub project [node-typescript-restify](https://github.com/vinicostaa/node-typescript-restify).
259+
This project used as reference part of the structure of the GitHub project [node-typescript-restify](https://github.com/vinicostaa/node-typescript-restify).
281260
Thank you [developer](https://github.com/vinicostaa/)!
261+
262+
## Contacting me
263+
264+
📧 rodrigo@wisetap.com

0 commit comments

Comments
 (0)