Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 184 additions & 3 deletions ConfiguringSystem.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,53 @@
# Configuring System Operation

## Configuration Template

You can use the following configuration template (or alternatively copy from `boilerplateConfig.json`) to add/update configurations in your `config.json`.

```json
{
"development": {
"username": "root", // required (MySQL mode)
"password": null, // required (MySQL mode)
"database": "database_development", // required (MySQL mode)
"host": "127.0.0.1", // required (MySQL mode)
"dialect": "mysql", // required ('mysql', 'sqlite')
"logging": true, // optional, default: console.log
"loggingOptions": { // optional
"logsFile": "sqlQueries.txt", // optional, default: 'sqlQueries.txt'. SQL query executions will be logged in this file if 'useFileLogging' is to true.
"useFileLogging": false, // optional, default: false. Set to true to log SQL queries to file.
"logPostBootOnly": false, // optional, default: false. Set to true to log SQL queries only after the system has booted.
"clearUponBoot": false // optional, default: false. Set to true to clear the SQL query logs file upon boot.
},
"routeLogging": false, // optional, default: false. Set to true to log all incoming requests in the console.
"routerRegistration": "manual" // optional, default: 'manual'. Set to 'automated' to automatically detect and register routes. Requires automated route export syntax.
}
}
```

## Environment Variables Template

Use this template to satisfy the required environment variables demanded by `BootCheck` and the codebase:

```env
SERVER_PORT= # Usually 8000
DEBUG_MODE=
DB_MODE=
DB_CONFIG=
EMAILING_ENABLED=
EMAIL_ADDRESS=
EMAIL_PASSWORD=
STORAGE_BUCKET_URL=
FIRESTORAGE_ENABLED=
FILEMANAGER_ENABLED=
LOGGING_ENABLED=
API_KEY=
JWT_KEY=
WS_PORT= # Usually 8080
```

## Database Configuration

There's a few ways you can configure the database the backend system uses.

`DB_MODE` is a mandatory `.env` variable that needs to be set.
Expand All @@ -13,7 +61,7 @@ Database Modes:
- Set `DB_MODE` to `sqlite` in `.env` file
- `database.sqlite` file will be auto-created in root directory and used

## MySQL Mode
### MySQL Mode

For each configuration, you need to provide:
- `username`
Expand Down Expand Up @@ -46,6 +94,139 @@ Select your configuration by changing `DB_CONFIG` in `.env` file. For example, i

The value is the same as the key of your configuration in `config/config.json`.

## Sqlite Mode
### Sqlite Mode

No configuration is needed for Sqlite mode. The system will automatically create a `database.sqlite` file in the root directory and use it.

## All Data Stores

There's quite a few places you can store data in this codebase. Here's a list of all the data stores and their purposes:

