-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
Proposed Technologies
- TypeScript
-
React using Vite
- MUI
- Mantine
-
Redux
- Single source of truth
- A place to manage states across the entire application
- RTX query for fetching
-
Axios
- Fetching Library
-
React Router
- Routing Library
- React-toastify
-
Storybook
- Component Testing
-
Jest
- Other testing if required
Framework : LoopBack4
Reasoning :
- Highly flexible and widely used
- Command Line Interface (CLI) generators (easy and standardised generation of code)
- Great documentation and explanations
- Great place to get started and helpful for learning other frameworks
- Very quick to create REST api with CRUD operationsd
Cons :
- Does have quite a bit of boilerplate code - minimised by CLI generators
- Contains some advanced features - E.g. DI. Auto-done by CLI generators, but other features (besides a simple CRUD REST api) such as auth may require more knowledge of how these work
- Structure is standardised and generated using CLI generators
- https://loopback.io/doc/en/lb4/todo-tutorial-scaffolding.html
- Note that there might be extra files not listed here. The above link to the LoopBack4 Documentation explains the purpose of each file.
.
├── public
│ └── index.html
├── src
│ ├── __tests__
│ │ ├── README.md
│ │ └── acceptance
│ │ ├── home-page.acceptance.ts
│ │ ├── ping-controller.acceptance.ts
│ │ └── test-helper.ts
│ ├── controllers
│ │ ├── index.ts
│ │ ├── README.md
│ │ └── ping-controller.ts
│ ├── datasources
│ │ └── README.md
│ ├── models
│ │ └── README.md
│ ├── repositories
│ │ └── README.md
│ ├── application.ts
│ ├── index.ts
│ ├── migrate.ts
│ └── sequence.ts
├── node_modules
│ └── ***
├── LICENSE
├── README.md
├── package.json
├── tsconfig.json
├── .eslintrc.js
├── .prettierrc
└── .mocharc.json
Front-end Layers | Responsibility |
---|---|
RTK Query | HTTP Calls to back-end |
Redux | Single source of truth (State management) |
React Component/Pages | Contains the react components (Components, Pages and Styling) |
Back-end Layers | Responsibility |
---|---|
Controllers | Handles HTTP endpoint logic |
Repositories | Handles CRUD operations as well as other data manipulation to be sent/received from DB |
Datasource | Represents DB connection source |
Services | Encapsulates any business logic/operations |
Middleware | Middleware layer with logics such as logging, authentication, authorisation, etc |
DB | The database server (In-memory, MongoDB, MySQL, etc) |
For authentication/authorization, I suggest using the technologies mentioned in the LoopBack documentation. This reduces the complexity as the documentation is all provided alongside examples.
- Diverging from AUth0 due to the lack of documentation with LoopBack4 & Auth0
Starting point : https://loopback.io/doc/en/lb4/Authentication-overview.html
- Authentication is a process of verifying user/entity to the system, which enables identified/validated access to the protected routes ("Who are you?")
- Authorization is the process of deciding if a user can perform an action on a protected resource ("Knowing you, what are your rights/permissions?")
From the documentation, it is highly recommended to understand the JWT todo example. This gives an understanding of how authentication works in LoopBack.
From there, I suggest we use JS Passport for our authentication middleware. This library simplifies our authentication as we do not need to write the authentication strategies ourselves. This middleware supports hundreds of strategies.
- https://www.passportjs.org/packages/ (All the strategies that are supported)
- https://loopback.io/doc/en/lb4/Authentication-passport.html (How to use passport with loopback)
- https://github.com/loopbackio/loopback-next/tree/master/examples/passport-login (passport loopback example)
Once we have verified the user, we need to focus on authorization. E.g. Students will have different permissions compared to sponsors. We need to define rights/privileges.
- https://loopback.io/doc/en/lb4/Authorization-overview.html (Overview)
- https://loopback.io/doc/en/lb4/RBAC-with-authorization.html (Role based access control)
Note: There are plenty of other links in the documentation that I also suggest reading. These are only some of them, have a look around >:D
The most common session management used in Web API's are token based session management. A common widely used token is JSON Web Tokens (JWT Tokens). If we are using Oauth Service (Google or Microsoft), these services will provide the access token. This can be implemented in the LoopBack4 framework following this tutorial :
JSON Web Tokens :
- Carries information related to identity and claims of user
- A claim is a piece of information about a user
- JWT tokens are signed by the server to detect tampering frontend, preventing modifications of identity/claims
- JWT Tokens are embedded in headers of HTTP requests
- JWT Tokens contain three parts
- Header: Contain JWT algorithm and type
- Payload: Contain identity/claim information
- Signature: Signed Hash to detect tampering
Authorization :
- With LoopBack4, we can use Role based access control to determine roles. These roles can be incorperated with JWT tokens to determine what your role is. A following example is linked https://loopback.io/doc/en/lb4/migration-auth-access-control-example.html
Login :
- Upon successful login, a JWT token is created by the server and returned to the client
- Using external Oauth services like Google, will return a valid access token after the Oauth process is valid and complete
Logout :
- In most scenarios, once JWT tokens are issued, they are valid until they expire
- To logout, we can delete the token from the client localstorage
- If we wish to be really secure, we can create an endpoint to invalidate/revoke access tokens. External Oauth services like Google contain these endpoints. However this might be unnecessary.
Session timeout :
- Session timeouts are important to reduce the likelihood of token hijacking where a valid access token is used on behalf of someone else
- The timeout session will be handled by the external Oauth services
Client JWT token storage :
- JWT tokens are usually stored in cookies/localstorage
- The user does not need to login every time they access the webpage
Page | Student | Alumni | Sponsor | Administrator | Public |
---|---|---|---|---|---|
Landing Screen | X | X | X | X | X |
Student Signup | - | - | - | X | X |
Sponsor Signup | - | - | - | X | X |
Alumni Signup | - | - | - | X | X |
Student Profile | X | X | X | X | - |
Sponsor Profile | X | X | X | X | - |
Alumni Profile | X | X | X | X | - |
Student Profile Board | - | - | X | X | - |
Job Board | X | X | X | X | - |
Job Ad Desc | X | X | X | X | - |
Admin Dashboard | - | - | - | X | - |
Link to the Diagram: https://app.diagrams.net/#G12tjEdDwFJbMe2BvL6ioYJoVsw2O0voGX#%7B%22pageId%22%3A%22qUg-_qivASDQXejja_5J%22%7D
Refering to the diagram above on how the OAuth mechanism works - If email ID is not registered locally, automatically create a new user class and return it.
Problem: A new account will be missing extra information we want (PhoneNo, first/last name, etc)
Solution: The default new account will have a field initialized==false
. This way front-end can detect this account has not been initialized and force a setup page.
- Signing up via an external Social Media app
- Click on
Login
from the header menu, the user will see various buttons underOther login options
- When the user click on any login option, the page is redirected to that social app's login page. On successful login with the social app, the
View account
page is loaded. - If the email ID registered in the social media app does not match any email IDs registered locally, then a new user is signed up.
View account
page will display the external profile used to login under thelinked accounts
section. - Click on
Logout
to log out of user session.
- Click on
NOTE: Planned OAuth providers are Google and Microsoft
A document that details the identified design patterns, their explanations and potential applicantions within the job board application.
Purpose and Benefits: The Singleton pattern ensures that only one instance of a class exists and provides a global point of access to it. This is useful when exactly one object is needed to coordinate actions across the system.
Drawbacks: Singleton can hide dependencies in your code and can make unit testing challenging because they introduce a global state into an application.
Real-world Example: A print spooler, a database connection, a file system, or a logging class are examples where only a single instance of a class should exist.
Job Board Application Scenario: could use the Singleton pattern to manage a database connection. This would ensure that all database operations are using the same connection, which can improve performance and consistency.
Purpose and Benefits: The Builder pattern allows the construction of complex objects step by step. It separates the construction of an object from its representation, enabling the same construction process to create different representations of an object. This is useful when the object creation process is complex or when you want to have a more readable and maintainable way to create objects.
Drawbacks: The Builder pattern requires creating a separate Builder class for each different type of Product which increases the overall code complexity.
Real-world Example: The process of assembling a car is a good example of the Builder pattern. The same construction process can create different representations (models) of the car.
Job Board Application Scenario: Use the Builder pattern for creating complex user profiles or job listings. For example, a JobBuilder could allow setting various attributes like job title, required skills, and description progressively, ensuring that the job listing is built step-by-step in a controlled and consistent manner. This approach enhances code readability and maintainability, particularly when dealing with complex objects with numerous attributes.
Purpose and Benefits: The Factory pattern provides an interface for creating objects, but allows subclasses or implementing classes to decide which class to instantiate. It encapsulates the object creation logic within a separate factory class, providing a common interface for creating objects without exposing the instantiation logic to the client.
Drawbacks: The code can become more complicated since you need to introduce a lot of new subclasses which implement the factory method. The factory method in the base class should return some default type of product, but the subclasses are obligated to override the method and return a different type of product.
Real-world Example: A hiring agency is an example of a Factory pattern. The agency is a ‘factory’ that creates employees. The agency interviews the candidates, does all paperwork and returns a ready-to-work employee.
Job Board Application Scenario: Implement the Factory pattern to handle user account creation within the job board. Depending on the user role (Student, Alumni, Sponsor), different types of profiles with specific privileges and features can be generated. For instance, an Alumni account might have access to additional features, whereas a Student account may focus more on job listings and application functionalities.
Purpose and Benefits: The Observer pattern establishes a one-to-many dependency between objects, such that when one object changes its state, all its dependents are notified and updated automatically. This is useful in scenarios where multiple objects need to be notified about changes in another object’s state.
Drawbacks: The Observer pattern can lead to issues like the infinite loop and unexpected updates if not implemented carefully.
Real-world Example: A real-world example is an email subscription model where the Subject is the Email System and the Observers are the subscribers who get notified when a new email arrives.
Job Board Application Scenario: Employ the Observer pattern for real-time notifications within the job board. When a new job is posted by a sponsor, all subscribed members (Observers) can receive immediate updates. This pattern is also applicable in notifying users about changes in application status or updates on job postings they are interested in.
Purpose and Benefits: The Strategy pattern enables you to define a family of interchangeable algorithms or strategies, encapsulate each one, and make them interchangeable at runtime. It allows the algorithms to vary independently from the clients that use them.
Drawbacks: Clients must be aware of the differences between strategies to be able to select a proper one. Also, a lot of modern programming languages have functional type support that lets you implement different versions of an algorithm inside a set of anonymous functions. Therefore, you might use a function parameter instead of the whole strategy object.
Real-world Example: A real-world example is a sorting algorithm where the Context is the Sorting System and the Strategies are the different sorting algorithms (like Bubble Sort, Quick Sort, etc.) that can be used interchangeably.
Job Board Application Scenario: Use the Strategy pattern to offer different job recommendation algorithms based on user preferences. For instance, some users might prefer job recommendations based on proximity, while others might prioritise salary or company prestige. By encapsulating these algorithms within different strategy classes, the system can dynamically adjust the job recommendation logic per user preferences, enhancing personalization.
Purpose and Benefits: The MVC pattern separates the application logic into three interconnected components: the model (data and business logic), the view (user interface), and the controller (handles user input and updates the model and view). This promotes separation of concerns and facilitates modular development.
Drawbacks: Overuse of the MVC pattern can lead to an over-complicated structure as the application grows, since more and more classes are added to each part of the MVC triangle. It can also lead to slower performance as more dependencies are added.
Real-world Example: A real-world example is a music player application where the Model is the music files, the View is the user interface, and the Controller is the logic that handles user input and updates the model and view.
Job Board Application Scenario: Apply the MVC pattern to separate concerns within the job board. For instance, the Model handles data related to job postings and user profiles, the View presents this data through a user-friendly interface, and the Controller manages the flow of data between the server and UI, handling user inputs and system outputs. This separation facilitates easier maintenance and scalability of the application.
This section will be split into two parts.
Section 1: For the developer who wants to use Authorization and authentication
Section 2: For the developer who wants to know how Authorization and authentication is implemented
The following assumptions are made, otherwise this documentation will be huge.
To understand section 1:
- Understand JWT tokens and how they're used
- Understanding of the parts of a HTTP request, specifically what headers are and how authentication works in Http Requests as bearer tokens.
- Understand difference between Authorization / Authentication
- Understanding of FSAE's user models. Specfically roles and the
activate
flag
To understand section 2:
- Understanding of dependency injection
- Understanding differences between services, repositories, models, datasources
- Understanding of backend systems and how authorization and authentication works at a high level. EG Auth Strategies
- Understanding of the Loopback4 architecture, inbuilt Authentication library, inbuilt Authorization library
- Understanding of how JWT tokens work and store user data
Do you want endpoint require a user to be logged in first? This section is for you.
Do you want an endpoint to only be accessible to a certain role? Read this section, then refer to sect 1.2 authorization.
To get an access token, use one of login endpoints in login.controller.ts
. If the correct credentials are sent via HTTP POST a response similar is given
{
"userId": "1",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJyb2xlIjoiYWRtaW4iLCJhY3RpdmF0ZWQiOmZhbHNlLCJpYXQiOjE3MjAyNDA5NjMsImV4cCI6MTcyMDMyNzM2M30.t6bdkZesragtj6N7N83SaN6W3YDlQdMphh5qdhiTHrk"
}
This JWT token can be decoded with websites such at these link. For every endpoint that requires authentication, this JWT token must be passed part of the 'Authentication header' as a bearer token'. What this means.
The HTTP request, headers are a key, value pair. To use authentication
Key/Name of header is `authentication`
The value of the header is `Bearer <tokenHere>`
This is a standard, widely used across the web, so it will be similar to many other web apps out there.
Add @authenticate('fsae-jwt')
decorator. This endpoint now requires a JWT bearer token as part of the Authorization
header to be accessed.
It is important to note, this endpoint still allows user accounts that have the 'activate' flag set or unset. The sole purpose of authentication is identifying who you are, not what permissions you have.
Authorization requires authentication. Once authentication determines who you are, authorization determines what you can access.
Currently authorization is Claim based role authentication
using JWT Tokens
The role is stored within the JWT claim, alongside whether your user account is activated or not. (You can see this by decoding a JWT token)
To add authorization to your endpoint add the following decorator. You can modify the allowRoles as you wish.
@authorize({
allowedRoles: [FsaeRole.ADMIN, FsaeRole.SPONSOR, FsaeRole.ALUMNI, FsaeRole.ALUMNI],
})
By default this decorator, will reject users whose activate
flag is set to false. This can be bypassed by adding the following scope
@authorize({
allowedRoles: [FsaeRole.ADMIN, FsaeRole.SPONSOR, FsaeRole.ALUMNI, FsaeRole.ALUMNI],
scopes: ['allow-non-activated'],
})
A series of example endpoints are defined in ping.controller with different permissions.
// Unprotected endpoint. Anyone can access it
@get('/ping')
@response(200, PING_RESPONSE)
ping(): object {...}
// Protected endpoint. Anyone logged in can access it. Including deactivated users
@get('/protected-ping')
@authenticate('fsae-jwt')
// Protected endpoint, that only admins can access, and whose admin account is activated
@get('/protected-ping/admin-only')
@authenticate('fsae-jwt')
@authorize({
allowedRoles: [FsaeRole.ADMIN],
})
// Protected endpoint that all users can access, alongside non-activated accounts.
@authorize({
allowedRoles: [FsaeRole.ADMIN, FsaeRole.SPONSOR, FsaeRole.ALUMNI, FsaeRole.ALUMNI],
scopes: ['allow-non-activated'],
})