Cloud Computing part of FitSync
Team members:
Name | ID |
---|---|
Alida Shidqiya Naifa Ulmulikhun | C248BSX4205 |
Muhammad Alfayed Dennita | C134BSY3479 |
ENVIRONMENT
[development, production]IS_LOCAL
[true, false]API_KEY
(🔐 Secret)PORT
ML_BASE_URL
STATIC_ASSETS_BUCKET
USER_PHOTOS_BUCKET
DB_HOST
DB_USERNAME
DB_PASSWORD
(🔐 Secret)DB_NAME
DB_DIALECT
ACCESS_TOKEN_PRIVATE_KEY
(🔐 Secret)REFRESH_TOKEN_PRIVATE_KEY
(🔐 Secret)
EMAIL_TRANSPORTER_HOST
(🛠️ Development/Testing)EMAIL_TRANSPORTER_PORT
(🛠️ Development/Testing)EMAIL_TRANSPORTER_SERVICE
(🌏 Production)EMAIL_TRANSPORTER_USERNAME
EMAIL_TRANSPORTER_PASSWORD
(🔐 Secret)EMAIL_TRANSPORTER_NAME
TYPESENSE_HOST
TYPESENSE_PORT
TYPESENSE_PROTOCOL
TYPESENSE_API_KEY
(🔐 Secret)
This project is used as a back-end app for the FitSync Android app.
Requirements:
- Code Editor (Recommendation: Visual Studio Code)
- Node.js v20.10.0 & NPM
- Postman
- Google Cloud Project
Environments:
- Development environment on the local machine.
- Development environment on the cloud machine (Cloud Run).
- Production environment on the cloud machine (Cloud Run).
Create a Google Cloud project on the Google Cloud Platform (GCP).
Create a Firestore database on the Firestore page. Make sure the mode should be Native.
- Create a VM instance on the Compute Engine page. If the Compute Engine API is not yet enabled, please enable it first. The VM will be used for the Typesense server. This project uses Typesense to easily index data on the Firestore. Follow the required options below.
- Boot disk -> Image: Debian GNU/Linux 11 (bullseye)
- Firewall:
- Allow HTTP traffic
- Allow HTTPS traffic
- Advanced Options -> Networking -> Network interfaces -> Edit default network -> External IPv4 address -> RESERVE STATIC EXTERNAL IP ADDRESS. After that, please note/save the static external IP address.
- Advanced Options -> Management -> Metadata:
- enable-config = TRUE
- enable-guest-attributes = TRUE
- Open the VM's terminal via SSH. Run this command to install the Typesense server.
curl -O https://dl.typesense.org/releases/0.25.1/typesense-server-0.25.1-amd64.deb sudo apt install ./typesense-server-0.25.1-amd64.deb
- Open the Typesense config by running this command.
sudo nano /etc/typesense/typesense-server.ini
- Change the
api-port
to443
and note/save theapi-key
value. - Restart the Typesense service by running this command.
sudo systemctl restart typesense-server.service
- Check the server status by running this command.
curl http://localhost:443/health
- Make sure the response is like this to show the server is running well.
{ "ok": true }
- Set up the
exercises
Typesense collection schema. To define the collection schema, you can make this request using Postman.- Method: POST
- URL: http://[YOUR_TYPESENSE_IP_ADDRESS]:443/collections
- Headers:
x-typesense-api-key
: [YOUR_TYPESENSE_API_KEY]
- Body (JSON):
{ "name": "exercises", "enable_nested_fields": true, "fields": [ { "name": "title", "type": "string", "sort": true }, { "name": "type", "type": "string" }, { "name": "level", "type": "string" }, { "name": "gender", "type": "string" }, { "name": "bodyPart", "type": "string" }, { "name": "desc", "type": "string" }, { "name": "jpg", "type": "string" }, { "name": "gif", "type": "string" }, { "name": "duration.sec", "type": "string", "optional": true }, { "name": "duration.rep", "type": "string", "optional": true }, { "name": "duration.set", "type": "string", "optional": true }, { "name": "duration.min", "type": "string", "optional": true }, { "name": "duration.desc", "type": "string", "optional": true } ] }
- The next step is integrating the Typesense with the Firestore database. Open the Firebase site and add the GCP project as a Firebase project.
- Add the Firestore/Firebase Typesense Search Extension by opening this link. After that, choose the project. Then, follow all the installation instructions until the extension is installed successfully. For the extension configuration, you can follow these options:
- Firestore Collection Path: exercises
- Typesense Hosts: [YOUR_TYPESENSE_IP_ADDRESS]
- Typesense API Key: [YOUR_TYPESENSE_API_KEY] (Then, click the
CREATE SECRET
button) - Typesense Collection Name: exercises
- Flatten Nested Documents: No
- Cloud Functions Location: Jakarta (asia-southeast2)
- Open the Cloud Functions page. Click the
ext-firestore-typesense-search-backfillToTypesenseFromFirestore
function. After that, click theEDIT
button. Add this new environment variable and deploy the new version:- TYPESENSE_PROTOCOL = http
- Do the same for the
ext-firestore-typesense-search-indexToTypesenseOnFirestoreWrite
function.
Local Machine
You can create a MySQL database on the local machine using any stacks, such as XAMPP, LAMP, Laragon, etc. Create a database with the name main_api
or anything.
Cloud Machine
You can create a MySQL database on GCP using Cloud SQL.
- Open the Cloud SQL page.
- Click the
CREATE INSTANCE
button. - Choose MySQL.
- Set up the database configuration as you need. Make sure to note/save the
root
's password. - After finishing the installation, you can note/save the instance's Connection name.
- Finally, you need to create a database on that instance. Open the Databases tab and click the
CREATE DATABASE
button. Create a database with the namemain_api
or anything.
- Open the Cloud Storage page.
- Create two buckets with these required options:
- First bucket (for storing static assets)
- Name: [Fill in the unique global name]
- Class: Standard
- (Uncheck) Enforce public access prevention on this bucket
- Access Control: Fine-grained
- Second bucket (for storing user photos)
- Name: [Fill in the unique global name]
- Class: Standard
- (Uncheck) Enforce public access prevention on this bucket
- Access Control: Uniform
- First bucket (for storing static assets)
- Please note/save all the buckets' name.
- Upload this file on the first bucket. Make the file accessible to the public by adding
allUsers
as aReader
on the access. - Make the second bucket accessible to the public by adding
allUsers
as a new principal with theStorage Object Viewer
role.
Local Machine
Important
Before configuring on the local machine, you need to clone this repository first. Run the command below on your local machine to clone this project repository.
git clone https://github.com/CH2-PS020-FitSync/CH2-PS020-CC.git
You can define the environment variables by creating the .env
file on the project root directory. The list of the variables can be found in this section. For reference, you can follow this example.
# General
ENVIRONMENT='development'
IS_LOCAL='true'
API_KEY='[CREATE_YOUR_CUSTOM_API_KEY]'
PORT='8080'
# URLs
ML_BASE_URL='[SEE_ML_TEAM_API_DOCUMENTATION]'
# Cloud Storage
STATIC_ASSETS_BUCKET='[FIRST_BUCKET_NAME]'
USER_PHOTOS_BUCKET='[SECOND_BUCKET_NAME]'
# Database
DB_HOST='localhost'
DB_USERNAME='root'
DB_PASSWORD=''
DB_NAME='main_api'
DB_DIALECT='mysql'
# JWT
ACCESS_TOKEN_PRIVATE_KEY='[CREATE_YOUR_CUSTOM_PRIVATE_KEY]'
REFRESH_TOKEN_PRIVATE_KEY='[CREATE_YOUR_CUSTOM_PRIVATE_KEY]'
# Email Transporter
EMAIL_TRANSPORTER_HOST='smtp.ethereal.email'
EMAIL_TRANSPORTER_PORT='587'
EMAIL_TRANSPORTER_USERNAME='[email protected]'
EMAIL_TRANSPORTER_PASSWORD='strongest_password'
EMAIL_TRANSPORTER_NAME='FitSync'
# Typesense
TYPESENSE_HOST='[PUT_YOUR_TYPESENSE_EXTERNAL_IP_ADDRESS]'
TYPESENSE_PORT='443'
TYPESENSE_PROTOCOL='http'
TYPESENSE_API_KEY='[PUT_YOUR_TYPESENSE_API_KEY]'
You can get the ML_BASE_URL
value by visiting the Machine Learning team repository.
Cloud Machine
Important
Before configuring on the cloud machine, you need to duplicate this repository to your repository first. For the simple, you can fork this repository.
Basically, setting up the environment variables on the cloud machine is the same as on the local machine. However, for the secret variables, you need to store them first in the Secret Manager. You are free to define the secret variables' name. But, if you want to keep everything by default as stated in the cloudbuild.dev.yaml or cloudbuild.prod.yaml files, you can follow these variables' name.
# Development
API_KEY=fitsync-main-api-API_KEY
DB_PASSWORD=fitsync-main-api-DB_PASSWORD
ACCESS_TOKEN_PRIVATE_KEY=fitsync-main-api-ACCESS_TOKEN_PRIVATE_KEY
REFRESH_TOKEN_PRIVATE_KEY=fitsync-main-api-REFRESH_TOKEN_PRIVATE_KEY
EMAIL_TRANSPORTER_PASSWORD=fitsync-main-api-EMAIL_TRANSPORTER_PASSWORD
TYPESENSE_API_KEY=firestore-typesense-search-TYPESENSE_API_KEY
# Production
API_KEY=fitsync-main-api-API_KEY
DB_PASSWORD=fitsync-main-api-PROD_DB_PASSWORD
ACCESS_TOKEN_PRIVATE_KEY=fitsync-main-api-ACCESS_TOKEN_PRIVATE_KEY
REFRESH_TOKEN_PRIVATE_KEY=fitsync-main-api-REFRESH_TOKEN_PRIVATE_KEY
EMAIL_TRANSPORTER_PASSWORD=fitsync-main-api-PROD_EMAIL_TRANSPORTER_PASSWORD
TYPESENSE_API_KEY=firestore-typesense-search-TYPESENSE_API_KEY
For the non-secret variables, you can store them on the env.dev.yaml for the development environment and env.prod.yaml for the production environment.
To setup the SQL connection, you need to change the --add-cloudsql-instances
argument value in the cloudbuild.dev.yaml or cloudbuild.prod.yaml files. After that, you need to change the DB_HOST
variable value in the env.dev.yaml or env.prod.yaml files with the /cloudsql/YOUR_SQL_CONNECTION_NAME
format.
Before you run the application in the development or production environment, you must create the service account first. To create the service account, you can follow these steps.
- Open the Service Accounts page, then click the
CREATE SERVICE ACCOUNT
button. - Fill in the service account name and ID, then click the
CREATE AND CONTINUE
button. - Add these roles to the service account.
- Cloud SQL Client
- Firebase Admin SDK Administrator Service Agent
- Secret Manager Secret Accessor
- Storage Object Admin
- After that, please note/save the service account email.
Local Machine
To use the service account, you need to download the key first.
- Open the Service Accounts page, then find the service account email.
- Click the three-dots action button on the right of the service account, then click the
Manage keys
button. - After that, you can click the
ADD KEY
button, then click theCreate new key
button to download the key. Choose the JSON key type. - After downloading the service account key, you can rename the file to
main-api-cloud-run.json
, and then place it in thekeys
folder on the project root directory. The key location should bekeys/main-api-cloud-run.json
.
Cloud Machine
You can use the service account by updating the --service-account
argument value in the cloudbuild.dev.yaml or cloudbuild.prod.yaml files.
Local Machine
Run this command in the project root directory to run the app.
npm run start
If you're actively developing the app and need a hot reload, you can run this command.
npm run start-dev
Since this project uses Sequelize as an ORM, we provide the model synchronization options.
- Default: Creates the table if it doesn't exist (and does nothing if it already exists).
npm run start-dev
- Force the database: Creates the table, dropping it first if it already existed.
npm run start-dev-force
- Alter the database: Checks what is the current state of the table in the database (which columns it has, what are their data types, etc), and then performs the necessary changes in the table to make it match the model.
npm run start-dev-alter
Cloud Machine
You can host this project app on the Cloud Run because you only pay when the app serves requests. To easily deploy and run the app, you can set up a continuous deployment flow first.
- Enable the Cloud Run API and Cloud SQL Admin API.
- Open the Cloud Build Service Account Settings, then enable these GCP services:
- Cloud Run
- Service Accounts
- Please note/save the Cloud Build service account email that is stated on the Service Account Settings page.
- Move to the Cloud Build dashboard page. Then, click the
SET UP BUILD TRIGGERS
button. - You can specify how the Cloud Build will be triggered. For reference, you can follow these options:
- Name: [Fill in a unique name]
- Region: global (Global)
- Event: Push new tag
- Source -> Repository: [Connect it to your repository]
- Source -> Tag: [Define your RegEx rule]
- Configuration -> Type: Cloud Build configuration file (yaml or json)
- Configuration -> Location: Repository
- Configuration -> Cloud build configuration file location:
cloudbuild.dev.yaml
orcloudbuild.prod.yaml
- Build logs: (Check) Send build logs to GitHub
- Service account -> Service account email: [Put the Cloud Build service account email]
- Now, you can push a new tag on your repository to trigger the build process.
- Once the build process is complete, you can open the Cloud Run page to see the deployed service.
- Click on the service. Now, you can see the service is running. You can use the app with the service URL.
Base URL:
- 🛠️ Development: https://fitsync-main-api-k3bfbgtn5q-et.a.run.app
- 🌏 Production: https://prod-fitsync-main-api-k3bfbgtn5q-et.a.run.app
Global Headers:
Content-Type
: STRING - 🔸Required- ['application/json', 'application/x-www-form-urlencoded']
x-api-key
: STRING - 🔸Required- Value: See API Key
x-smtp-host
: STRING (required for development)- Value: Get at https://ethereal.email
x-smtp-port
: INTEGER (required for development)- Value: Get at https://ethereal.email
x-smtp-username
: STRING (required for development)- Value: Get at https://ethereal.email
x-smtp-password
: STRING (required for development)- Value: Get at https://ethereal.email
Responses Format:
🟢 Success
{
"status": "success",
"message": "<string>",
"<data>": "<object>/<array>/<string>"
}
Data list:
- user
- bmis
- bmi
- workouts
- workout
- exercises
- exercise
- nutrition
🔴 Fail
{
"status": "fail",
"message": "<string>",
"<data>": "<object>/<array>/<string>"
}
Data list:
- error
- validationErrors
🔴 Error
{
"status": "error",
"message": "<string>"
}
Global Possible Responses:
🔴 400 Bad Request
{
"status": "fail",
"message": "Validation error.",
"validationErrors": [
{
"type": "<string>",
"msg": "<string>",
"path": "<string>",
"location": "<string>"
}
]
}
🔴 401 Unauthorized
{
"status": "fail",
"message": "API key is invalid."
}
🔴 500 Internal Server Error
{
"status": "error",
"message": "Internal server error."
}
Endpoint: /auth/register
Body:
email
: STRING - 🔸Required- Should be a valid format.
- Shouldn't have been used and verified.
password
: STRING - 🔸Required- Min. length: 8.
passwordConfirmation
: STRING - 🔸Required- Should be matched
password
.
- Should be matched
name
: STRING - 🔹Optionalgender
: STRING - 🔹Optional- ['male', 'female'] (case insensitive).
birthDate
: STRING - 🔹Optional- Format: YYYY-MM-DD or YYYY/MM/DD (UTC+0).
level
: STRING - 🔹Optional- ['beginner', 'intermediate', 'expert'] (case insensitive).
goalWeight
: FLOAT - 🔹Optionalheight
: FLOAT - 🔹Optional- Should paired with
weight
.
- Should paired with
weight
: FLOAT - 🔹Optional- Should paired with
height
.
- Should paired with
Possible Responses:
🟢 201 Created or 200 OK
{
"status": "success",
"message": "User registered successfully. OTP code sent.",
"user": {
"id": "<string>"
}
}
Endpoint: /auth/register/otp
Body:
userId
: STRING - 🔸Required- User should exist.
code
: STRING - 🔸Required- Length: 4.
Possible Responses:
🟢 200 OK
{
"status": "success",
"message": "User successfully verified.",
"user": {
"id": "<string>"
}
}
🔴 400 Bad Request
{
"status": "fail",
"message": "User doesn't have an active OTP code."
}
🔴 400 Bad Request
{
"status": "fail",
"message": "Session expired."
}
🔴 400 Bad Request
{
"status": "fail",
"message": "OTP code is incorrect."
}
Endpoint: /auth/login
Body:
email
: STRING - 🔸Required- Should be a valid format.
- User should exist.
- User should be verified.
password
: STRING - 🔸Required
Possible Responses:
🟢 200 OK
{
"status": "success",
"message": "User successfully logged in. Access token and refresh token created.",
"user": {
"id": "<string>",
"accessToken": "<string>",
"refreshToken": "<string>"
}
}
🔴 400 Bad Request
{
"status": "fail",
"message": "Password is incorrect."
}
Endpoint: /auth/logout
Headers:
Authorization
: STRING - 🔸Required- Bearer + Access token.
- "Bearer {accessToken}"
Possible Responses:
🟢 200 OK
{
"status": "success",
"message": "User successfully logged out. Refresh token destroyed.",
"user": {
"id": "<string>"
}
}
🔴 401 Unauthorized
{
"status": "fail",
"message": "Unauthorized. Need access token."
}
🔴 400 Bad Request
{
"status": "fail",
"message": "User already logged out."
}
Endpoint: /auth/refresh-token
Body:
refreshToken
: STRING - 🔸Required- Should exist.
Possible Responses:
🟢 201 Created
{
"status": "success",
"message": "Access token updated.",
"user": {
"id": "<string>",
"accessToken": "<string>",
"refreshToken": "<string>"
}
}
🔴 400 Bad Request
{
"status": "fail",
"message": "Can't create access token.",
"error": "<string>"
}
Endpoint: /auth/forgot-password/request
Body:
email
: STRING - 🔸Required- Should be a valid format.
- User should exist.
- User should be verified.
Possible Responses:
🟢 200 OK
{
"status": "success",
"message": "OTP code successfully sent.",
"user": {
"id": "<string>"
}
}
Endpoint: /auth/forgot-password/otp
Body:
userId
: STRING - 🔸Required- User should exist.
- User should be verified.
code
: STRING - 🔸Required- Length: 4.
Possible Responses:
🟢 200 OK
{
"status": "success",
"message": "Verification success. User ready to change their password.",
"user": {
"id": "<string>"
}
}
🔴 400 Bad Request
{
"status": "fail",
"message": "User doesn't have an active OTP code."
}
🔴 400 Bad Request
{
"status": "fail",
"message": "Session expired."
}
🔴 400 Bad Request
{
"status": "fail",
"message": "OTP code is incorrect."
}
Endpoint: /auth/forgot-password/change
Body:
userId
: STRING - 🔸Required- User should exist.
- User should be verified.
password
: STRING - 🔸Required- Min. length: 8.
passwordConfirmation
: STRING - 🔸Required- Should be matched
password
.
- Should be matched
Possible Responses:
🟢 200 OK
{
"status": "success",
"message": "User's password successfully changed.",
"user": {
"id": "<string>"
}
}
Endpoint: /auth/otp/refresh
Body:
userId
: STRING - 🔸Required- User should exist .
Possible Responses:
🟢 200 OK
{
"status": "success",
"message": "OTP code successfully refreshed and sent.",
"user": {
"id": "<string>"
}
}
🔴 400 Bad Request
{
"status": "fail",
"message": "User doesn't have an active OTP code."
}
Subglobal Headers:
Authorization
: STRING - 🔸Required- Bearer + Access token.
- "Bearer {accessToken}"
Subglobal Possible Responses:
🔴 401 Unauthorized
{
"status": "fail",
"message": "Unauthorized. Need access token."
}
🔴 401 Unauthorized
{
"status": "fail",
"message": "Access token expired. Please refresh it.",
"error": "jwt expired"
}
Endpoint: /me
Possible Responses:
🟢 200 OK
{
"status": "success",
"message": "User successfully retrieved.",
"user": {
"id": "<string>",
"email": "<string>",
"isVerified": "<boolean>",
"name": "<string>",
"gender": "<string>",
"birthDate": "<string>",
"level": "<string>",
"goalWeight": "<float>",
"photoUrl": "<string>",
"createdAt": "<string>",
"updatedAt": "<string>",
"latestBMI": {
"id": "<integer>",
"height": "<float>",
"weight": "<float>",
"date": "<string>",
"createdAt": "<string>",
"updatedAt": "<string>"
}
}
}
Endpoint: /me
Body:
name
: STRING - 🔹Optionalgender
: STRING - 🔹Optional- ['male', 'female'] (case insensitive).
birthDate
: STRING - 🔹Optional- Format: YYYY-MM-DD or YYYY/MM/DD (UTC+0).
level
: STRING - 🔹Optional- ['beginner', 'intermediate', 'expert'] (case insensitive).
goalWeight
: FLOAT - 🔹Optionalheight
: FLOAT - 🔹Optional- Should paired with
weight
.
- Should paired with
weight
: FLOAT - 🔹Optional- Should paired with
height
.
- Should paired with
Possible Responses:
🟢 200 OK
{
"status": "success",
"message": "User successfully patched.",
"user": {
"id": "<string>",
"email": "<string>",
"isVerified": "<boolean>",
"name": "<string>",
"gender": "<string>",
"birthDate": "<string>",
"level": "<string>",
"goalWeight": "<float>",
"photoUrl": "<string>",
"createdAt": "<string>",
"updatedAt": "<string>",
"latestBMI": {
"id": "<integer>",
"height": "<float>",
"weight": "<float>",
"date": "<string>",
"createdAt": "<string>",
"updatedAt": "<string>"
}
}
}
Note
The image will be converted & compressed into JPG format with a size of 256×256 pixels.
Endpoint: /me/photo
Body:
photo
: FILE - 🔸Required- MIME types: ['image/png', 'image/jpeg'].
- Max. size: 2MB.
Possible Responses:
🟢 200 OK
{
"status": "success",
"message": "User's photo successfully changed.",
"user": {
"id": "<string>",
"photoUrl": "<string>"
}
}
🔴 400 Bad Request
{
"status": "fail",
"message": "Please upload the photo."
}
🔴 400 Bad Request
{
"status": "fail",
"message": "Photo MIME type should be [image/png, image/jpeg]."
}
🔴 400 Bad Request
{
"status": "fail",
"message": "Photo size can't be larger than 2MB."
}
🔴 500 Internal Server Error
{
"status": "error",
"message": "<string>"
}
Endpoint: /me/bmis
Query Parameters:
dateFrom
: STRING - 🔹Optional- Format: ISO 8601, YYYY-MM-DDTHH:mm:ssZ (UTC+0).
dateTo
: STRING - 🔹Optional- Format: ISO 8601, YYYY-MM-DDTHH:mm:ssZ (UTC+0).
orderType
: STRING - 🔹Optional- ['asc', 'desc'] (case insensitive).
- Order by
date
.
limit
: INTEGER - 🔹Optional- Min. value: 0.
- Default value: 10.
- Set 0 to disable limit.
- Set >0 to enable limit.
offset
: INTEGER - 🔹Optional- Min. value: 1.
Possible Responses:
🟢 200 OK
{
"status": "success",
"message": "User's BMIs successfully retrieved.",
"bmis": [
{
"id": "<integer>",
"height": "<float>",
"weight": "<float>",
"date": "<string>",
"createdAt": "<string>",
"updatedAt": "<string>"
}
]
}
Endpoint: /me/bmis/{id}
Path Parameters:
id
: STRING/INTEGER - 🔸Required- BMI's id.
- BMI should exist.
Possible Responses:
🟢 200 OK
{
"status": "success",
"message": "User's BMI successfully retrieved.",
"bmi": {
"id": "<integer>",
"height": "<float>",
"weight": "<float>",
"date": "<string>",
"createdAt": "<string>",
"updatedAt": "<string>"
}
}
🔴 403 Forbidden
{
"status": "fail",
"message": "Forbidden."
}
Endpoint: /me/bmis
Body:
height
: FLOAT - 🔸Requiredweight
: FLOAT - 🔸Requireddate
: STRING - 🔹Optional- Format: ISO 8601, YYYY-MM-DDTHH:mm:ssZ (UTC+0).
- Default value: current date & time.
Possible Responses:
🟢 201 Created or 200 OK
{
"status": "success",
"message": "User's BMI succesfully added/updated.",
"bmi": {
"id": "<integer>",
"height": "<float>",
"weight": "<float>",
"date": "<string>",
"createdAt": "<string>",
"updatedAt": "<string>"
}
}
Endpoint: /me/bmis/many
Body:
bmis
: ARRAY - 🔸Requiredheight
: FLOAT - 🔸Requiredweight
: FLOAT - 🔸Requireddate
: STRING - 🔹Optional- Format: ISO 8601, YYYY-MM-DDTHH:mm:ssZ (UTC+0).
- Default value: current date & time.
Raw Body:
{
"bmis": [
{
"height": "<float>",
"weight": "<float>",
"date": "<string>"
}
]
}
Possible Responses:
🟢 200 OK
{
"status": "success",
"message": "User's BMIs succesfully added.",
"bmis": [
{
"id": "<integer>",
"height": "<float>",
"weight": "<float>",
"date": "<string>",
"createdAt": "<string>",
"updatedAt": "<string>"
}
]
}
Endpoint: /me/workouts
Query Parameters:
detail
: BOOLEAN - 🔹Optional- [true, false, 0, 1].
dateFrom
: STRING - 🔹Optional- Format: ISO 8601, YYYY-MM-DDTHH:mm:ssZ (UTC+0).
dateTo
: STRING - 🔹Optional- Format: ISO 8601, YYYY-MM-DDTHH:mm:ssZ (UTC+0).
ratingFrom
: INTEGER - 🔹Optional- Range: 1-10.
- Should be lesser than
ratingTo
.
ratingTo
: INTEGER - 🔹Optional- Range: 1-10.
- Should be greater than
ratingFrom
.
orderType
: STRING - 🔹Optional- ['asc', 'desc'] (case insensitive).
- Order by
date
.
limit
: INTEGER - 🔹Optional- Min. value: 0.
- Default value: 10.
- Set 0 to disable limit.
- Set >0 to enable limit.
offset
: INTEGER - 🔹Optional- Min. value: 1.
Possible Responses:
🟢 200 OK
{
"status": "success",
"message": "User's workouts successfully retrieved.",
"workouts": [
{
"id": "<integer>",
"exerciseId": "<string>",
"rating": "<integer>",
"date": "<string>",
"createdAt": "<string>",
"updatedAt": "<string>"
}
]
}
🟢 200 OK (Detail)
{
"status": "success",
"message": "User's workouts successfully retrieved.",
"workouts": [
{
"id": "<integer>",
"exercise": {
"id": "<string>",
"title": "<string>",
"type": "<string>",
"level": "<string>",
"gender": "<string>",
"bodyPart": "<string>",
"desc": "<string>",
"jpg": "<string>",
"gif": "<string>",
"duration": {
"sec": "<string>",
"rep": "<string>",
"set": "<string>",
"min": "<string>",
"desc": "<string>"
}
},
"rating": "<integer>",
"date": "<string>",
"createdAt": "<string>",
"updatedAt": "<string>"
}
]
}
Endpoint: /me/workouts/{id}
Path Parameters:
id
: STRING/INTEGER - 🔸Required- Workout's id.
- Workout should exist.
Query Parameters:
detail
: BOOLEAN - 🔹Optional- [true, false, 0, 1].
Possible Responses:
🟢 200 OK
{
"status": "success",
"message": "User's workout successfully retrieved.",
"workout": {
"id": "<integer>",
"exerciseId": "<string>",
"rating": "<integer>",
"date": "<string>",
"createdAt": "<string>",
"updatedAt": "<string>"
}
}
🟢 200 OK (Detail)
{
"status": "success",
"message": "User's workout successfully retrieved.",
"workout": {
"id": "<integer>",
"exercise": {
"id": "<string>",
"title": "<string>",
"type": "<string>",
"level": "<string>",
"gender": "<string>",
"bodyPart": "<string>",
"desc": "<string>",
"jpg": "<string>",
"gif": "<string>",
"duration": {
"sec": "<string>",
"rep": "<string>",
"set": "<string>",
"min": "<string>",
"desc": "<string>"
}
},
"rating": "<integer>",
"date": "<string>",
"createdAt": "<string>",
"updatedAt": "<string>"
}
}
🔴 403 Forbidden
{
"status": "fail",
"message": "Forbidden."
}
Endpoint: /me/workouts
Body:
exerciseId
: STRING - 🔸Required- Exercise should exist.
rating
: INTEGER - 🔹Optional- Range: 1-10.
date
: STRING - 🔹Optional- Format: ISO 8601, YYYY-MM-DDTHH:mm:ssZ (UTC+0).
- Default value: current date & time.
Possible Responses:
🟢 201 Created
{
"status": "success",
"message": "User's workout successfully added.",
"workout": {
"id": "<integer>",
"exerciseId": "<string>",
"rating": "<integer>",
"date": "<string>",
"createdAt": "<string>",
"updatedAt": "<string>"
}
}
Endpoint: /me/workouts/many
Body:
workouts
: ARRAY - 🔸RequiredexerciseId
: STRING - 🔸Required- Exercise should exist.
rating
: INTEGER - 🔹Optional- Range: 1-10.
date
: STRING - 🔹Optional- Format: ISO 8601, YYYY-MM-DDTHH:mm:ssZ (UTC+0).
- Default value: current date & time.
Raw Body:
{
"workouts": [
{
"exerciseId": "<string>",
"rating": "<integer>",
"date": "<string>"
}
]
}
Possible Responses:
🟢 201 Created
{
"status": "success",
"message": "User's workout successfully added.",
"workouts": [
{
"id": "<integer>",
"exerciseId": "<string>",
"rating": "<integer>",
"date": "<string>",
"createdAt": "<string>",
"updatedAt": "<string>"
}
]
}
Endpoint: /me/recommendation/exercises
Possible Responses:
🟢 200 OK
{
"status": "success",
"message": "User's exercises recommendation successfully retrieved.",
"exercises": [
{
"id": "<string>",
"title": "<string>",
"type": "<string>",
"level": "<string>",
"gender": "<string>",
"bodyPart": "<string>",
"desc": "<string>",
"jpg": "<string>",
"gif": "<string>",
"duration": {
"sec": "<string>",
"rep": "<string>",
"set": "<string>",
"min": "<string>",
"desc": "<string>"
}
}
]
}
🔴 503 Service Unavailable
{
"status": "error",
"message": "Failed to get recommendation.",
"error": "ML API Error: <string>"
}
Endpoint: /me/recommendation/nutrition
Possible Responses:
🟢 200 OK
{
"status": "success",
"message": "User's nutrition recommendation successfully retrieved.",
"nutrition": {
"estimatedCalories": "<float>",
"estimatedCarbohydrates": "<float>",
"estimatedFat": "<float>",
"estimatedProteinMean": "<float>"
}
}
🔴 503 Service Unavailable
{
"status": "error",
"message": "Failed to get recommendation.",
"error": "ML API Error: <string>"
}
Endpoint: /exercises
Query Parameters:
title
: STRING - 🔹Optionaltype
: STRING - 🔹Optional- ['strength', 'stretching', 'aerobic'] (case insensitive).
level
: STRING - 🔹Optional- ['beginner', 'intermediate', 'expert'] (case insensitive).
gender
: STRING - 🔹Optional- ['male', 'female'] (case insensitive).
orderType
: STRING - 🔹Optional- ['asc', 'desc'] (case insensitive).
- Order by
title
.
limit
: INTEGER - 🔹Optional- Min. value: 0.
- Default value: 10.
- Set 0 to disable limit.
- Set >0 to enable limit.
offset
: INTEGER - 🔹Optional- Min. value: 1.
Possible Responses:
🟢 200 OK
{
"status": "success",
"message": "Exercises successfully retrieved.",
"exercises": [
{
"id": "<string>",
"title": "<string>",
"type": "<string>",
"level": "<string>",
"gender": "<string>",
"bodyPart": "<string>",
"desc": "<string>",
"jpg": "<string>",
"gif": "<string>",
"duration": {
"sec": "<string>",
"rep": "<string>",
"set": "<string>",
"min": "<string>",
"desc": "<string>"
}
}
]
}
Endpoint: /exercises/{id}
Path Parameters:
id
: STRING - 🔸Required- Exercise's id.
- Exercise should exist.
Possible Responses:
🟢 200 OK
{
"status": "success",
"message": "Exercise successfully retrieved.",
"exercise": {
"id": "<string>",
"title": "<string>",
"type": "<string>",
"level": "<string>",
"gender": "<string>",
"bodyPart": "<string>",
"desc": "<string>",
"jpg": "<string>",
"gif": "<string>",
"duration": {
"sec": "<string>",
"rep": "<string>",
"set": "<string>",
"min": "<string>",
"desc": "<string>"
}
}
}
{
"type": "<string>",
"value": "<string>",
"msg": "<string>",
"path": "<string>",
"location": "<string>"
}
{
"id": "<string>",
"email": "<string>",
"isVerified": "<boolean>",
"name": "<string>",
"gender": "<string>",
"birthDate": "<string>",
"level": "<string>",
"goalWeight": "<float>",
"photoUrl": "<string>",
"createdAt": "<string>",
"updatedAt": "<string>",
"latestBMI": {
"id": "<integer>",
"height": "<float>",
"weight": "<float>",
"date": "<string>",
"createdAt": "<string>",
"updatedAt": "<string>"
},
"accessToken": "<string>",
"refreshToken": "<string>"
}
{
"id": "<integer>",
"height": "<float>",
"weight": "<float>",
"date": "<string>",
"createdAt": "<string>",
"updatedAt": "<string>"
}
{
"id": "<integer>",
"exerciseId": "<string>",
"rating": "<integer>",
"date": "<string>",
"createdAt": "<string>",
"updatedAt": "<string>"
}
{
"id": "<string>",
"title": "<string>",
"type": "<string>",
"level": "<string>",
"gender": "<string>",
"bodyPart": "<string>",
"desc": "<string>",
"jpg": "<string>",
"gif": "<string>",
"duration": {
"sec": "<string>",
"rep": "<string>",
"set": "<string>",
"min": "<string>",
"desc": "<string>"
}
}
{
"estimatedCalories": "<float>",
"estimatedCarbohydrates": "<float>",
"estimatedFat": "<float>",
"estimatedProteinMean": "<float>"
}