-
0. Redis utils
- Inside the folder
utils
, create a fileredis.js
that contains the classRedisClient
. RedisClient
should have:- The constructor that creates a client to Redis:
- Any error of the redis client must be displayed in the console (you should use
on('error')
of the redis client). - A function
isAlive
that returnstrue
when the connection to Redis is a success otherwise,false
. - An asynchronous function
get
that takes a string key as argument and returns the Redis value stored for this key. - An asynchronous function
set
that takes a string key, a value and a duration in second as arguments to store it in Redis (with an expiration set by the duration argument). - An asynchronous function
del
that takes a string key as argument and remove the value in Redis for this key.
- After the class definition, create and export an instance of
RedisClient
calledredisClient
.
- Inside the folder
-
1. MongoDB utils
- Inside the folder
utils
, create a filedb.js
that contains the classDBClient
. DBClient
should have:- The constructor that creates a client to MongoDB:
- host: from the environment variable
DB_HOST
or default:localhost
. - port: from the environment variable
DB_PORT
or default:27017
. - database: from the environment variable
DB_DATABASE
or default:files_manager
.
- host: from the environment variable
- A function
isAlive
that returnstrue
when the connection to MongoDB is a success otherwise,false
. - An asynchronous function
nbUsers
that returns the number of documents in the collectionusers
. - An asynchronous function
nbFiles
that returns the number of documents in the collectionfiles
.
- The constructor that creates a client to MongoDB:
- After the class definition, create and export an instance of
DBClient
calleddbClient
.
- Inside the folder
-
2. First API
- Inside
server.js
, create the Express server:- It should listen on the port set by the environment variable
PORT
or by default 5000. - It should load all routes from the file
routes/index.js
.
- It should listen on the port set by the environment variable
- Inside the folder
routes
, create a fileindex.js
that contains all endpoints of our API:GET /status
=>AppController.getStatus
.GET /stats
=>AppController.getStats
.
- Inside the folder controllers, create a file AppController.js that contains the definition of the 2 endpoints:
GET /status
should return if Redis is alive and if the DB is alive too by using the 2 utils created previously:{ "redis": true, "db": true }
with a status code 200.GET /stats
should return the number of users and files in DB:{ "users": 12, "files": 1231 }
with a status code 200.users
collection must be used for counting all users.files
collection must be used for counting all files.
- Inside
-
3. Create a new user
- Now that we have a simple API, it's time to add users to our database.
- In the file
routes/index.js
, add a new endpoint:POST /users
=>UsersController.postNew
.
- Inside
controllers
, add a fileUsersController.js
that contains the new endpoint:POST /users
should create a new user in DB:- To create a user, you must specify an
email
and apassword
. - If the
email
is missing, return an errorMissing email
with a status code 400. - If the
password
is missing, return an errorMissing password
with a status code 400. - If the
email
already exists in DB, return an errorAlready exist
with a status code 400. - The error messages should be in a JSON object. For e.g.;
{"error":"Already exist"}
. - The
password
must be stored after being hashed inSHA1
. - The endpoint is returning the new user with only the
email
and theid
(auto generated by MongoDB) with a status code 201. - The new user must be saved in the collection
users
:email
: same as the value received.password
:SHA1
value of the value received.
- To create a user, you must specify an
-
4. Authenticate a user
- In the file
routes/index.js
, add 3 new endpoints:GET /connect
=>AuthController.getConnect
.GET /disconnect
=>AuthController.getDisconnect
.GET /users/me
=>UserController.getMe
.
- Inside
controllers
, add a fileAuthController.js
that contains new endpoints:GET /connect
should sign-in the user by generating a new authentication token:- By using the header
Authorization
and the technique of the Basic auth (Base64 of the<email>:<password>
), find the user associated to this email and with this password (reminder: we are storing the SHA1 of the password). - If no user has been found, return an error
Unauthorized
with a status code 401 - Otherwise:
- Generate a random string (using
uuidv4
) as token. - Create a key:
auth_<token>
. - Use this key for storing in Redis (by using the
redisClient
create previously) the user ID for 24 hours. - Return this token: Format:
{ "token": "155342df-2399-41da-9e8c-458b6ac52a0c" }
with a status code 200.
- Generate a random string (using
- By using the header
- Now, we have a way to identify a user, create a token (= avoid to store the password on any front-end) and use this token for 24h to access to the API!
- Every authenticated endpoints of our API will look at this token inside the header X-Token.
GET /disconnect
should sign-out the user based on the token:- Retrieve the user based on the token:
- If not found, return an error
Unauthorized
with a status code 401. - Otherwise, delete the token in Redis and return nothing with a status code 204.
- If not found, return an error
- Retrieve the user based on the token:
- Inside the file
controllers/UsersController.js
add a new endpoint: GET /users/me
should retrieve the user base on the token used:- Retrieve the user based on the token:
- If not found, return an error
Unauthorized
with a status code 401. - Otherwise, return the user object (
email
andid
only).
- If not found, return an error
- Retrieve the user based on the token:
- In the file
-
5. First file
- In the file routes/index.js, add a new endpoint:
POST /files
=>FilesController.postUpload
.
- Inside
controllers
, add a fileFilesController.js
that contains the new endpoint:POST /files
should create a new file in DB and in disk:- Retrieve the user based on the token:
- If not found, return an error
Unauthorized
with a status code 401.
- If not found, return an error
- To create a file, you must specify:
name
: as filename.type
: eitherfolder
,file
orimage
.parentId
: (optional) as ID of the parent (default: 0 -> the root).isPublic
: (optional) as boolean to define if the file is public or not (default: false).data
: (only fortype=file|image
) as Base64 of the file content.
- If the
name
is missing, return an errorMissing name
with a status code 400. - If the
type
is missing or not part of the list of accepted type, return an errorMissing type
with a status code 400. - If the
data
is missing andtype != folder
, return an errorMissing data
with a status code 400. - If the
parentId
is set:- If no file is present in DB for this
parentId
, return an errorParent not found
with a status code 400. - If the file present in DB for this
parentId
is not of typefolder
, return an errorParent is not a folder
with a status code 400.
- If no file is present in DB for this
- The user ID should be added to the document saved in DB - as owner of a file.
- If the type is
folder
, add the new file document in the DB and return the new file with a status code 201. - Otherwise:
- All file will be stored locally in a folder (to create automatically if not present):
- The relative path of this folder is given by the environment variable
FOLDER_PATH
. - If this variable is not present or empty, use
/tmp/files_manager
as storing folder path.
- The relative path of this folder is given by the environment variable
- Create a local path in the storing folder with filename a UUID.
- Store the file in clear (reminder:
data
contains the Base64 of the file) in this local path. - Add the new file document in the collection
files
with these attributes:userId
: ID of the owner document (owner from the authentication).name
: same as the value received.type
: same as the value received.isPublic
: same as the value received.parentId
: same as the value received - if not present: 0.localPath
: for atype=file|image
, the absolute path to the file save in local.
- Return the new file with a status code 201.
- All file will be stored locally in a folder (to create automatically if not present):
- Retrieve the user based on the token:
- In the file routes/index.js, add a new endpoint:
-
6. Get and list file
- In the file
routes/index.js
, add 2 new endpoints:GET /files/:id
=>FilesController.getShow
.GET /files
=>FilesController.getIndex
.
- In the file
controllers/FilesController.js
, add the 2 new endpoints:GET /files/:id
should retrieve the file document based on the ID:- Retrieve the user based on the token:
- If not found, return an error
Unauthorized
with a status code 401.
- If not found, return an error
- If no file document is linked to the user and the ID passed as parameter, return an error
Not found
with a status code 404. - Otherwise, return the file document.
- Retrieve the user based on the token:
GET /files
should retrieve all users file documents for a specificparentId
and with pagination:- Retrieve the user based on the token:
- If not found, return an error
Unauthorized
with a status code 401.
- If not found, return an error
- Based on the query parameters
parentId
andpage
, return the list of file document.parentId
:- No validation of
parentId
needed - if theparentId
is not linked to any user folder, returns an empty list. - By default,
parentId
is equal to 0 = the root.
- No validation of
- Pagination:
- Each page should be 20 items max.
page
query parameter starts at 0 for the first page. If equals to 1, it means it's the second page (from the 20th to the 40th), etc…- Pagination can be done directly by the
aggregate
of MongoDB.
- Retrieve the user based on the token:
- In the file
-
7. File publish/unpublish
- In the file
routes/index.js
, add 2 new endpoints:PUT /files/:id/publish
=>FilesController.putPublish
.PUT /files/:id/unpublish
=>FilesController.putUnpublish
.
- In the file
controllers/FilesController.js
, add the 2 new endpoints:PUT /files/:id/publish
should setisPublic
totrue
on the file document based on the ID:- Retrieve the user based on the token:
- If not found, return an error
Unauthorized
with a status code 401.
- If not found, return an error
- If no file document is linked to the user and the ID passed as parameter, return an error
Not found
with a status code 404. - Otherwise:
- Update the value of
isPublic
totrue
. - And return the file document with a status code 200.
- Update the value of
- Retrieve the user based on the token:
PUT /files/:id/unpublish
should setisPublic
tofalse
on the file document based on the ID:- Retrieve the user based on the token:
- If not found, return an error
Unauthorized
with a status code 401.
- If not found, return an error
- If no file document is linked to the user and the ID passed as parameter, return an error
Not found
with a status code 404. - Otherwise:
- Update the value of
isPublic
tofalse
. - And return the file document with a status code 200.
- Update the value of
- Retrieve the user based on the token:
- In the file
-
8. File data
- In the file
routes/index.js
, add one new endpoint:GET /files/:id/data
=>FilesController.getFile
.
- In the file
controllers/FilesController.js
, add the new endpoint:GET /files/:id/data
should return the content of the file document based on the ID:- If no file document is linked to the ID passed as parameter, return an error
Not found
with a status code 404. - If the file document (folder or file) is not public (
isPublic: false
) and no user authenticate or not the owner of the file, return an errorNot found
with a status code 404. - If the type of the file document is
folder
, return an errorA folder doesn't have content
with a status code 400. - If the file is not locally present, return an error
Not found
with a status code 404. - Otherwise:
- By using the module
mime-types
, get the MIME-type based on thename
of the file. - Return the content of the file with the correct MIME-type.
- By using the module
- If no file document is linked to the ID passed as parameter, return an error
- In the file
-
9. Image Thumbnails
- Update the endpoint
POST /files
endpoint to start a background processing for generating thumbnails for a file of typeimage
:- Create a
Bull
queuefileQueue
. - When a new image is stored (in local and in DB), add a job to this queue with the
userId
andfileId
.
- Create a
- Create a file
worker.js
:- By using the module
Bull
, create a queuefileQueue
. - Process this queue:
- If
fileId
is not present in the job, raise an errorMissing fileId
. - If
userId is not present in the job, raise an error
Missing userId`. - If no document is found in DB based on the
fileId
anduserId
, raise an errorFile not found
. - By using the module
image-thumbnail
, generate 3 thumbnails withwidth
= 500, 250 and 100 - store each result on the same location of the original file by appending_<width size>
.
- If
- By using the module
- Update the endpoint
GET /files/:id/data
to accept a query parametersize
:size
can be500
,250
or100
.- Based on
size
, return the correct local file. - If the local file doesn't exist, return an error
Not found
with a status code 404.
- Update the endpoint
-
10. Tests!
- Of course, a strong and stable project can not be good without tests.
- Create tests for
redisClient
anddbClient
. - Create tests for each endpoint:
GET /status
GET /stats
POST /users
GET /connect
GET /disconnect
GET /users/me
POST /files
GET /files/:id
GET /files
(don't forget the pagination)PUT /files/:id/publish
PUT /files/:id/unpublish
GET /files/:id/data
-
11. New user - welcome email
- Update the endpoint
POST /users
endpoint to start a background processing for sending a “Welcome email” to the user:- Create a
Bull
queueuserQueue
. - When a new user is stored (in DB), add a job to this queue with the
userId
.
- Create a
- Update the file worker.js:
- By using the module
Bull
, create a queueuserQueue
. - Process this queue:
- If
userId
is not present in the job, raise an errorMissing userId
. - If no document is found in DB based on the userId, raise an error
User not found
. - Print in the console
Welcome <email>!
.
- If
- By using the module
- In real life, you can use a third party service like Mailgun to send real email. These API are slow, (sending via SMTP is worst!) and sending emails via a background job is important to optimize API endpoint.
- Update the endpoint