- **`SQL Database`**
- Managed by [Sequelize ORM](https://sequelize.org)
- Stores all application data
- Defined by models in `./models`
- Database initialisation and sequelize setup is done by `./models/index.js` automatically. Exports hard-imported and detected models along with `sequelize` instance.
- `./FileStore`
- Used to store files uploaded by users
- Managed by `FileManager` service at `./services/FileManager.js`
- Is programmatically enforced to have a `context.json`, a representation of all files currently in the store
- `./cache.json`
- Used to store byte-sized data for small persistence needs
- A local JSON file, so data integrity is not maintained in the situation of snapshot-based boots in the cloud
- Managed by `Cache` service at `./services/Cache.js`
- `./logs.txt`
- Used by `Logger` service at `./services/Logger.js` to log all system logs from across the entire codebase
- Logs are timestamped and stored.
- Logs are expected to have "log tags" (`ORDERS`, `LISTINGS`, `ERROR` etc.) followed by the log message. E.g: `ORDERS CONFIRMRESERVATION ERROR: Failed to create reservation; error: Sequelize connection failed.`
- `Universal.data`
- In-memory storage located in `Universal` service at `./services/Universal.js`
- Should be used for debugging purposes only

## Authentication System

### Backend View

The system uses JWT for authentication. The JWT secret is stored in the `.env` file as `JWT_KEY`.

JWTs should be signed, refreshed and verified with the `TokenManager` service at `./services/TokenManager.js`.

The system uses a middleware to authenticate requests. The middleware, `validateToken` is located at `./middleware/auth.js`. This middleware uses `TokenManager` to verify the JWT.

Payload schema of a MakanMatch JWT:
```json
{
"userID": string,
"username": string
"userType": string
}
```

Standard flow:
1. User logs in with their credentials
2. Login endpoints use `TokenManager` to sign a JWT and return it to the user
3. User sends JWT in the `Authorization` header of their requests as `Bearer <JWT>`

Token expiring soon:
1. User sends JWT that expires in 10 minutes or less in the `Authorization` header of their requests
2. `validateToken` middleware detects expiring JWT and signs a new JWT. This is inserted into the response headers as `refreshedtoken`.
3. Client replaces the expiring token with the new token detected in the response headers.

Token expired:
1. User sends expired JWT in the `Authorization` header of their requests
2. `validateToken` middleware detects expired JWT and sends a `403 Forbidden` response and indicates that their token has expired
3. Client must send new request to login endpoint to get a new JWT

### Frontend view

Frontend requests to the backend are centralised through an axios instance configured with the backend server's base URL at `./src/networking.js`.

This instance has been configured with request and response interceptors to help manage authentication and authorisation.

JWTs are stored in `localStorage` at `jwt`. These will automatically be added to the `Authorization` header of all requests by the request interceptor.

The response interceptor will detect if the JWT is expiring soon or has expired. As such, one of the following scenarios occur:
- `refreshedtoken` header detected in response
- This indicates that the JWT needs to be replaced as it's expiring.
- The new JWT is stored in `localStorage` and replaces the old JWT.
- `403 Forbidden` response detected (token expired situation only)
- If the token has expired, the `jwt` item is removed from `localStorage`.

The response interceptor successfully keeps `localStorage` up-to-date. However, the frontend uses `AuthState` (at `./src/slices/AuthState.js`), which is a redux slice, to keep track of the user's authentication state. This slice needs to be updated, so that user-dependent UI components and logic appropriately re-render/run to reflect the changes.

Thus, `AuthState.js` exports a method called `reloadAuthToken(authToken)` that should be called immediately after every request. This method updates the redux state with the new JWT, triggering a re-render of the UI.

The method takes in an auth token, from the `AuthState` redux itself, and returns a function which itself takes in a `useDispatch` hook initialisation. This dispatch hook is used to dispatch updates to the redux state, triggering the actual re-render. See `./src/slices/AuthState.js` for more information.

Thus, whenever you are writing code that makes a request to the backend using the `server` instance from `./src/networking.js`, you need to call `dispatch(reloadAuthToken(authToken))` immediately after the request, in both the `.then` and `.catch` blocks. Sample implementation:

```js
// Sample implementation of a component that needs a user to be logged in to access it

import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { reloadAuthToken } from './slices/AuthState';

function MyReactComponent() {
const navigate = useNavigate();
const dispatch = useDispatch();
const { loaded, user, authToken } = useSelector(state => state.auth);

useEffect(() => {
if (loaded == true) {
if (!user) {
console.log("User is not logged in! Re-directing to homepage.")
alert("Please sign in first.")
navigate("/")
return
}

// Example request to backend server
server.get("/somedata")
.then(res => {
dispatch(reloadAuthToken(authToken))

// Carry on with your own response processing
})
.catch(err => {
dispatch(reloadAuthToken(authToken))

// Carry on with your own error handling
})
}
}, [loaded, user])

if (loading) {
return <h1>Loading...</h1>
}

return (
<div>
{/* Your component JSX */}
<h1>Hello, {user.username}!</h1>
</div>
)
}
```

No configuration is needed for Sqlite mode. The system will automatically create a `database.sqlite` file in the root directory and use it.
In the above component, as you may have observed, if the user's token expires, the `reloadAuthToken` method will update the redux state, causing `user` to be `null`, triggering a re-render, re-running the `user`-dependent `useEffect`, redirecting the user to the homepage.
162 changes: 156 additions & 6 deletions dbTools.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
const { v4: uuidv4 } = require('uuid')
const { Admin, ChatHistory, ChatMessage, FoodListing, Guest, Host, Reservation, Review, Warning, sequelize } = require('./models')
const prompt = require("prompt-sync")({ sigint: true });
const jwt = require('jsonwebtoken');
const { Admin, ChatHistory, ChatMessage, FoodListing, Guest, Host, Reservation, Review, Warning, sequelize } = require('./models');
const Encryption = require('./services/Encryption');
require('dotenv').config()

async function resetDB() {
Expand Down Expand Up @@ -29,9 +32,144 @@ async function clearFiles() {
console.log("Files cleared!")
}

async function createHost() {
var creating = true;
var createdHostIDs = []
while (creating) {
console.log("")
console.log("Creating a new host...")

const userID = uuidv4()
try {
const host = await Host.create({
userID: userID,
email: prompt("Email (must be unique): "),
password: await Encryption.hash(prompt("Password: ")),
username: prompt("Username (must be unique): "),
contactNum: prompt("Phone number (must be unique): "),
address: prompt("Address: "),
emailVerified: prompt("Email verified? (y/n): ").toLowerCase() !== 'n',
favCuisine: prompt("Favourite cuisine (optional): ") || null,
mealsMatched: parseInt(prompt("Meals matched (optional): ")) || 0,
foodRating: parseFloat(prompt("Food rating (optional): ")) || null,
hygieneGrade: parseFloat(prompt("Hygiene grade (optional): ")) || null,
paymentImage: prompt("Payment image (optional): ") || null
})
} catch (err) {
console.log("Failed to create host; error: " + err)
creating = prompt("Try again? (y/n) ") == "y"
console.log("")
continue
}

console.log("Host created!")
console.log(`Host ID: ${userID}`)
console.log("")
createdHostIDs.push(userID)

if (prompt("Create another host? (y/n): ").toLowerCase() !== 'y') {
creating = false;
console.log("")
}
}

console.log(createdHostIDs.length + " hosts created successfully.")
}

async function createGuest() {
var creating = true;
var createdGuestIDs = []
while (creating) {
console.log("")
console.log("Creating a new guest...")

const userID = uuidv4()
try {
const guest = await Guest.create({
userID: userID,
email: prompt("Email (must be unique): "),
password: await Encryption.hash(prompt("Password: ")),
username: prompt("Username (must be unique): "),
contactNum: prompt("Phone number (must be unique) (optional): ") || null,
address: prompt("Address (optional): ") || null,
emailVerified: prompt("Email verified? (y/n): ").toLowerCase() !== 'n',
favCuisine: prompt("Favourite cuisine (optional): ") || null,
mealsMatched: parseInt(prompt("Meals matched (optional): ")) || 0
})
} catch (err) {
console.log("Failed to create guest; error: " + err)
creating = prompt("Try again? (y/n) ") == "y"
console.log("")
continue
}

console.log("Guest created!")
console.log(`Guest ID: ${userID}`)
console.log("")
createdGuestIDs.push(userID)

if (prompt("Create another guest? (y/n): ").toLowerCase() !== 'y') {
creating = false;
console.log("")
}
}

console.log(createdGuestIDs.length + " guests created successfully.")
}

async function signJWT() {
console.log("")
if (!process.env.JWT_KEY) { console.log("JWT_KEY not found in .env; aborting..."); return; }
var signMore = true;
while (signMore) {
var username = prompt("Username of account for JWT: ")
var user = null;
var userType = null;

while (user == null) {
console.log("Locating user...")
// Check in Guest
user = await Guest.findOne({ where: { username: username } })
userType = 'Guest';

// Check in Host if not found in Guest
if (!user) {
user = await Host.findOne({ where: { username: username } });
userType = 'Host';
}

// Check in Admin if not found in Guest or Host
if (!user) {
user = await Admin.findOne({ where: { username: username } })
userType = 'Admin';
}

if (!user) {
console.log("User not found. Please try again.")
username = prompt("Account username: ")
}
console.log("")
}

console.log("Signing JWT...")
const accessToken = jwt.sign(
{
userID: user.userID,
username: user.username,
userType: userType
},
process.env.JWT_KEY,
{ expiresIn: '1h' }
);
console.log("Signed JWT: " + accessToken)
console.log("")
signMore = prompt("Sign another? (y/n): ").toLowerCase() === 'y'
}
}

sequelize.sync({ alter: true })
.then(() => {
const tools = process.argv.slice(2)
.then(async () => {
const tools = (process.argv.slice(2)).map(t => t.toLowerCase())
if (tools.length == 0) {
console.log("No tool activated.")
return
Expand All @@ -40,11 +178,23 @@ sequelize.sync({ alter: true })
console.log()

if (tools.includes("reset")) {
resetDB()
await resetDB()
}

if (tools.includes("clearfiles")) {
clearFiles()
await clearFiles()
}

if (tools.includes("createhost")) {
await createHost()
}

if (tools.includes("createguest")) {
await createGuest()
}

if (tools.includes("signjwt")) {
await signJWT()
}
})
.catch(err => {
Expand Down
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const logRoutes = require('./middleware/logRoutes');

// Configure express app and chat web socket server
const app = express();
app.use(cors())
app.use(cors({ exposedHeaders: ['refreshedtoken'] }))
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
// app.use(express.static('public'))
Expand Down
Loading