diff --git a/.env.example b/.env.example index ef02d09..95ed38d 100644 --- a/.env.example +++ b/.env.example @@ -12,3 +12,9 @@ EXPRESS_PORT=8080 JWT_ACCESS_TOKEN_SECRET='' # The JWT secret can be generate with this command `npm run generate:secret:jwt` JWT_REFRESH_TOKEN_SECRET='' + +GCLOUD_PROJECT_ID="your-gcloud-project-id" +GCLOUD_BUCKET_NAME="your-bucket-name" + +GENERATIVE_AI_SERVICE_URL="" +SUGGESTION_AI_SERVICE_URL="" diff --git a/.github/workflows/google-cloudrun-docker.yml b/.github/workflows/google-cloudrun-docker.yml index c5fa8be..80e40ce 100644 --- a/.github/workflows/google-cloudrun-docker.yml +++ b/.github/workflows/google-cloudrun-docker.yml @@ -86,6 +86,10 @@ jobs: echo "EXPRESS_PORT=${{ secrets.EXPRESS_PORT }}" >> .env.production echo "JWT_ACCESS_TOKEN_SECRET=${{ secrets.JWT_ACCESS_TOKEN_SECRET }}" >> .env.production echo "JWT_REFRESH_TOKEN_SECRET=${{ secrets.JWT_REFRESH_TOKEN_SECRET }}" >> .env.production + echo "GCLOUD_PROJECT_ID=${{ secrets.GCLOUD_PROJECT_ID }}" >> .env.production + echo "GCLOUD_BUCKET_NAME=${{ secrets.GCLOUD_BUCKET_NAME }}" >> .env.production + echo "GENERATIVE_AI_SERVICE_URL=${{ secrets.GENERATIVE_AI_SERVICE_URL }}" >> .env.production + echo "SUGGESTION_AI_SERVICE_URL=${{ secrets.SUGGESTION_AI_SERVICE_URL }}" >> .env.production elif [[ "${GITHUB_REF##*/}" == "staging" ]]; then echo "NODE_ENV=staging" >> $GITHUB_ENV echo "DOCKER_TAG=staging-${{ github.sha }}" >> $GITHUB_ENV @@ -95,6 +99,10 @@ jobs: echo "EXPRESS_PORT=${{ secrets.EXPRESS_PORT }}" >> .env.staging echo "JWT_ACCESS_TOKEN_SECRET=${{ secrets.JWT_ACCESS_TOKEN_SECRET }}" >> .env.staging echo "JWT_REFRESH_TOKEN_SECRET=${{ secrets.JWT_REFRESH_TOKEN_SECRET }}" >> .env.staging + echo "GCLOUD_PROJECT_ID=${{ secrets.GCLOUD_PROJECT_ID }}" >> .env.staging + echo "GCLOUD_BUCKET_NAME=${{ secrets.GCLOUD_BUCKET_NAME }}" >> .env.staging + echo "GENERATIVE_AI_SERVICE_URL=${{ secrets.GENERATIVE_AI_SERVICE_URL }}" >> .env.staging + echo "SUGGESTION_AI_SERVICE_URL=${{ secrets.SUGGESTION_AI_SERVICE_URL }}" >> .env.staging elif [[ "${GITHUB_REF##*/}" == "feat/setup-node" ]]; then echo "NODE_ENV=dev" >> $GITHUB_ENV echo "DOCKER_TAG=dev-${{ github.sha }}" >> $GITHUB_ENV @@ -104,6 +112,10 @@ jobs: echo "EXPRESS_PORT=${{ secrets.EXPRESS_PORT }}" >> .env.dev echo "JWT_ACCESS_TOKEN_SECRET=${{ secrets.JWT_ACCESS_TOKEN_SECRET }}" >> .env.dev echo "JWT_REFRESH_TOKEN_SECRET=${{ secrets.JWT_REFRESH_TOKEN_SECRET }}" >> .env.dev + echo "GCLOUD_PROJECT_ID=${{ secrets.GCLOUD_PROJECT_ID }}" >> .env.dev + echo "GCLOUD_BUCKET_NAME=${{ secrets.GCLOUD_BUCKET_NAME }}" >> .env.dev + echo "GENERATIVE_AI_SERVICE_URL=${{ secrets.GENERATIVE_AI_SERVICE_URL }}" >> .env.dev + echo "SUGGESTION_AI_SERVICE_URL=${{ secrets.SUGGESTION_AI_SERVICE_URL }}" >> .env.dev else echo "NODE_ENV=dev" >> $GITHUB_ENV echo "DOCKER_TAG=dev-${{ github.sha }}" >> $GITHUB_ENV @@ -113,6 +125,10 @@ jobs: echo "EXPRESS_PORT=${{ secrets.EXPRESS_PORT }}" >> .env.dev echo "JWT_ACCESS_TOKEN_SECRET=${{ secrets.JWT_ACCESS_TOKEN_SECRET }}" >> .env.dev echo "JWT_REFRESH_TOKEN_SECRET=${{ secrets.JWT_REFRESH_TOKEN_SECRET }}" >> .env.dev + echo "GCLOUD_PROJECT_ID=${{ secrets.GCLOUD_PROJECT_ID }}" >> .env.dev + echo "GCLOUD_BUCKET_NAME=${{ secrets.GCLOUD_BUCKET_NAME }}" >> .env.dev + echo "GENERATIVE_AI_SERVICE_URL=${{ secrets.GENERATIVE_AI_SERVICE_URL }}" >> .env.dev + echo "SUGGESTION_AI_SERVICE_URL=${{ secrets.SUGGESTION_AI_SERVICE_URL }}" >> .env.dev fi - name: Clear Docker cache diff --git a/.gitignore b/.gitignore index a4d5e8e..2a52f68 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# gcp files +serviceaccount.json diff --git a/Dockerfile b/Dockerfile index 19665da..8a81f95 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ -FROM node:20 +# Stage 1: Build stage +FROM node:20 AS build # Define build argument ARG NODE_ENV @@ -7,20 +8,48 @@ ARG NODE_ENV ENV NODE_ENV=${NODE_ENV} ENV PORT=8080 -RUN mkdir -p /opt/app +# Install tzdata and set timezone +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y tzdata +RUN ln -fs /usr/share/zoneinfo/Asia/Jakarta /etc/localtime && dpkg-reconfigure -f noninteractive tzdata +# Create app directory WORKDIR /opt/app -COPY package*.json . +# Copy package.json and package-lock.json +COPY package*.json ./ +# Install dependencies RUN npm install +# Copy the rest of the application code COPY . . +# Generate Prisma client RUN npm install -g dotenv-cli - RUN npx prisma generate +RUN dotenv -e .env.${NODE_ENV} -- npx prisma migrate deploy + +# Stage 2: Run stage +# FROM gcr.io/distroless/nodejs20-debian12 +# FROM node:20-slim + +# Install tzdata and set timezone +# RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y tzdata +# RUN ln -fs /usr/share/zoneinfo/Asia/Jakarta /etc/localtime && dpkg-reconfigure -f noninteractive tzdata + +# Copy built files from the build stage +# COPY --from=build /opt/app /opt/app + +# Set the working directory +# WORKDIR /opt/app + +# Set environment variable for runtime +# ENV NODE_ENV=${NODE_ENV} +# ENV PORT=8080 +ENV TZ=Asia/Jakarta +# Expose the port EXPOSE 8080 -CMD ["sh", "-c", "npm run start:${NODE_ENV}"] \ No newline at end of file +# Run the application using shell to ensure the environment variable is picked up +CMD ["sh", "-c", "npm run start:${NODE_ENV}"] diff --git a/README.md b/README.md index dc4818e..a3ce018 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,224 @@

Finboost Back-End

-

This repository is the backend for a mobile application that aims to provide various features such as authentication, chat, and more. This backend is built using Express.js, Prisma, MySQL (Cloud SQL), Zod, JWT, Docker, OpenAPI, GitHub Acitons, Cloud Run, and Cloud Storage

+ +
+ +![Node.js](https://img.shields.io/badge/-Node.js-05122A?style=flat&logo=node.js)  +![Express.js](https://img.shields.io/badge/-Express.js-05122A?style=flat&logo=express)  +![Prisma](https://img.shields.io/badge/-Prisma-05122A?style=flat&logo=prisma)  +![MySQL](https://img.shields.io/badge/-MySQL-05122A?style=flat&logo=mariadb)  +![Firebase](https://img.shields.io/badge/-Firebase-05122A?style=flat&logo=firebase)  +![Zod](https://img.shields.io/badge/-Zod-05122A?style=flat&logo=zod)  +![JWT](https://img.shields.io/badge/-JWT-05122A?style=flat&logo=auth0)  +![Docker](https://img.shields.io/badge/-Docker-05122A?style=flat&logo=docker)  +![OpenAPI Swagger](https://img.shields.io/badge/-OpenAPI%20Swagger-05122A?style=flat&logo=swagger)  +![Postman](https://img.shields.io/badge/-Postman-05122A?style=flat&logo=postman)  +![Google Cloud Platform](https://img.shields.io/badge/-Google%20Cloud%20Platform-05122A?style=flat&logo=googlecloud)  +![Linux](https://img.shields.io/badge/-Linux-05122A?style=flat&logo=linux)  +![GitHub Actions](https://img.shields.io/badge/-GitHub%20Actions-05122A?style=flat&logo=githubactions)  + +
+ +

This repository is the backend for a mobile application that aims to provide various features such as authentication, chat, and more. This backend is built using Express.js, Prisma, MySQL, Firebase, Zod, JWT, Docker, OpenAPI (Swagger), GitHub Acitons, Cloud Run, and Cloud Storage

## Table of Contents -- [Tech Stack](#tech-stack) -- [Architecture File and Folder](#architecture-file-and-folder) -- [Prisma Cheatsheet](#prisma-cheatsheet) +- [Tech Stack](#tech-stack) +- [Architecture File and Folder](#architecture-file-and-folder) +- [Entity Relationship Diagram](#entity-relationship-diagram) +- [Infrastructure Diagram](#infrastructure-diagram) +- [Flow MVP Application](#flow-mvp-application) +- [Running on Localhost](#running-on-localhost) +- [Setting Up CI/CD Pipeline (GitHub Actions)](#setting-up-cicd-pipeline-github-actions) +- [Prisma Cheatsheet](#prisma-cheatsheet) ## Tech Stack -- [Express.js](https://expressjs.com) (`Framework`): Minimal and flexible Node.js web application framework that provides robust set of features for web and mobile applications. APIs. -- [Prisma](https://www.prisma.io) (`ORM`): Let your team ship features faster, and leave the database complexities to us (orm, schema, introspection, migration, seeding, studio). -- [MySQL](https://www.mysql.com) (`Database`): Is an open-source relational database management system. -- [PostgreSQL](https://www.postgresql.org) (`Database`): PostgreSQL, also known as Postgres, is a free and open-source relational database management system emphasizing extensibility and SQL compliance. -- [Zod](https://zod.dev) (`Validation`): Zod is a TypeScript-first schema declaration and validation library. I'm using the term "schema" to broadly refer to any data type, from a simple `string` to a complex nested object. Zod is designed to be as developer-friendly as possible. The goal is to eliminate duplicative type declarations. With Zod, you declare a validator once and Zod will automatically infer the static TypeScript type. It's easy to compose simpler types into complex data structures. -- [JWT](https://jwt.io) (`Auth/Token`): JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties. -- [Docker](https://www.docker.com) (`Container`): Docker is a platform designed to help developers build, share, and run container applications. We handle the tedious setup, so you can focus on the code. -- [OpenAPI (Swagger)](https://www.openapis.org) (`Documentation`): The OpenAPI Specification, previously known as the Swagger Specification, is a specification for a machine-readable interface definition language for describing, producing, consuming and visualizing web services. +- [Express.js](https://expressjs.com) (`Framework`): Minimal and flexible Node.js web application framework that provides robust set of features for web and mobile applications. APIs. +- [Prisma](https://www.prisma.io) (`ORM`): Let your team ship features faster, and leave the database complexities to us (orm, schema, introspection, migration, seeding, studio). +- [MySQL](https://www.mysql.com) (`Database`): Is an open-source relational database management system. +- [Zod](https://zod.dev) (`Validation`): Zod is a TypeScript-first schema declaration and validation library. I'm using the term "schema" to broadly refer to any data type, from a simple `string` to a complex nested object. Zod is designed to be as developer-friendly as possible. The goal is to eliminate duplicative type declarations. With Zod, you declare a validator once and Zod will automatically infer the static TypeScript type. It's easy to compose simpler types into complex data structures. +- [JWT](https://jwt.io) (`Auth/Token`): JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties. +- [Docker](https://www.docker.com) (`Container`): Docker is a platform designed to help developers build, share, and run container applications. We handle the tedious setup, so you can focus on the code. +- [OpenAPI (Swagger)](https://www.openapis.org) (`Documentation`): The OpenAPI Specification, previously known as the Swagger Specification, is a specification for a machine-readable interface definition language for describing, producing, consuming and visualizing web services. +- [Cloud Run](https://cloud.google.com/run?hl=en) (`Serverless`): For host the Node.js if using container deployment +- [Compute Engine](https://cloud.google.com/products/compute?hl=en) (`Virtual Machine`): For installing database or web server if Node.js deployment not using container +- [Cloud Storage](https://cloud.google.com/storage?hl=en) (`Object Disk`): For storing data objects (e.g. images, files, etc) +- [GitHub Actions](https://docs.github.com/actions) (`CI/CD Pipeline`): Is used for DevOps CI/CD Pipeline if deployment using Cloud Run ## Architecture File and Folder -| File/Folder Name | Description | -| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `src/app.js` | Application starter, the entry point express application | -| `src/routes/api.js` | List of route the RESTful Application, handle request method and mapping to the correct controller | -| `src/middleware/verifyToken.middleware.js` | Middleware for verify the `Authorization: Bearer "accessToken"` request before to the actual destination or controller | -| `src/controller` | Folder that contain many controller file to handle the request from the route. In this controller, you can validate the request body and etc with Zod before sending request to the service file | -| `src/service` | Folder that contain many service file to handle the request from the controller. In this service, you can define the business logic before sending query to the repository or database | -| `src/repository` | Folder that contain many repository file to handle query with some or no data from the service file. In this repository, you can define the query with prisma ORM to the database | +| File/Folder Name | Description | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `src/app.js` | Application starter, the entry point express application | +| `src/routes/api.js` | List of route the RESTful Application, handle request method and mapping to the correct controller | +| `src/middleware` | Middleware for verify the request before to the actual destination or controller, e.g. Bearer access token or upload file | +| `src/controller` | Folder that contain many controller file to handle the request from the route. In this controller, you can validate the request body and etc with Zod before sending request to the service file | +| `src/service` | Folder that contain many service file to handle the request from the controller. In this service, you can define the business logic before sending query to the repository or database | +| `src/repository` | Folder that contain many repository file to handle query with some or no data from the service file. In this repository, you can define the query with prisma ORM to the database | +| `src/schema` | Folder that contain user input schema | +| `src/exceptions` | Folder that contain handle error response, e.g. Validation error, Server error, Client error | +| `src/utils` | Folder that contain function utility to support the application | +| `prisma` | Folder that contain prisma configuration, e.g. Schema, Seeding, Migration | +| `db/prisma.js` | File prisma client connection to connect to the database | +| `.github` | Folder that contain configuration of GitHub repository, e.g. GitHub Actions for CI/CD Pipeline | +| `postman` | Folder that contain collection and environment for testing RESTful API on Postman | + +## Entity Relationship Diagram + +![ERD](assets/erd.png) + +## Infrastructure Diagram + +![Infrastructure Diagram Capstone](assets/infrastructure-diagram-capstone.gif) + +## Flow MVP Application + +![Flow MVP Capstone](assets/flow-mvp-capstone.gif) + +## Running on Localhost + +> **NOTE**: Before you running this application on your localhost, your computer must be authenticate with Google Cloud Project and make sure all of resource is already setup: +> +> - Gcloud CLI +> - Google Cloud Storage (for storing avatar user file) +> +> You can authenticate your local computer using ADC method or Service Account +> +> - ADC +> +> ```bash +> gcloud auth application-default login +> ``` +> +> - Service Account +> +> ```bash +> gcloud auth activate-service-account --key-file=/path/to/serviceaccount.json --project=project-id +> ``` + +- Clone this repository + +```bash +git clone https://github.com/Finboost/finboost-backend.git finboost-backend +``` + +- Setup environment file for development + +```bash +cd finboost-backend && cp .env.example .env.dev +``` + +> **NOTE**: For the database setup, you can use `docker-compose.yml`, to run this yml file you can run with this command +> +> ```bash +> docker compose up -d +> ``` +> +> Please consider port mapping the database, to ensure the Prisma connection has the correct `database_url` + +After make a copy environment file from example, fill the `.env.dev` with your own configuration e.g. database url, api version, express port, jwt secret, gcloud project id & bucket name + +> **NOTE**: For JWT access or refresh token secret, you can generate with this command +> +> ```bash +> npm run generate:secret:jwt +> ``` + +- Install dependency application + +```bash +npm i +npm i -g dotenv-cli +``` + +- Generate Prisma Client and Run the Migration + +> **NOTE**: For windows operating system, run with this command +> +> ```bash +> npm run migrate:dev:windows +> ``` +> +> For more running command, you can check on `package.json` + +```bash +npx prisma generate +npm run migrate:dev +``` + +- Running the application + +> **NOTE**: For windows operating system, run with this command +> +> ```bash +> npm run start:dev:windows +> ``` + +```bash +npm run start:dev +``` + +## Setting Up CI/CD Pipeline (GitHub Actions) + +> **NOTE**: Before you setting up the CI/CD Pipeline, make sure all of the permission need on the Google Cloud Platform are already setup: +> +> - Enable Cloud Run and Artifact Registry Service +> +> ```bash +> gcloud services enable run.googleapis.com +> gcloud services enable artifactregistry.googleapis.com +> ``` +> +> - Creating Repository for Artifact Registry +> +> ```bash +> gcloud artifacts repositories create [repository-name] --repository-format=docker --location=asia-southeast2 +> ``` +> +> - Creating Dedicated Service Account for GitHub Actions +> +> ```bash +> export PROJECT_ID=$(gcloud config get-value project) +> export SA_NAME="github-actions" +> gcloud iam service-accounts create $SA_NAME --description="This service account is for authenticate GitHub Actions DevOps CI/CD Pipeline" --display-name="GitHub Actions" +> ``` +> +> Add IAM policy binding to the Service Account +> +> ```bash +> gcloud projects add-iam-policy-binding $PROJECT_ID --member=serviceAccount:$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com --role=roles/artifactregistry.admin +> +> gcloud projects add-iam-policy-binding $PROJECT_ID --member=serviceAccount:$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com --role=roles/run.admin +> +> gcloud projects add-iam-policy-binding $PROJECT_ID --member=serviceAccount:$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com --role=roles/iam.serviceAccountUser +> ``` +> +> Creating JSON Key from that Service Account +> +> > **NOTE**: Please consider the content, this is sensitive data because with this key everybody can authenticate to the Google Cloud Project based on the role binding attached +> +> ```bash +> gcloud iam service-accounts keys create github-actions-service-account.json --iam-account=$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com +> ``` + +- Setup Actions Secrets + +> **NOTE**: `XXX` mean the environment deployment, e.g. `dev`, `staging`, or `production` + +You can setup your `.env.XXX` file to the Actions Secrets on GitHub Repository. The setup is very easy you can follow this guide: + +1. Move to your Repository GitHub which will be store this application +2. Enter to the `Settings` tab +3. Scroll down and select from the section `Security` > `Secrets and Variable` > `Actions` +4. Click the `New repository secret` button + +In this secret, you can store all of your sensitive data, e.g. the Service Account before you create the above. You can fill the name with `GCLOUD_SERVICE_ACCOUNT_KEY`. + +You can check the `.github/workflows/google-cloudrun-docker.yml` file and see all the `secrets` names. This should correspond to the `.env` file, the different is the value from each deployment (dev, staging, or production) ## Prisma Cheatsheet -- Initialize +- Initialize Run this command if you first setup with the prisma, prisma will generate the `.env` file and generate `prisma/schema.prisma` file for the data model @@ -39,7 +226,15 @@ Run this command if you first setup with the prisma, prisma will generate the `. npx prisma init ``` -- Introspection +- Generate Client Connection + +Run this command if you first setup with the prisma, prisma will generate the client connection to connect to the database + +```bash +npx prisma generate +``` + +- Introspection Run this command if you have already database and inside the database already defined the tables, so prisma introspection will create the data model based on the tables inside your database. So the database will be the single source of tools. @@ -47,17 +242,23 @@ Run this command if you have already database and inside the database already de npx prisma db pull ``` -- Migration +- Migration Run this command if you don't have a database, but with this migration you must have a data model first by creating manually then you run the migration and your tables inside your database will be created based on data models in the `prisma/schema.prisma` file -> **Note**: Mostly if you run the migration first time, the migration name use `init` +> **NOTE**: Mostly if you run the migration first time, the migration name use `init` +> +> Do not run this command in staging or production environment, instead, you must run migration with this command for those environments (The command should be run in an automated CI/CD pipeline, for example GitHub Actions) +> +> ```bash +> npx prisma migrate deploy +> ``` ```bash npx prisma migrate dev --name ``` -- Prisma Studio +- Prisma Studio Run this command if you want to open a model via web @@ -65,7 +266,7 @@ Run this command if you want to open a model via web npx prisma studio ``` -- Seeding +- Seeding > **Note**: Make sure you check the `package.json` file that have the custom command like this > diff --git a/assets/avatar/female.png b/assets/avatar/female.png new file mode 100644 index 0000000..1990e28 Binary files /dev/null and b/assets/avatar/female.png differ diff --git a/assets/avatar/male.png b/assets/avatar/male.png new file mode 100644 index 0000000..5badd3a Binary files /dev/null and b/assets/avatar/male.png differ diff --git a/assets/erd.png b/assets/erd.png new file mode 100644 index 0000000..f491fa8 Binary files /dev/null and b/assets/erd.png differ diff --git a/assets/flow-mvp-capstone.gif b/assets/flow-mvp-capstone.gif new file mode 100644 index 0000000..76fa0ba Binary files /dev/null and b/assets/flow-mvp-capstone.gif differ diff --git a/assets/infrastructure-diagram-capstone.gif b/assets/infrastructure-diagram-capstone.gif new file mode 100644 index 0000000..86c8656 Binary files /dev/null and b/assets/infrastructure-diagram-capstone.gif differ diff --git a/db/prisma.js b/db/prisma.js index 6edeeac..825c186 100644 --- a/db/prisma.js +++ b/db/prisma.js @@ -2,6 +2,7 @@ import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient({ errorFormat: "pretty", + log: ["query", "info", "warn", "error"], }); export default prisma; diff --git a/openapi.json b/openapi.json index 0eb1d5c..6be4a67 100644 --- a/openapi.json +++ b/openapi.json @@ -17,14 +17,18 @@ "variables": { "environment": { "description": "Server Information", - "default": "production---finboost-backend-jzrwg5bjqq-et.a.run.app", + "default": "production---finboost-backend-rtalegstna-et.a.run.app", "enum": [ - "production---finboost-backend-jzrwg5bjqq-et.a.run.app", - "staging---finboost-backend-jzrwg5bjqq-et.a.run.app", - "dev---finboost-backend-jzrwg5bjqq-et.a.run.app" + "production---finboost-backend-rtalegstna-et.a.run.app", + "staging---finboost-backend-rtalegstna-et.a.run.app", + "dev---finboost-backend-rtalegstna-et.a.run.app" ] } } + }, + { + "description": "Local Finboost Back-End URL", + "url": "http://localhost:3000/api/v1" } ], "tags": [ @@ -76,7 +80,7 @@ "value": { "fullName": "John Doe", "email": "john.doe@example.com", - "gender": "Laki-laki", + "gender": "Laki_laki", "age": 23, "phoneNumber": "081234567890", "password": "Lts47S~^{yp^)$I", @@ -114,7 +118,7 @@ "status": "success", "message": "User created successfully", "data": { - "id": "d969ee14-3362-46cd-ace9-af4d7b131733" + "userId": "d969ee14-3362-46cd-ace9-af4d7b131733" } } }, @@ -124,7 +128,7 @@ "status": "success", "message": "User created successfully", "data": { - "id": "f5e05c30-99ff-4693-a0aa-bc1e869dd3e5" + "userId": "f5e05c30-99ff-4693-a0aa-bc1e869dd3e5" } } } @@ -178,6 +182,25 @@ } } }, + "409": { + "description": "Failed signup user because email already exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Fail" + }, + "examples": { + "Email already exist": { + "description": "Fail signup because email already exist", + "value": { + "status": "fail", + "message": "email already exist" + } + } + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -190,26 +213,14 @@ "description": "Fail signup due to database connection issue", "value": { "status": "error", - "message": "Internal server error", - "errors": [ - { - "field": "database", - "message": "Failed to connect to the database" - } - ] + "message": "Database connection error" } }, "Unexpected server error": { "description": "Fail signup due to unexpected server error", "value": { "status": "error", - "message": "Internal server error", - "errors": [ - { - "field": "server", - "message": "Unexpected condition encountered" - } - ] + "message": "Unexpected server error" } } } @@ -371,26 +382,14 @@ "description": "Fail sigin due to database connection issue", "value": { "status": "error", - "message": "Internal server error", - "errors": [ - { - "field": "database", - "message": "Failed to connect to the database" - } - ] + "message": "Database connection error" } }, "Unexpected server error": { "description": "Fail sigin due to unexpected server error", "value": { "status": "error", - "message": "Internal server error", - "errors": [ - { - "field": "server", - "message": "Unexpected condition encountered" - } - ] + "message": "Unexpected server error" } } } @@ -433,6 +432,51 @@ } } }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Fail" + }, + "examples": { + "not provided refresh token": { + "description": "Fail to refresh token because the request not provided refresh token on cookies", + "value": { + "status": "fail", + "message": "Unauthorized. Please provide `refreshToken` on cookies before request" + } + } + } + } + } + }, + "403": { + "description": "Fobidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Fail" + }, + "examples": { + "invalid refresh token": { + "description": "Fail to refresh token because invalid refresh token", + "value": { + "status": "fail", + "message": "Forbidden. Can't find who the owner `refreshToken`" + } + }, + "decode error": { + "description": "Fail to refresh token because decode error", + "value": { + "status": "fail", + "message": "Forbidden. Can't decode or compare token with secret" + } + } + } + } + } + }, "500": { "$ref": "#/components/responses/InternalServerError" } @@ -803,7 +847,7 @@ "status": "success", "message": "User data updated successfully", "data": { - "id": "d969ee14-3362-46cd-ace9-af4d7b131733" + "userId": "d969ee14-3362-46cd-ace9-af4d7b131733" } } }, @@ -813,7 +857,7 @@ "status": "success", "message": "User data updated successfully", "data": { - "id": "f5e05c30-99ff-4693-a0aa-bc1e869dd3e5" + "userId": "f5e05c30-99ff-4693-a0aa-bc1e869dd3e5" } } } @@ -936,7 +980,7 @@ "status": "success", "message": "User data partially updated successfully", "data": { - "id": "d969ee14-3362-46cd-ace9-af4d7b131733" + "userId": "d969ee14-3362-46cd-ace9-af4d7b131733" } } }, @@ -946,7 +990,7 @@ "status": "success", "message": "User data partially updated successfully", "data": { - "id": "f5e05c30-99ff-4693-a0aa-bc1e869dd3e5" + "userId": "f5e05c30-99ff-4693-a0aa-bc1e869dd3e5" } } } @@ -1035,7 +1079,7 @@ "status": "success", "message": "Successfully delete user data", "data": { - "id": "d969ee14-3362-46cd-ace9-af4d7b131733" + "userId": "d969ee14-3362-46cd-ace9-af4d7b131733" } } }, @@ -1045,7 +1089,7 @@ "status": "success", "message": "Successfully delete user data", "data": { - "id": "f5e05c30-99ff-4693-a0aa-bc1e869dd3e5" + "userId": "f5e05c30-99ff-4693-a0aa-bc1e869dd3e5" } } } @@ -1100,6 +1144,7 @@ "data": { "profile": { "id": "de5b1599-b1b7-4633-8a5d-331690ee9413", + "about": "Sophismata tum crur tabgo angulus. Certe statua adfero statua talis aveho aspicio. Pel voluptatum strenuus.", "avatar": "https://storage.googleapis.com/finboost/example.png", "maritalStatus": "Lajang", "certifiedStatus": null, @@ -1110,7 +1155,12 @@ "education": { "id": "7fe46a1f-fb32-4e36-9741-d96f22cf4b4b", "name": "SMA / Sederajat" - } + }, + "investment": "Saham", + "insurance": "Obligasi", + "incomePerMonth": "9211156", + "totalSaving": "111138960", + "totalDebt": "10000000" } } } @@ -1123,6 +1173,7 @@ "data": { "profile": { "id": "36429cef-96d1-41a7-b579-c2dc8d4f2beb", + "about": null, "avatar": "https://storage.googleapis.com/finboost/example2.png", "maritalStatus": "Menikah", "certifiedStatus": "Certified Financial Planner", @@ -1133,7 +1184,12 @@ "education": { "id": "b5fbeaee-812f-4ec3-b807-22dd2794e09a", "name": "S1" - } + }, + "investment": null, + "insurance": null, + "incomePerMonth": null, + "totalSaving": null, + "totalDebt": null } } } @@ -1161,10 +1217,10 @@ } ] }, - "post": { + "patch": { "tags": ["User", "Profile"], - "summary": "Insert user profile data by user id", - "description": "Insert specific user profile by user id. Requires valid access token", + "summary": "Partial update user profile data by user id", + "description": "Partially update specific user profile by user id. Requires valid access token", "parameters": [ { "$ref": "#/components/parameters/UserId" @@ -1173,27 +1229,31 @@ "requestBody": { "required": true, "content": { - "multipart/form-data": { + "application/json": { "schema": { - "$ref": "#/components/schemas/InsertAllFieldProfile" + "$ref": "#/components/schemas/UpdatePartialFieldProfile" }, "examples": { "User 1": { - "description": "Example request to insert user profile", + "description": "Example request to partially update user profile", "value": { - "avatar": "file", - "maritalStatus": "Menikah", + "about": "Testing About", + "maritalStatus": "Lajang", "certifiedStatus": null, "workId": "319defd8-1836-4b9d-af2d-655a43a60bde", - "educationId": "7fe46a1f-fb32-4e36-9741-d96f22cf4b4b" + "educationId": "7fe46a1f-fb32-4e36-9741-d96f22cf4b4b", + "investment": "Saham", + "insurance": "Obligasi", + "incomePerMonth": "2400000", + "totalSaving": "5000000", + "totalDebt": "10000000" } }, "User 2": { - "description": "Example request to insert user profile", + "description": "Example request to update user profile", "value": { - "avatar": "file", - "maritalStatus": "Cerai", - "certifiedStatus": "Certified Financial Planner", + "maritalStatus": "Menikah", + "certifiedStatus": "Certified Financial Planner (CFP)", "workId": "dc4aa7d6-7cbf-4789-a7a6-5fdc9c1ca642", "educationId": "b5fbeaee-812f-4ec3-b807-22dd2794e09a" } @@ -1203,7 +1263,7 @@ } }, "responses": { - "201": { + "200": { "description": "Successfully updated user profile data", "content": { "application/json": { @@ -1217,7 +1277,7 @@ "status": "success", "message": "User profile data updated successfully", "data": { - "id": "d969ee14-3362-46cd-ace9-af4d7b131733" + "userId": "d969ee14-3362-46cd-ace9-af4d7b131733" } } }, @@ -1227,7 +1287,7 @@ "status": "success", "message": "User profile data updated successfully", "data": { - "id": "f5e05c30-99ff-4693-a0aa-bc1e869dd3e5" + "userId": "f5e05c30-99ff-4693-a0aa-bc1e869dd3e5" } } } @@ -1254,50 +1314,18 @@ } ] }, - "put": { + "delete": { "tags": ["User", "Profile"], - "summary": "Update user profile data by user id", - "description": "Update specific user profile by user id. Requires valid access token", + "summary": "Delete user profile data by user id", + "description": "Delete specific user profile by user id. Requires valid access token", "parameters": [ { "$ref": "#/components/parameters/UserId" } ], - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/UpdateAllFieldProfile" - }, - "examples": { - "User 1": { - "description": "Example request to update user profile", - "value": { - "avatar": "file", - "maritalStatus": "Menikah", - "certifiedStatus": null, - "workId": "72608273-18c1-4924-b67f-d67225e73faa", - "educationId": "b5fbeaee-812f-4ec3-b807-22dd2794e09a" - } - }, - "User 2": { - "description": "Example request to update user profile", - "value": { - "avatar": "file", - "maritalStatus": "Cerai", - "certifiedStatus": "Certified Financial Planner", - "workId": "dc4aa7d6-7cbf-4789-a7a6-5fdc9c1ca642", - "educationId": "bd2bb42f-d42e-4b7d-a3f4-221a867d1836" - } - } - } - } - } - }, "responses": { "200": { - "description": "Successfully updated user profile data", + "description": "Successfully deleted user profile data", "content": { "application/json": { "schema": { @@ -1305,22 +1333,22 @@ }, "examples": { "User 1": { - "description": "Successfully updated user data", + "description": "Successfully deleted data user profile 1", "value": { "status": "success", - "message": "User profile data updated successfully", + "message": "Successfully delete user profile data", "data": { - "id": "d969ee14-3362-46cd-ace9-af4d7b131733" + "userId": "d969ee14-3362-46cd-ace9-af4d7b131733" } } }, "User 2": { - "description": "Successfully updated user data", + "description": "Successfully deleted data user profile 2", "value": { "status": "success", - "message": "User profile data updated successfully", + "message": "Successfully delete user profile data", "data": { - "id": "f5e05c30-99ff-4693-a0aa-bc1e869dd3e5" + "userId": "f5e05c30-99ff-4693-a0aa-bc1e869dd3e5" } } } @@ -1346,11 +1374,13 @@ "bearerAuth": [] } ] - }, - "patch": { + } + }, + "/users/{userId}/profile/avatar": { + "put": { "tags": ["User", "Profile"], - "summary": "Partial update user profile data by user id", - "description": "Partially update specific user profile by user id. Requires valid access token", + "summary": "Update avatar user profile data by user id", + "description": "Update specific avatar user profile by user id. Requires valid access token", "parameters": [ { "$ref": "#/components/parameters/UserId" @@ -1361,23 +1391,13 @@ "content": { "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/UpdatePartialFieldProfile" + "$ref": "#/components/schemas/UpdateAvatarUserProfile" }, "examples": { "User 1": { - "description": "Example request to partially update user profile", - "value": { - "maritalStatus": "Lajang", - "workId": "319defd8-1836-4b9d-af2d-655a43a60bde", - "educationId": "7fe46a1f-fb32-4e36-9741-d96f22cf4b4b" - } - }, - "User 2": { - "description": "Example request to update user profile", + "description": "Example request to update avatar user profile", "value": { - "maritalStatus": "Menikah", - "workId": "dc4aa7d6-7cbf-4789-a7a6-5fdc9c1ca642", - "educationId": "b5fbeaee-812f-4ec3-b807-22dd2794e09a" + "avatar": "file" } } } @@ -1386,7 +1406,7 @@ }, "responses": { "200": { - "description": "Successfully updated user profile data", + "description": "Successfully updated avatar user profile data", "content": { "application/json": { "schema": { @@ -1394,22 +1414,22 @@ }, "examples": { "User 1": { - "description": "Successfully updated user data", + "description": "Successfully updated avatar user data", "value": { "status": "success", - "message": "User profile data updated successfully", + "message": "Avatar user updated successfully", "data": { - "id": "d969ee14-3362-46cd-ace9-af4d7b131733" + "userId": "d969ee14-3362-46cd-ace9-af4d7b131733" } } }, "User 2": { - "description": "Successfully updated user data", + "description": "Successfully updated avatar user data", "value": { "status": "success", - "message": "User profile data updated successfully", + "message": "Avatar user updated successfully", "data": { - "id": "f5e05c30-99ff-4693-a0aa-bc1e869dd3e5" + "userId": "f5e05c30-99ff-4693-a0aa-bc1e869dd3e5" } } } @@ -1417,61 +1437,26 @@ } } }, - "401": { - "$ref": "#/components/responses/MiddlewareUnauthorizedError" - }, - "403": { - "$ref": "#/components/responses/MiddlewareForbiddenError" - }, - "404": { - "$ref": "#/components/responses/ErrorUserNotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "delete": { - "tags": ["User", "Profile"], - "summary": "Delete user profile data by user id", - "description": "Delete specific user profile by user id. Requires valid access token", - "parameters": [ - { - "$ref": "#/components/parameters/UserId" - } - ], - "responses": { - "200": { - "description": "Successfully deleted user profile data", + "400": { + "description": "Fail updated avatar user data because validation error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuccessSignupOrRegisUser" + "$ref": "#/components/schemas/Fail" }, "examples": { - "User 1": { - "description": "Successfully deleted data user profile 1", + "Validation 1": { + "description": "Fail updated data avatar user because file type not allowed", "value": { - "status": "success", - "message": "Successfully delete user profile data", - "data": { - "id": "d969ee14-3362-46cd-ace9-af4d7b131733" - } + "status": "fail", + "message": "Invalid file type. Only JPG, PNG are allowed!" } }, - "User 2": { - "description": "Successfully deleted data user profile 2", + "Validation 2": { + "description": "Fail updated data avatar user because file size is too large", "value": { - "status": "success", - "message": "Successfully delete user profile data", - "data": { - "id": "f5e05c30-99ff-4693-a0aa-bc1e869dd3e5" - } + "status": "fail", + "message": "File too large. Maximum size is 2MB." } } } @@ -1503,6 +1488,28 @@ "tags": ["Role", "User"], "summary": "Get all user role", "description": "Retrieve all user role", + "parameters": [ + { + "name": "name", + "in": "query", + "description": "Get role by name", + "required": false, + "schema": { + "type": "string", + "nullable": true + }, + "examples": { + "User": { + "description": "Get role by name User", + "value": "User" + }, + "Expert": { + "description": "Get role by name Expert", + "value": "Expert" + } + } + } + ], "responses": { "200": { "description": "Successfully retrieved all user role", @@ -1579,7 +1586,7 @@ "status": "success", "message": "Role created successfully", "data": { - "id": "fdc3462c-96fb-4797-8341-cd2b1cce88bb" + "roleId": "fdc3462c-96fb-4797-8341-cd2b1cce88bb" } } } @@ -1705,7 +1712,7 @@ "status": "success", "message": "Role updated successfully", "data": { - "id": "fdc3462c-96fb-4797-8341-cd2b1cce88bb" + "roleId": "fdc3462c-96fb-4797-8341-cd2b1cce88bb" } } } @@ -1770,7 +1777,7 @@ "status": "success", "message": "Role deleted successfully", "data": { - "id": "fdc3462c-96fb-4797-8341-cd2b1cce88bb" + "roleId": "fdc3462c-96fb-4797-8341-cd2b1cce88bb" } } } @@ -1792,6 +1799,28 @@ "tags": ["Work", "User", "Profile"], "summary": "Get all work profile user", "description": "Retrieve all work profile user", + "parameters": [ + { + "name": "name", + "in": "query", + "description": "Get specific work data by name", + "required": false, + "schema": { + "type": "string", + "nullable": true + }, + "examples": { + "Work 1": { + "description": "Get specific work data by name", + "value": "Pegawai Swasta" + }, + "Work 2": { + "description": "Get specific work data by name", + "value": "Profesional" + } + } + } + ], "responses": { "200": { "description": "Successfully retrieved all work profile user", @@ -1908,7 +1937,7 @@ "status": "success", "message": "Work created successfully", "data": { - "id": "6ce7e34b-93f9-406c-aa1d-09fac038b138" + "workId": "6ce7e34b-93f9-406c-aa1d-09fac038b138" } } } @@ -2034,7 +2063,7 @@ "status": "success", "message": "Work updated successfully", "data": { - "id": "6ce7e34b-93f9-406c-aa1d-09fac038b138" + "workId": "6ce7e34b-93f9-406c-aa1d-09fac038b138" } } } @@ -2099,7 +2128,7 @@ "status": "success", "message": "Work deleted successfully", "data": { - "id": "6ce7e34b-93f9-406c-aa1d-09fac038b138" + "workId": "6ce7e34b-93f9-406c-aa1d-09fac038b138" } } } @@ -2121,16 +2150,38 @@ "tags": ["Education", "User", "Profile"], "summary": "Get all education profile user", "description": "Retrieve all education profile user", - "responses": { - "200": { - "description": "Successfully retrieved all education profile user", - "content": { - "application/json": { - "schema": { - "type": "object", - "$ref": "#/components/schemas/SuccessGetAllEducations" - }, - "examples": { + "parameters": [ + { + "name": "name", + "in": "query", + "description": "Get specific education data by name", + "required": false, + "schema": { + "type": "string", + "nullable": true + }, + "examples": { + "Education 1": { + "description": "Get specific education data by name", + "value": "SD" + }, + "Education 2": { + "description": "Get specific education data by name", + "value": "Diploma" + } + } + } + ], + "responses": { + "200": { + "description": "Successfully retrieved all education profile user", + "content": { + "application/json": { + "schema": { + "type": "object", + "$ref": "#/components/schemas/SuccessGetAllEducations" + }, + "examples": { "All educations": { "description": "Successfully get all education profile user", "value": { @@ -2221,7 +2272,7 @@ "status": "success", "message": "Education created successfully", "data": { - "id": "8a69ef80-8d1d-4832-9c6a-fba9d9f16230" + "educationId": "8a69ef80-8d1d-4832-9c6a-fba9d9f16230" } } } @@ -2347,7 +2398,7 @@ "status": "success", "message": "Education updated successfully", "data": { - "id": "8a69ef80-8d1d-4832-9c6a-fba9d9f16230" + "educationId": "8a69ef80-8d1d-4832-9c6a-fba9d9f16230" } } } @@ -2406,13 +2457,503 @@ "$ref": "#/components/schemas/SuccessSignupOrRegisUser" }, "examples": { - "Education deleted": { - "description": "Successfully deleted the education", + "Education deleted": { + "description": "Successfully deleted the education", + "value": { + "status": "success", + "message": "Education deleted successfully", + "data": { + "educationId": "8a69ef80-8d1d-4832-9c6a-fba9d9f16230" + } + } + } + } + } + } + }, + "404": { + "$ref": "#/components/responses/ErrorEducationNotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/chat-rooms": { + "get": { + "tags": ["Chat Room"], + "summary": "Get all chat rooms", + "description": "Retrieve all chat rooms", + "parameters": [ + { + "name": "userId", + "in": "query", + "description": "Get chat rooms by user id", + "required": false, + "schema": { + "type": "string", + "nullable": true + }, + "examples": { + "User 1": { + "description": "Get chat rooms by user id 1", + "value": "d969ee14-3362-46cd-ace9-af4d7b131733" + } + } + } + ], + "responses": { + "200": { + "description": "Successfully retrieved all chat rooms", + "content": { + "application/json": { + "schema": { + "type": "object", + "$ref": "#/components/schemas/SuccessGetAllChatRooms" + }, + "examples": { + "All chat rooms": { + "description": "Successfully get all chat rooms", + "value": { + "status": "success", + "message": "Get all chat rooms", + "data": { + "chatRooms": [ + { + "id": "d969ee14-3362-46cd-ace9-af4d7b131733", + "type": "AI", + "userId": "d969ee14-3362-46cd-ace9-af4d7b131733" + }, + { + "id": "d969ee14-3362-46cd-ace9-af4d7b131733", + "type": "Expert", + "userId": "d969ee14-3362-46cd-ace9-af4d7b131733" + } + ] + } + } + } + } + } + } + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "post": { + "tags": ["Chat Room"], + "summary": "Create a new chat room", + "description": "Add a new chat room to the app", + "requestBody": { + "description": "Chat room object that needs to be added", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InsertAllFieldChatRoom" + }, + "examples": { + "New chat room": { + "description": "Example of a new chat room", + "value": { + "type": "AI", + "userId": "d969ee14-3362-46cd-ace9-af4d7b131733" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Successfully created a new chat room", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessSignupOrRegisUser" + }, + "examples": { + "Chat Room 1": { + "description": "Successfully created a new chat room", + "value": { + "status": "success", + "message": "Chat room created successfully", + "data": { + "id": "d969ee14-3362-46cd-ace9-af4d7b131733" + } + } + } + } + } + } + }, + "400": { + "description": "Failed add new chat room because validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailValidationSignupOrRegister" + }, + "examples": { + "name empty": { + "description": "Fail add new chat room because name field empty", + "value": { + "status": "fail", + "message": "Validation error", + "errors": [ + { + "field": "name", + "message": "name is required" + } + ] + } + } + } + } + } + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/chat-rooms/{chatRoomId}": { + "get": { + "tags": ["Chat Room"], + "summary": "Get a chat room by id", + "description": "Get specific chat room by id", + "parameters": [ + { + "$ref": "#/components/parameters/ChatRoomId" + } + ], + "responses": { + "200": { + "description": "Successfully retrieved the chat room", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessGetChatRoom" + }, + "examples": { + "Chat Room": { + "description": "Successfully retrieved the chat room", + "value": { + "status": "success", + "message": "Get chat room data by id", + "data": { + "chatRoom": { + "id": "d969ee14-3362-46cd-ace9-af4d7b131733", + "type": "AI", + "user": { + "id": "d969ee14-3362-46cd-ace9-af4d7b131733", + "name": "User 1" + } + } + } + } + } + } + } + } + }, + "404": { + "$ref": "#/components/responses/ErrorChatRoomNotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "put": { + "tags": ["Chat Room"], + "summary": "Update a chat room by id", + "description": "Update specific chat room by id", + "parameters": [ + { + "$ref": "#/components/parameters/ChatRoomId" + } + ], + "requestBody": { + "description": "Chat room object that needs to be updated", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAllFieldChatRoom" + }, + "examples": { + "Update chat room": { + "description": "Example of a chat room update", + "value": { + "type": "Expert", + "userId": "d969ee14-3362-46cd-ace9-af4d7b131733", + "expertId": "f5e05c30-99ff-4693-a0aa-bc1e869dd3e5" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully update the chat room", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessSignupOrRegisUser" + }, + "examples": { + "Chat Room updated": { + "description": "Successfully updated the chat room", + "value": { + "status": "success", + "message": "Chat room updated successfully", + "data": { + "id": "d969ee14-3362-46cd-ace9-af4d7b131733" + } + } + } + } + } + } + }, + "400": { + "description": "Failed to update chat room due to validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailValidationSignupOrRegister" + }, + "examples": { + "name empty": { + "description": "Failed to update chat room because name field is empty", + "value": { + "status": "fail", + "message": "Validation error", + "errors": [ + { + "field": "name", + "message": "name is required" + } + ] + } + } + } + } + } + }, + "404": { + "$ref": "#/components/responses/ErrorChatRoomNotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "delete": { + "tags": ["Chat Room"], + "summary": "Delete a chat room by id", + "description": "Delete a specific chat room by its ID", + "parameters": [ + { + "$ref": "#/components/parameters/ChatRoomId" + } + ], + "responses": { + "200": { + "description": "Successfully deleted the chat room", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessSignupOrRegisUser" + }, + "examples": { + "Chat Room deleted": { + "description": "Successfully deleted the chat room", + "value": { + "status": "success", + "message": "Chat room deleted successfully", + "data": { + "id": "d969ee14-3362-46cd-ace9-af4d7b131733" + } + } + } + } + } + } + }, + "404": { + "$ref": "#/components/responses/ErrorChatRoomNotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/chats/ai/predict": { + "post": { + "tags": ["Chat Room"], + "summary": "Predict chat message", + "description": "Predict chat message", + "requestBody": { + "description": "Chat message object that needs to be predicted", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InsertAllFieldChatMessage" + }, + "examples": { + "New chat message": { + "description": "Example of a new chat message", + "value": { + "prompt": "Halo" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully predicted the chat message", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessPredictChatMessage" + }, + "examples": { + "Chat Message": { + "description": "Successfully predicted the chat message", + "value": { + "status": "success", + "message": "Predict chat message", + "data": { + "response": "Halo, ada yang bisa saya bantu?", + "isExpert": false + } + } + } + } + } + } + }, + "400": { + "description": "Failed predict chat message because validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailValidationSignupOrRegister" + }, + "examples": { + "message empty": { + "description": "Fail predict chat message because message field empty", + "value": { + "status": "fail", + "message": "Validation error", + "errors": [ + { + "field": "message", + "message": "message is required" + } + ] + } + } + } + } + } + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/chats/ai/suggestions": { + "post": { + "tags": ["Chat Room"], + "summary": "Get all chat suggestions", + "description": "Retrieve all chat suggestions", + "requestBody": { + "description": "Chat suggestion object that needs to be retrieved", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InsertAllFieldChatSuggestion" + }, + "examples": { + "New chat suggestion": { + "description": "Example of a new chat suggestion", + "value": { + "user_input": "Halo", + "total_questions": "4" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully retrieved all chat suggestions", + "content": { + "application/json": { + "schema": { + "type": "object", + "$ref": "#/components/schemas/SuccessGetAllChatSuggestions" + }, + "examples": { + "All chat suggestions": { + "description": "Successfully get all chat suggestions", "value": { "status": "success", - "message": "Education deleted successfully", + "message": "Get all chat suggestions", "data": { - "id": "8a69ef80-8d1d-4832-9c6a-fba9d9f16230" + "suggested_questions": [ + "Apa saja jenis reksadana yang tersedia di Indonesia?", + "Apa itu reksadana offshore dan bagaimana cara berinvestasi di dalamnya?", + "Apa itu reksadana terproteksi dan bagaimana cara kerjanya?", + "Bagaimana cara menghindari penipuan dalam investasi reksadana?" + ], + "probability": 0.9, + "top_category": "Reksadana" } } } @@ -2420,13 +2961,15 @@ } } }, - "404": { - "$ref": "#/components/responses/ErrorEducationNotFound" - }, "500": { "$ref": "#/components/responses/InternalServerError" } - } + }, + "security": [ + { + "bearerAuth": [] + } + ] } } }, @@ -2507,6 +3050,21 @@ "value": "8a69ef80-8d1d-4832-9c6a-fba9d9f16230" } } + }, + "ChatRoomId": { + "name": "chatRoomId", + "in": "path", + "description": "Room chat ID", + "required": true, + "schema": { + "type": "string" + }, + "examples": { + "Room Chat 1": { + "description": "Sample room chat id for room chat 1", + "value": "d969ee14-3362-46cd-ace9-af4d7b131733" + } + } } }, "schemas": { @@ -2792,6 +3350,9 @@ }, "phoneNumber": { "type": "string" + }, + "password": { + "type": "string" } } }, @@ -2801,6 +3362,9 @@ "id": { "type": "string" }, + "about": { + "type": "string" + }, "avatar": { "type": "string" }, @@ -2815,6 +3379,21 @@ }, "educationId": { "type": "string" + }, + "investment": { + "type": "string" + }, + "insurance": { + "type": "string" + }, + "incomePerMonth": { + "type": "string" + }, + "totalSaving": { + "type": "string" + }, + "totalDebt": { + "type": "string" } } }, @@ -2841,10 +3420,6 @@ "InsertAllFieldProfile": { "type": "object", "properties": { - "avatar": { - "type": "string", - "format": "binary" - }, "maritalStatus": { "type": "string" }, @@ -2862,10 +3437,6 @@ "UpdateAllFieldProfile": { "type": "object", "properties": { - "avatar": { - "type": "string", - "format": "binary" - }, "maritalStatus": { "type": "string" }, @@ -2890,9 +3461,8 @@ "UpdatePartialFieldProfile": { "type": "object", "properties": { - "avatar": { - "type": "string", - "format": "binary" + "about": { + "type": "string" }, "maritalStatus": { "type": "string" @@ -2905,6 +3475,30 @@ }, "educationId": { "type": "string" + }, + "investment": { + "type": "string" + }, + "insurance": { + "type": "string" + }, + "incomePerMonth": { + "type": "string" + }, + "totalSaving": { + "type": "string" + }, + "totalDebt": { + "type": "string" + } + } + }, + "UpdateAvatarUserProfile": { + "type": "object", + "properties": { + "avatar": { + "type": "string", + "format": "binary" } } }, @@ -3117,6 +3711,225 @@ "type": "string" } } + }, + "GetChatRoom": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["Expert", "AI"] + }, + "user": { + "$ref": "#/components/schemas/User" + }, + "expert": { + "$ref": "#/components/schemas/User" + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + }, + "SuccessGetChatRoom": { + "allOf": [ + { + "$ref": "#/components/schemas/Success" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "chatRoom": { + "$ref": "#/components/schemas/GetChatRoom" + } + } + } + } + } + ] + }, + "SuccessGetAllChatRooms": { + "allOf": [ + { + "$ref": "#/components/schemas/Success" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "chatRooms": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GetChatRoom" + } + } + } + } + } + } + ] + }, + "InsertAllFieldChatRoom": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["Expert", "AI"] + }, + "userId": { + "type": "string" + }, + "expertId": { + "type": "string", + "nullable": true + } + } + }, + "UpdateAllFieldChatRoom": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["Expert", "AI"] + }, + "userId": { + "type": "string" + }, + "expertId": { + "type": "string", + "nullable": true + } + } + }, + "InsertAllFieldChatMessage": { + "type": "object", + "properties": { + "prompt": { + "type": "string" + } + } + }, + "InsertAllFieldChatSuggestion": { + "type": "object", + "properties": { + "user_input": { + "type": "string" + }, + "total_questions": { + "type": "string" + } + } + }, + "SuccessPredictChatMessage": { + "allOf": [ + { + "$ref": "#/components/schemas/Success" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "response": { + "type": "string" + }, + "isExpert": { + "type": "boolean" + } + } + } + } + } + ] + }, + "SuccessPredictChatAi": { + "allOf": [ + { + "$ref": "#/components/schemas/Success" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "response": { + "type": "string" + }, + "isExpert": { + "type": "boolean" + } + } + } + } + } + ] + }, + "SuccessSuggestChatAi": { + "allOf": [ + { + "$ref": "#/components/schemas/Success" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "suggested_questions": { + "type": "array", + "items": { + "type": "string" + } + }, + "probability": { + "type": "number" + }, + "top_category": { + "type": "string" + } + } + } + } + } + ] + }, + "SuccessGetAllChatSuggestions": { + "allOf": [ + { + "$ref": "#/components/schemas/Success" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "suggestion_questions": { + "type": "array" + }, + "probability": { + "type": "number" + }, + "top_category": { + "type": "string" + } + } + } + } + } + ] } }, "responses": { @@ -3259,6 +4072,26 @@ } } } + }, + "ErrorChatRoomNotFound": { + "description": "Chat room not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "fail" + }, + "message": { + "type": "string", + "example": "Chat room not found" + } + } + } + } + } } } } diff --git a/package.json b/package.json index 692bb86..7517ebb 100644 --- a/package.json +++ b/package.json @@ -42,14 +42,15 @@ "devDependencies": { "@faker-js/faker": "^8.4.1", "nodemon": "^3.1.1", - "prisma": "^5.14.0" + "prisma": "^5.15.0" }, "dependencies": { "@google-cloud/storage": "^7.11.1", - "@prisma/client": "^5.14.0", + "@prisma/client": "^5.15.0", "bcrypt": "^5.1.1", "cookie-parser": "^1.4.6", "cors": "^2.8.5", + "dateformat": "^5.0.3", "dotenv": "^16.4.5", "express": "^4.19.2", "jsonwebtoken": "^9.0.2", diff --git a/postman/Finboost Back-End.postman_collection.json b/postman/Finboost Back-End.postman_collection.json new file mode 100644 index 0000000..25a0e96 --- /dev/null +++ b/postman/Finboost Back-End.postman_collection.json @@ -0,0 +1,2736 @@ +{ + "info": { + "_postman_id": "a3a07a5e-e6d6-409c-aaae-8dee3409a6ef", + "name": "Finboost Back-End", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "22843638" + }, + "item": [ + { + "name": "Get all role data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('status code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should be an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body object should have correct property and value', () => {\r", + " const responsJson = pm.response.json();\r", + "\r", + " pm.expect(responsJson).to.haveOwnProperty('status');\r", + " pm.expect(responsJson).to.haveOwnProperty(\"message\");\r", + " pm.expect(responsJson).to.haveOwnProperty('data');\r", + "\r", + " pm.expect(responsJson.status).to.equals('success');\r", + " pm.expect(responsJson.message).to.equals(\"Get all user role\");\r", + " pm.expect(responsJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body data object should have a array roles and contains two items', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data } = responseJson;\r", + "\r", + " pm.expect(data).to.haveOwnProperty('roles');\r", + " pm.expect(data.roles).to.be.an('array');\r", + " pm.expect(data.roles).to.lengthOf(2);\r", + "});\r", + "\r", + "pm.test('the roles should have contains only id, name, createdAt, and updatedAt property', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data: { roles } } = responseJson;\r", + "\r", + " roles.forEach((role) => {\r", + " pm.expect(Object.keys(role)).to.lengthOf(4);\r", + " pm.expect(role).to.haveOwnProperty('id');\r", + " pm.expect(role).to.haveOwnProperty('name');\r", + " pm.expect(role).to.haveOwnProperty('createdAt');\r", + " pm.expect(role).to.haveOwnProperty('updatedAt');\r", + " });\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{host}}/api/v1/roles", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "roles" + ] + } + }, + "response": [] + }, + { + "name": "Get role data by name", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('status code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should be an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body object should have correct property and value', () => {\r", + " const responsJson = pm.response.json();\r", + "\r", + " pm.expect(responsJson).to.haveOwnProperty('status');\r", + " pm.expect(responsJson).to.haveOwnProperty(\"message\");\r", + " pm.expect(responsJson).to.haveOwnProperty('data');\r", + "\r", + " pm.expect(responsJson.status).to.equals('success');\r", + " pm.expect(responsJson.message).to.equals(\"Get all user role\");\r", + " pm.expect(responsJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body data object should have a array roles and contains one items', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data } = responseJson;\r", + "\r", + " pm.expect(data).to.haveOwnProperty('roles');\r", + " pm.expect(data.roles).to.be.an('array');\r", + " pm.expect(data.roles).to.lengthOf(1);\r", + "});\r", + "\r", + "pm.test('the roles should have contains only id, name, createdAt, and updatedAt property', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data: { roles } } = responseJson;\r", + "\r", + " roles.forEach((role) => {\r", + " pm.expect(Object.keys(role)).to.lengthOf(4);\r", + " pm.expect(role).to.haveOwnProperty('id');\r", + " pm.expect(role).to.haveOwnProperty('name');\r", + " pm.expect(role).to.haveOwnProperty('createdAt');\r", + " pm.expect(role).to.haveOwnProperty('updatedAt');\r", + " });\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{host}}/api/v1/roles?name=User", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "roles" + ], + "query": [ + { + "key": "name", + "value": "User" + } + ] + } + }, + "response": [] + }, + { + "name": "Insert Role Data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('status code should be 201', () => {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should be an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body should have correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " \r", + " pm.expect(responseJson).to.haveOwnProperty('status');\r", + " pm.expect(responseJson).to.haveOwnProperty('message');\r", + " pm.expect(responseJson).to.haveOwnProperty('data');\r", + "\r", + " pm.expect(responseJson.status).to.equals('success');\r", + " pm.expect(responseJson.message).to.equals('Role created successfully');\r", + " pm.expect(responseJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body data should contain roleId', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data } = responseJson;\r", + "\r", + " pm.expect(data).to.haveOwnProperty('roleId');\r", + " pm.expect(data.roleId).to.not.equals('');\r", + "\r", + " pm.environment.set('roleId', data.roleId);\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"{{newRoleName}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/api/v1/roles", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "roles" + ] + } + }, + "response": [] + }, + { + "name": "Get a User Role by Id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('response code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body object should contain correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " \r", + " pm.expect(responseJson).to.haveOwnProperty('status');\r", + " pm.expect(responseJson).to.haveOwnProperty(\"message\");\r", + " pm.expect(responseJson).to.haveOwnProperty('data');\r", + "\r", + " pm.expect(responseJson.status).to.equals('success');\r", + " pm.expect(responseJson.message).to.equals('Get role data by id');\r", + " pm.expect(responseJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body data object should contain role object', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data } = responseJson;\r", + "\r", + " pm.expect(data).to.haveOwnProperty('role');\r", + " pm.expect(data.role).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('role object should contain correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data: { role } } = responseJson;\r", + "\r", + " pm.expect(role).to.haveOwnProperty('id');\r", + " pm.expect(role).to.haveOwnProperty('name');\r", + " pm.expect(role).to.haveOwnProperty('createdAt');\r", + " pm.expect(role).to.haveOwnProperty('updatedAt');\r", + "\r", + " pm.expect(role.id).to.equals(pm.environment.get('roleId'));\r", + " pm.expect(role.name).to.equals(pm.environment.get('newRoleName'));\r", + " pm.expect(role.createdAt).to.not.equals('');\r", + " pm.expect(role.updatedAt).to.not.equals('');\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{host}}/api/v1/roles/{{roleId}}", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "roles", + "{{roleId}}" + ] + } + }, + "response": [] + }, + { + "name": "Update Role By Id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('status code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should be an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body should have correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " \r", + " pm.expect(responseJson).to.haveOwnProperty('status');\r", + " pm.expect(responseJson).to.haveOwnProperty('message');\r", + " pm.expect(responseJson).to.haveOwnProperty('data');\r", + "\r", + " pm.expect(responseJson.status).to.equals('success');\r", + " pm.expect(responseJson.message).to.equals('Role updated successfully');\r", + " pm.expect(responseJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('when get detail role', () => {\r", + " pm.sendRequest(`${pm.environment.get('host')}/api/v1/roles/${pm.environment.get('roleId')}`, (error, response) => {\r", + " const responseJson = response.json();\r", + " const { data: { role } } = responseJson;\r", + "\r", + " pm.test('role object should contain updated values', () => {\r", + " pm.expect(role).to.haveOwnProperty('id');\r", + " pm.expect(role).to.haveOwnProperty('name');\r", + " pm.expect(role).to.haveOwnProperty('createdAt');\r", + " pm.expect(role).to.haveOwnProperty('updatedAt');\r", + " \r", + " pm.expect(role.id).to.equals(pm.environment.get('roleId'));\r", + " pm.expect(role.name).to.equals(pm.environment.get('updateRoleName'));\r", + " pm.expect(role.createdAt).to.not.equals('');\r", + " pm.expect(role.updatedAt).to.not.equals('');\r", + " }); \r", + " });\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"{{updateRoleName}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/api/v1/roles/{{roleId}}", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "roles", + "{{roleId}}" + ] + } + }, + "response": [] + }, + { + "name": "Delete Role By Id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('status code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should be an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body should have correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " \r", + " pm.expect(responseJson).to.haveOwnProperty('status');\r", + " pm.expect(responseJson).to.haveOwnProperty('message');\r", + "\r", + " pm.expect(responseJson.status).to.equals('success');\r", + " pm.expect(responseJson.message).to.equals('Role deleted successfully');\r", + "});\r", + "\r", + "pm.test('when get detail roles', () => {\r", + " pm.sendRequest(`${pm.environment.get('host')}/api/v1/roles/${pm.environment.get('roleId')}`, (error, response) => {\r", + " pm.test('The role should be not found', () => {\r", + " pm.expect(response.code).to.equals(404);\r", + " }); \r", + " });\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{host}}/api/v1/roles/{{roleId}}", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "roles", + "{{roleId}}" + ] + } + }, + "response": [] + }, + { + "name": "Signup User", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('status code should be 201', () => {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should be an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body should have correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " \r", + " pm.expect(responseJson).to.haveOwnProperty('status');\r", + " pm.expect(responseJson).to.haveOwnProperty('message');\r", + " pm.expect(responseJson).to.haveOwnProperty('data');\r", + "\r", + " pm.expect(responseJson.status).to.equals('success');\r", + " pm.expect(responseJson.message).to.equals('User signup successfully');\r", + " pm.expect(responseJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body data should contain userId', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data } = responseJson;\r", + "\r", + " pm.expect(data).to.haveOwnProperty('userId');\r", + " pm.expect(data.userId).to.not.equals('');\r", + "\r", + " pm.environment.set('userId', data.userId);\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"fullName\": \"{{newUserFullName}}\",\r\n \"email\": \"{{newUserEmail}}\",\r\n \"gender\": \"{{newUserGender}}\",\r\n \"age\": {{newUserAge}},\r\n \"phoneNumber\": \"{{newUserPhoneNumber}}\",\r\n \"password\": \"{{newUserPassword}}\",\r\n \"roleId\": \"{{roleId}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/api/v1/auth/signup", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "auth", + "signup" + ] + } + }, + "response": [] + }, + { + "name": "Signin User", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('response status code should have 200 values', () => {\r", + " pm.response.to.have.status(200)\r", + "})\r", + "\r", + "pm.test('response Content-Type header should have application/json value', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8')\r", + "})\r", + "\r", + "pm.test('response body should an object', () => {\r", + " const responseJson = pm.response.json()\r", + " pm.expect(responseJson).to.be.an('object')\r", + "})\r", + "\r", + "pm.test('response body object should contain correct property and value', () => {\r", + " const responseJson = pm.response.json()\r", + "\r", + " pm.expect(responseJson).to.haveOwnProperty('status')\r", + " pm.expect(responseJson).to.haveOwnProperty('message')\r", + " pm.expect(responseJson).to.haveOwnProperty('data')\r", + "\r", + " pm.expect(responseJson.status).to.equals(\"success\")\r", + " pm.expect(responseJson.message).to.equals('Login successfully')\r", + " pm.expect(responseJson.data).to.be.an(\"object\")\r", + "})\r", + "\r", + "pm.test(\"response body data object should contain `accessToken` string\", () => {\r", + " const responseJson = pm.response.json()\r", + " const { data } = responseJson\r", + "\r", + " pm.expect(data).to.haveOwnProperty(\"accessToken\")\r", + " pm.expect(data.accessToken).to.be.an('string')\r", + "\r", + " pm.environment.set('accessToken', data.accessToken)\r", + " \r", + " const tokenParts = data.accessToken.split('.')\r", + " const encodedPayload = tokenParts[1]\r", + " const decodedPayload = Buffer.from(encodedPayload, 'base64').toString('utf-8')\r", + " const payload = JSON.parse(decodedPayload)\r", + " pm.environment.set('expiredAccessToken', payload.exp)\r", + " \r", + " pm.environment.set('refreshToken', pm.cookies.get('refreshToken'))\r", + "})" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"email\": \"{{newUserEmail}}\",\r\n \"password\": \"{{newUserPassword}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/api/v1/auth/signin", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "auth", + "signin" + ] + } + }, + "response": [] + }, + { + "name": "Refresh Token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('response status code should have 200 values', () => {\r", + " pm.response.to.have.status(200)\r", + "})\r", + "\r", + "pm.test('response Content-Type header should have application/json value', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8')\r", + "})\r", + "\r", + "pm.test('response body should an object', () => {\r", + " const responseJson = pm.response.json()\r", + " pm.expect(responseJson).to.be.an('object')\r", + "})\r", + "\r", + "pm.test('response body object should contain correct property and value', () => {\r", + " const responseJson = pm.response.json()\r", + "\r", + " pm.expect(responseJson).to.haveOwnProperty('status')\r", + " pm.expect(responseJson).to.haveOwnProperty('message')\r", + " pm.expect(responseJson).to.haveOwnProperty('data')\r", + "\r", + " pm.expect(responseJson.status).to.equals(\"success\")\r", + " pm.expect(responseJson.message).to.equals('Refresh token successfully')\r", + " pm.expect(responseJson.data).to.be.an(\"object\")\r", + "})\r", + "\r", + "pm.test(\"response body data object should contain `accessToken` string\", () => {\r", + " const responseJson = pm.response.json()\r", + " const { data } = responseJson\r", + "\r", + " pm.expect(data).to.haveOwnProperty(\"accessToken\")\r", + " pm.expect(data.accessToken).to.be.an('string')\r", + "\r", + " pm.environment.set('accessToken', data.accessToken)\r", + " \r", + " const tokenParts = data.accessToken.split('.')\r", + " const encodedPayload = tokenParts[1]\r", + " const decodedPayload = Buffer.from(encodedPayload, 'base64').toString('utf-8')\r", + " const payload = JSON.parse(decodedPayload)\r", + " pm.environment.set('expiredAccessToken', payload.exp)\r", + " \r", + " pm.environment.set('refreshToken', pm.cookies.get('refreshToken'))\r", + "})" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "", + "value": "", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{host}}/api/v1/token/refresh", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "token", + "refresh" + ] + } + }, + "response": [] + }, + { + "name": "Signout User", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('response status code should have 200 values', () => {\r", + " pm.response.to.have.status(200)\r", + "})\r", + "\r", + "pm.test('response Content-Type header should have application/json value', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8')\r", + "})\r", + "\r", + "pm.test('response body should an object', () => {\r", + " const responseJson = pm.response.json()\r", + " pm.expect(responseJson).to.be.an('object')\r", + "})\r", + "\r", + "pm.test('response body object should contain correct property and value', () => {\r", + " const responseJson = pm.response.json()\r", + "\r", + " pm.expect(responseJson).to.haveOwnProperty('status')\r", + " pm.expect(responseJson).to.haveOwnProperty('message')\r", + "\r", + " pm.expect(responseJson.status).to.equals(\"success\")\r", + " pm.expect(responseJson.message).to.equals('Logout successfully')\r", + "\r", + " pm.environment.set('refreshToken', null)\r", + "})" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "", + "value": "", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{host}}/api/v1/auth/signout", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "auth", + "signout" + ] + } + }, + "response": [] + }, + { + "name": "Get all user data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('status code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should be an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body object should have correct property and value', () => {\r", + " const responsJson = pm.response.json();\r", + "\r", + " pm.expect(responsJson).to.haveOwnProperty('status');\r", + " pm.expect(responsJson).to.haveOwnProperty(\"message\");\r", + " pm.expect(responsJson).to.haveOwnProperty('data');\r", + "\r", + " pm.expect(responsJson.status).to.equals('success');\r", + " pm.expect(responsJson.message).to.equals(\"Get all user data\");\r", + " pm.expect(responsJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body data object should have a array users and contains eleven items', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data } = responseJson;\r", + "\r", + " pm.expect(data).to.haveOwnProperty('users');\r", + " pm.expect(data.users).to.be.an('array');\r", + " pm.expect(data.users).to.lengthOf(11);\r", + "});\r", + "\r", + "pm.test('the users should have contains only id, fullName, email, gender, age, phoneNumber, createdAt, updatedAt, role, and profile property', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data: { users } } = responseJson;\r", + "\r", + " users.forEach((user) => {\r", + " pm.expect(Object.keys(user)).to.lengthOf(10);\r", + " pm.expect(user).to.haveOwnProperty('id');\r", + " pm.expect(user).to.haveOwnProperty('fullName');\r", + " pm.expect(user).to.haveOwnProperty('email');\r", + " pm.expect(user).to.haveOwnProperty('gender');\r", + " pm.expect(user).to.haveOwnProperty('age');\r", + " pm.expect(user).to.haveOwnProperty('phoneNumber');\r", + " pm.expect(user).to.haveOwnProperty('createdAt');\r", + " pm.expect(user).to.haveOwnProperty('updatedAt');\r", + " pm.expect(user).to.haveOwnProperty('role');\r", + " pm.expect(user).to.haveOwnProperty('profile');\r", + " });\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}", + "type": "text" + } + ], + "url": { + "raw": "{{host}}/api/v1/users", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "users" + ] + } + }, + "response": [] + }, + { + "name": "Get all user data by filters", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('status code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should be an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body object should have correct property and value', () => {\r", + " const responsJson = pm.response.json();\r", + "\r", + " pm.expect(responsJson).to.haveOwnProperty('status');\r", + " pm.expect(responsJson).to.haveOwnProperty(\"message\");\r", + " pm.expect(responsJson).to.haveOwnProperty('data');\r", + "\r", + " pm.expect(responsJson.status).to.equals('success');\r", + " pm.expect(responsJson.message).to.equals(\"Get all user data\");\r", + " pm.expect(responsJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body data object should have a array users and contains one items', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data } = responseJson;\r", + "\r", + " pm.expect(data).to.haveOwnProperty('users');\r", + " pm.expect(data.users).to.be.an('array');\r", + " pm.expect(data.users).to.lengthOf(1);\r", + "});\r", + "\r", + "pm.test('the users should have contains only id, fullName, email, gender, age, phoneNumber, createdAt, updatedAt, role, and profile property', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data: { users } } = responseJson;\r", + "\r", + " users.forEach((user) => {\r", + " pm.expect(Object.keys(user)).to.lengthOf(10);\r", + " pm.expect(user).to.haveOwnProperty('id');\r", + " pm.expect(user).to.haveOwnProperty('fullName');\r", + " pm.expect(user).to.haveOwnProperty('email');\r", + " pm.expect(user).to.haveOwnProperty('gender');\r", + " pm.expect(user).to.haveOwnProperty('age');\r", + " pm.expect(user).to.haveOwnProperty('phoneNumber');\r", + " pm.expect(user).to.haveOwnProperty('createdAt');\r", + " pm.expect(user).to.haveOwnProperty('updatedAt');\r", + " pm.expect(user).to.haveOwnProperty('role');\r", + " pm.expect(user).to.haveOwnProperty('profile');\r", + " });\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}", + "type": "text" + } + ], + "url": { + "raw": "{{host}}/api/v1/users?role={{newRoleName}}&fullName={{newUserFullName}}", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "users" + ], + "query": [ + { + "key": "role", + "value": "{{newRoleName}}" + }, + { + "key": "fullName", + "value": "{{newUserFullName}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Get a user data by id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('response code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body object should contain correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " \r", + " pm.expect(responseJson).to.haveOwnProperty('status');\r", + " pm.expect(responseJson).to.haveOwnProperty(\"message\");\r", + " pm.expect(responseJson).to.haveOwnProperty('data');\r", + "\r", + " pm.expect(responseJson.status).to.equals('success');\r", + " pm.expect(responseJson.message).to.equals('Get user data by id');\r", + " pm.expect(responseJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body data object should contain user object', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data } = responseJson;\r", + "\r", + " pm.expect(data).to.haveOwnProperty('user');\r", + " pm.expect(data.user).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('user object should contain correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data: { user } } = responseJson;\r", + "\r", + " pm.expect(user).to.haveOwnProperty('id');\r", + " pm.expect(user).to.haveOwnProperty('fullName');\r", + " pm.expect(user).to.haveOwnProperty('email');\r", + " pm.expect(user).to.haveOwnProperty('gender');\r", + " pm.expect(user).to.haveOwnProperty('age');\r", + " pm.expect(user).to.haveOwnProperty('phoneNumber');\r", + " pm.expect(user).to.haveOwnProperty('password');\r", + " pm.expect(user).to.haveOwnProperty('createdAt');\r", + " pm.expect(user).to.haveOwnProperty('updatedAt');\r", + " pm.expect(user).to.haveOwnProperty('role');\r", + " pm.expect(user).to.haveOwnProperty('profile');\r", + "\r", + " pm.expect(user.id).to.equals(pm.environment.get('userId'));\r", + " pm.expect(user.fullName).to.equals(pm.environment.get('newUserFullName'));\r", + " pm.expect(user.email).to.equals(pm.environment.get('newUserEmail'));\r", + " pm.expect(user.gender).to.equals(pm.environment.get('newUserGender'));\r", + " pm.expect(user.age).to.equals(pm.environment.get('newUserAge'));\r", + " pm.expect(user.phoneNumber).to.equals(pm.environment.get('newUserPhoneNumber'));\r", + " pm.expect(user.password).to.not.equals('');\r", + " pm.expect(user.createdAt).to.not.equals('');\r", + " pm.expect(user.updatedAt).to.not.equals('');\r", + " pm.expect(user.role).to.not.equals('');\r", + " pm.expect(user.profile).to.not.equals('');\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}", + "type": "text" + } + ], + "url": { + "raw": "{{host}}/api/v1/users/{{userId}}", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "users", + "{{userId}}" + ] + } + }, + "response": [] + }, + { + "name": "Update all field user data by id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('status code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should be an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body should have correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " \r", + " pm.expect(responseJson).to.haveOwnProperty('status');\r", + " pm.expect(responseJson).to.haveOwnProperty('message');\r", + " pm.expect(responseJson).to.haveOwnProperty('data');\r", + "\r", + " pm.expect(responseJson.status).to.equals('success');\r", + " pm.expect(responseJson.message).to.equals('User data updated successfully');\r", + " pm.expect(responseJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('when get detail user', () => {\r", + " pm.sendRequest({\r", + " url: `${pm.environment.get('host')}/api/v1/users/${pm.environment.get('userId')}`,\r", + " auth: {\r", + " type: \"bearer\",\r", + " bearer: [\r", + " {\r", + " key: \"token\",\r", + " value: `${pm.environment.get('accessToken')}`,\r", + " type: \"string\"\r", + " }\r", + " ]\r", + " },\r", + " headers: {\r", + " \"Content-Type\": \"application/json\"\r", + " }\r", + " }, (error, response) => {\r", + " const responseJson = response.json();\r", + " const { data: { user } } = responseJson;\r", + "\r", + " pm.test('user object should contain updated values', () => {\r", + " pm.expect(user).to.haveOwnProperty('id');\r", + " pm.expect(user).to.haveOwnProperty('fullName');\r", + " pm.expect(user).to.haveOwnProperty('email');\r", + " pm.expect(user).to.haveOwnProperty('age');\r", + " pm.expect(user).to.haveOwnProperty('phoneNumber');\r", + "\r", + " pm.expect(user.id).to.equals(pm.environment.get('userId'));\r", + " pm.expect(user.fullName).to.equals(pm.environment.get('updateUserFullName'));\r", + " pm.expect(user.email).to.equals(pm.environment.get('updateUserEmail'));\r", + " pm.expect(user.gender).to.equals(pm.environment.get('newUserGender'));\r", + " pm.expect(user.age).to.equals(parseInt(pm.environment.get('updateUserAge')));\r", + " pm.expect(user.phoneNumber).to.equals(pm.environment.get('updateUserPhoneNumber'));\r", + " }); \r", + " });\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"fullName\": \"{{updateUserFullName}}\",\r\n \"email\": \"{{updateUserEmail}}\",\r\n \"age\": {{updateUserAge}},\r\n \"phoneNumber\": \"{{updateUserPhoneNumber}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/api/v1/users/{{userId}}", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "users", + "{{userId}}" + ] + } + }, + "response": [] + }, + { + "name": "Update partial field user data by id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('status code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should be an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body should have correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " \r", + " pm.expect(responseJson).to.haveOwnProperty('status');\r", + " pm.expect(responseJson).to.haveOwnProperty('message');\r", + " pm.expect(responseJson).to.haveOwnProperty('data');\r", + "\r", + " pm.expect(responseJson.status).to.equals('success');\r", + " pm.expect(responseJson.message).to.equals('User data partially updated successfully');\r", + " pm.expect(responseJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('when get detail user', () => {\r", + " pm.sendRequest({\r", + " url: `${pm.environment.get('host')}/api/v1/users/${pm.environment.get('userId')}`,\r", + " auth: {\r", + " type: \"bearer\",\r", + " bearer: [\r", + " {\r", + " key: \"token\",\r", + " value: `${pm.environment.get('accessToken')}`,\r", + " type: \"string\"\r", + " }\r", + " ]\r", + " },\r", + " headers: {\r", + " \"Content-Type\": \"application/json\"\r", + " }\r", + " }, (error, response) => {\r", + " const responseJson = response.json();\r", + " const { data: { user } } = responseJson;\r", + "\r", + " pm.test('user object should contain updated values', () => {\r", + " pm.expect(user).to.haveOwnProperty('id');\r", + " pm.expect(user).to.haveOwnProperty('phoneNumber');\r", + "\r", + " pm.expect(user.id).to.equals(pm.environment.get('userId'));\r", + " pm.expect(user.phoneNumber).to.equals(pm.environment.get('updatePartialUserPhoneNumber'));\r", + " });\r", + " });\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"phoneNumber\": \"{{updatePartialUserPhoneNumber}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/api/v1/users/{{userId}}", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "users", + "{{userId}}" + ] + } + }, + "response": [] + }, + { + "name": "Delete User By Id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('status code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should be an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body should have correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " \r", + " pm.expect(responseJson).to.haveOwnProperty('status');\r", + " pm.expect(responseJson).to.haveOwnProperty('message');\r", + " pm.expect(responseJson).to.haveOwnProperty('data')\r", + "\r", + " pm.expect(responseJson.status).to.equals('success');\r", + " pm.expect(responseJson.message).to.equals('Successfully delete user data');\r", + " pm.expect(responseJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('when get detail users', () => {\r", + " pm.sendRequest({\r", + " url: `${pm.environment.get('host')}/api/v1/users/${pm.environment.get('userId')}`,\r", + " auth: {\r", + " type: \"bearer\",\r", + " bearer: [\r", + " {\r", + " key: \"token\",\r", + " value: `${pm.environment.get('accessToken')}`,\r", + " type: \"string\"\r", + " }\r", + " ]\r", + " },\r", + " headers: {\r", + " \"Content-Type\": \"application/json\"\r", + " }\r", + " }, (error, response) => {\r", + " pm.test('The user should be not found', () => {\r", + " pm.expect(response.code).to.equals(404);\r", + " });\r", + " });\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}", + "type": "text" + } + ], + "url": { + "raw": "{{host}}/api/v1/users/{{userId}}", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "users", + "{{userId}}" + ] + } + }, + "response": [] + }, + { + "name": "Get all work data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('status code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should be an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body object should have correct property and value', () => {\r", + " const responsJson = pm.response.json();\r", + "\r", + " pm.expect(responsJson).to.haveOwnProperty('status');\r", + " pm.expect(responsJson).to.haveOwnProperty(\"message\");\r", + " pm.expect(responsJson).to.haveOwnProperty('data');\r", + "\r", + " pm.expect(responsJson.status).to.equals('success');\r", + " pm.expect(responsJson.message).to.equals(\"Get all work profile user\");\r", + " pm.expect(responsJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body data object should have a array works and contains two items', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data } = responseJson;\r", + "\r", + " pm.expect(data).to.haveOwnProperty('works');\r", + " pm.expect(data.works).to.be.an('array');\r", + " pm.expect(data.works).to.lengthOf(12);\r", + "});\r", + "\r", + "pm.test('the works should have contains only id, name, createdAt, and updatedAt property', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data: { works } } = responseJson;\r", + "\r", + " works.forEach((work) => {\r", + " pm.expect(Object.keys(work)).to.lengthOf(4);\r", + " pm.expect(work).to.haveOwnProperty('id');\r", + " pm.expect(work).to.haveOwnProperty('name');\r", + " pm.expect(work).to.haveOwnProperty('createdAt');\r", + " pm.expect(work).to.haveOwnProperty('updatedAt');\r", + " });\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{host}}/api/v1/works", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "works" + ] + } + }, + "response": [] + }, + { + "name": "Get work data by name", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('status code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should be an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body object should have correct property and value', () => {\r", + " const responsJson = pm.response.json();\r", + "\r", + " pm.expect(responsJson).to.haveOwnProperty('status');\r", + " pm.expect(responsJson).to.haveOwnProperty(\"message\");\r", + " pm.expect(responsJson).to.haveOwnProperty('data');\r", + "\r", + " pm.expect(responsJson.status).to.equals('success');\r", + " pm.expect(responsJson.message).to.equals(\"Get all work profile user\");\r", + " pm.expect(responsJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body data object should have a array works and contains one items', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data } = responseJson;\r", + "\r", + " pm.expect(data).to.haveOwnProperty('works');\r", + " pm.expect(data.works).to.be.an('array');\r", + " pm.expect(data.works).to.lengthOf(1);\r", + "});\r", + "\r", + "pm.test('the works should have contains only id, name, createdAt, and updatedAt property', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data: { works } } = responseJson;\r", + "\r", + " works.forEach((work) => {\r", + " pm.expect(Object.keys(work)).to.lengthOf(4);\r", + " pm.expect(work).to.haveOwnProperty('id');\r", + " pm.expect(work).to.haveOwnProperty('name');\r", + " pm.expect(work).to.haveOwnProperty('createdAt');\r", + " pm.expect(work).to.haveOwnProperty('updatedAt');\r", + " });\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{host}}/api/v1/works?name=Pekerja Seni", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "works" + ], + "query": [ + { + "key": "name", + "value": "Pekerja Seni" + } + ] + } + }, + "response": [] + }, + { + "name": "Insert Work Data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('status code should be 201', () => {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should be an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body should have correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " \r", + " pm.expect(responseJson).to.haveOwnProperty('status');\r", + " pm.expect(responseJson).to.haveOwnProperty('message');\r", + " pm.expect(responseJson).to.haveOwnProperty('data');\r", + "\r", + " pm.expect(responseJson.status).to.equals('success');\r", + " pm.expect(responseJson.message).to.equals('Work created successfully');\r", + " pm.expect(responseJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body data should contain workId', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data } = responseJson;\r", + "\r", + " pm.expect(data).to.haveOwnProperty('workId');\r", + " pm.expect(data.workId).to.not.equals('');\r", + "\r", + " pm.environment.set('workId', data.workId);\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"{{newWorkName}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/api/v1/works", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "works" + ] + } + }, + "response": [] + }, + { + "name": "Get a Work by Id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('response code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body object should contain correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " \r", + " pm.expect(responseJson).to.haveOwnProperty('status');\r", + " pm.expect(responseJson).to.haveOwnProperty(\"message\");\r", + " pm.expect(responseJson).to.haveOwnProperty('data');\r", + "\r", + " pm.expect(responseJson.status).to.equals('success');\r", + " pm.expect(responseJson.message).to.equals('Get work data by id');\r", + " pm.expect(responseJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body data object should contain work object', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data } = responseJson;\r", + "\r", + " pm.expect(data).to.haveOwnProperty('work');\r", + " pm.expect(data.work).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('work object should contain correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data: { work } } = responseJson;\r", + "\r", + " pm.expect(work).to.haveOwnProperty('id');\r", + " pm.expect(work).to.haveOwnProperty('name');\r", + " pm.expect(work).to.haveOwnProperty('createdAt');\r", + " pm.expect(work).to.haveOwnProperty('updatedAt');\r", + "\r", + " pm.expect(work.id).to.equals(pm.environment.get('workId'));\r", + " pm.expect(work.name).to.equals(pm.environment.get('newWorkName'));\r", + " pm.expect(work.createdAt).to.not.equals('');\r", + " pm.expect(work.updatedAt).to.not.equals('');\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{host}}/api/v1/works/{{workId}}", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "works", + "{{workId}}" + ] + } + }, + "response": [] + }, + { + "name": "Update Work By Id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('status code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should be an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body should have correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " \r", + " pm.expect(responseJson).to.haveOwnProperty('status');\r", + " pm.expect(responseJson).to.haveOwnProperty('message');\r", + " pm.expect(responseJson).to.haveOwnProperty('data');\r", + "\r", + " pm.expect(responseJson.status).to.equals('success');\r", + " pm.expect(responseJson.message).to.equals('Work updated successfully');\r", + " pm.expect(responseJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('when get detail work', () => {\r", + " pm.sendRequest(`${pm.environment.get('host')}/api/v1/works/${pm.environment.get('workId')}`, (error, response) => {\r", + " const responseJson = response.json();\r", + " const { data: { work } } = responseJson;\r", + "\r", + " pm.test('work object should contain updated values', () => {\r", + " pm.expect(work).to.haveOwnProperty('id');\r", + " pm.expect(work).to.haveOwnProperty('name');\r", + " pm.expect(work).to.haveOwnProperty('createdAt');\r", + " pm.expect(work).to.haveOwnProperty('updatedAt');\r", + " \r", + " pm.expect(work.id).to.equals(pm.environment.get('workId'));\r", + " pm.expect(work.name).to.equals(pm.environment.get('updateWorkName'));\r", + " pm.expect(work.createdAt).to.not.equals('');\r", + " pm.expect(work.updatedAt).to.not.equals('');\r", + " }); \r", + " });\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"{{updateWorkName}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/api/v1/works/{{workId}}", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "works", + "{{workId}}" + ] + } + }, + "response": [] + }, + { + "name": "Delete Work By Id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('status code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should be an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body should have correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " \r", + " pm.expect(responseJson).to.haveOwnProperty('status');\r", + " pm.expect(responseJson).to.haveOwnProperty('message');\r", + "\r", + " pm.expect(responseJson.status).to.equals('success');\r", + " pm.expect(responseJson.message).to.equals('Work deleted successfully');\r", + "});\r", + "\r", + "pm.test('when get detail works', () => {\r", + " pm.sendRequest(`${pm.environment.get('host')}/api/v1/works/${pm.environment.get('workId')}`, (error, response) => {\r", + " pm.test('The work should be not found', () => {\r", + " pm.expect(response.code).to.equals(404);\r", + " }); \r", + " });\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/api/v1/works/{{workId}}", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "works", + "{{workId}}" + ] + } + }, + "response": [] + }, + { + "name": "Get all education data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('status code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should be an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body object should have correct property and value', () => {\r", + " const responsJson = pm.response.json();\r", + "\r", + " pm.expect(responsJson).to.haveOwnProperty('status');\r", + " pm.expect(responsJson).to.haveOwnProperty(\"message\");\r", + " pm.expect(responsJson).to.haveOwnProperty('data');\r", + "\r", + " pm.expect(responsJson.status).to.equals('success');\r", + " pm.expect(responsJson.message).to.equals(\"Get all education profile user\");\r", + " pm.expect(responsJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body data object should have a array educations and contains two items', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data } = responseJson;\r", + "\r", + " pm.expect(data).to.haveOwnProperty('educations');\r", + " pm.expect(data.educations).to.be.an('array');\r", + " pm.expect(data.educations).to.lengthOf(8);\r", + "});\r", + "\r", + "pm.test('the educations should have contains only id, name, createdAt, and updatedAt property', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data: { educations } } = responseJson;\r", + "\r", + " educations.forEach((education) => {\r", + " pm.expect(Object.keys(education)).to.lengthOf(4);\r", + " pm.expect(education).to.haveOwnProperty('id');\r", + " pm.expect(education).to.haveOwnProperty('name');\r", + " pm.expect(education).to.haveOwnProperty('createdAt');\r", + " pm.expect(education).to.haveOwnProperty('updatedAt');\r", + " });\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{host}}/api/v1/educations", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "educations" + ] + } + }, + "response": [] + }, + { + "name": "Get education data by name", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('status code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should be an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body object should have correct property and value', () => {\r", + " const responsJson = pm.response.json();\r", + "\r", + " pm.expect(responsJson).to.haveOwnProperty('status');\r", + " pm.expect(responsJson).to.haveOwnProperty(\"message\");\r", + " pm.expect(responsJson).to.haveOwnProperty('data');\r", + "\r", + " pm.expect(responsJson.status).to.equals('success');\r", + " pm.expect(responsJson.message).to.equals(\"Get all education profile user\");\r", + " pm.expect(responsJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body data object should have a array educations and contains one items', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data } = responseJson;\r", + "\r", + " pm.expect(data).to.haveOwnProperty('educations');\r", + " pm.expect(data.educations).to.be.an('array');\r", + " pm.expect(data.educations).to.lengthOf(1);\r", + "});\r", + "\r", + "pm.test('the educations should have contains only id, name, createdAt, and updatedAt property', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data: { educations } } = responseJson;\r", + "\r", + " educations.forEach((education) => {\r", + " pm.expect(Object.keys(education)).to.lengthOf(4);\r", + " pm.expect(education).to.haveOwnProperty('id');\r", + " pm.expect(education).to.haveOwnProperty('name');\r", + " pm.expect(education).to.haveOwnProperty('createdAt');\r", + " pm.expect(education).to.haveOwnProperty('updatedAt');\r", + " });\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{host}}/api/v1/educations?name=SMP / Sederajat", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "educations" + ], + "query": [ + { + "key": "name", + "value": "SMP / Sederajat" + } + ] + } + }, + "response": [] + }, + { + "name": "Insert education data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('status code should be 201', () => {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should be an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body should have correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " \r", + " pm.expect(responseJson).to.haveOwnProperty('status');\r", + " pm.expect(responseJson).to.haveOwnProperty('message');\r", + " pm.expect(responseJson).to.haveOwnProperty('data');\r", + "\r", + " pm.expect(responseJson.status).to.equals('success');\r", + " pm.expect(responseJson.message).to.equals('Education created successfully');\r", + " pm.expect(responseJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body data should contain educationId', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data } = responseJson;\r", + "\r", + " pm.expect(data).to.haveOwnProperty('educationId');\r", + " pm.expect(data.educationId).to.not.equals('');\r", + "\r", + " pm.environment.set('educationId', data.educationId);\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"{{newEducationName}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/api/v1/educations", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "educations" + ] + } + }, + "response": [] + }, + { + "name": "Get a Education by Id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('response code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body object should contain correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " \r", + " pm.expect(responseJson).to.haveOwnProperty('status');\r", + " pm.expect(responseJson).to.haveOwnProperty(\"message\");\r", + " pm.expect(responseJson).to.haveOwnProperty('data');\r", + "\r", + " pm.expect(responseJson.status).to.equals('success');\r", + " pm.expect(responseJson.message).to.equals('Get education data by id');\r", + " pm.expect(responseJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body data object should contain education object', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data } = responseJson;\r", + "\r", + " pm.expect(data).to.haveOwnProperty('education');\r", + " pm.expect(data.education).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('education object should contain correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data: { education } } = responseJson;\r", + "\r", + " pm.expect(education).to.haveOwnProperty('id');\r", + " pm.expect(education).to.haveOwnProperty('name');\r", + " pm.expect(education).to.haveOwnProperty('createdAt');\r", + " pm.expect(education).to.haveOwnProperty('updatedAt');\r", + "\r", + " pm.expect(education.id).to.equals(pm.environment.get('educationId'));\r", + " pm.expect(education.name).to.equals(pm.environment.get('newEducationName'));\r", + " pm.expect(education.createdAt).to.not.equals('');\r", + " pm.expect(education.updatedAt).to.not.equals('');\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{host}}/api/v1/educations/{{educationId}}", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "educations", + "{{educationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Update Education By Id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('status code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should be an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body should have correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " \r", + " pm.expect(responseJson).to.haveOwnProperty('status');\r", + " pm.expect(responseJson).to.haveOwnProperty('message');\r", + " pm.expect(responseJson).to.haveOwnProperty('data');\r", + "\r", + " pm.expect(responseJson.status).to.equals('success');\r", + " pm.expect(responseJson.message).to.equals('Education updated successfully');\r", + " pm.expect(responseJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('when get detail education', () => {\r", + " pm.sendRequest(`${pm.environment.get('host')}/api/v1/educations/${pm.environment.get('educationId')}`, (error, response) => {\r", + " const responseJson = response.json();\r", + " const { data: { education } } = responseJson;\r", + "\r", + " pm.test('education object should contain updated values', () => {\r", + " pm.expect(education).to.haveOwnProperty('id');\r", + " pm.expect(education).to.haveOwnProperty('name');\r", + " pm.expect(education).to.haveOwnProperty('createdAt');\r", + " pm.expect(education).to.haveOwnProperty('updatedAt');\r", + " \r", + " pm.expect(education.id).to.equals(pm.environment.get('educationId'));\r", + " pm.expect(education.name).to.equals(pm.environment.get('updateEducationName'));\r", + " pm.expect(education.createdAt).to.not.equals('');\r", + " pm.expect(education.updatedAt).to.not.equals('');\r", + " }); \r", + " });\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"{{updateEducationName}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/api/v1/educations/{{educationId}}", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "educations", + "{{educationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Delete Education By Id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('status code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should be an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body should have correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " \r", + " pm.expect(responseJson).to.haveOwnProperty('status');\r", + " pm.expect(responseJson).to.haveOwnProperty('message');\r", + "\r", + " pm.expect(responseJson.status).to.equals('success');\r", + " pm.expect(responseJson.message).to.equals('Education deleted successfully');\r", + "});\r", + "\r", + "pm.test('when get detail educations', () => {\r", + " pm.sendRequest(`${pm.environment.get('host')}/api/v1/educations/${pm.environment.get('educationId')}`, (error, response) => {\r", + " pm.test('The education should be not found', () => {\r", + " pm.expect(response.code).to.equals(404);\r", + " }); \r", + " });\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{host}}/api/v1/educations/{{educationId}}", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "educations", + "{{educationId}}" + ] + } + }, + "response": [] + }, + { + "name": "Get Users Profile by User Id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('response code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body object should contain correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " \r", + " pm.expect(responseJson).to.haveOwnProperty('status');\r", + " pm.expect(responseJson).to.haveOwnProperty(\"message\");\r", + " pm.expect(responseJson).to.haveOwnProperty('data');\r", + "\r", + " pm.expect(responseJson.status).to.equals('success');\r", + " pm.expect(responseJson.message).to.equals('Get user data by id');\r", + " pm.expect(responseJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body data object should contain user object', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data } = responseJson;\r", + "\r", + " pm.expect(data).to.haveOwnProperty('profile');\r", + " pm.expect(data.profile).to.be.an('object');\r", + "\r", + " pm.environment.set('profileId', data.profile.id);\r", + "});\r", + "\r", + "pm.test('profile object should contain correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " const { data: { profile } } = responseJson;\r", + "\r", + " pm.expect(profile).to.haveOwnProperty('id');\r", + " pm.expect(profile).to.haveOwnProperty('avatar');\r", + " pm.expect(profile).to.haveOwnProperty('maritalStatus');\r", + " pm.expect(profile).to.haveOwnProperty('certifiedStatus');\r", + " pm.expect(profile).to.haveOwnProperty('work');\r", + " pm.expect(profile).to.haveOwnProperty('education');\r", + " pm.expect(profile).to.haveOwnProperty('createdAt');\r", + " pm.expect(profile).to.haveOwnProperty('updatedAt');\r", + "\r", + " pm.expect(profile.id).to.equals(pm.environment.get('profileId'));\r", + " const avatarUrl = pm.environment.get('newUserGender') === \"Perempuan\" ? \"https://storage.googleapis.com/finboost-avatar-user/female.png\" : \"https://storage.googleapis.com/finboost-avatar-user/male.png\"\r", + " pm.expect(profile.avatar).to.equals(avatarUrl);\r", + " pm.expect(profile.createdAt).to.not.equals('');\r", + " pm.expect(profile.updatedAt).to.not.equals('');\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}", + "type": "text" + } + ], + "url": { + "raw": "{{host}}/api/v1/users/{{userId}}/profile", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "users", + "{{userId}}", + "profile" + ] + } + }, + "response": [] + }, + { + "name": "Update Partial User Profile by User Id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('status code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should be an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body should have correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " \r", + " pm.expect(responseJson).to.haveOwnProperty('status');\r", + " pm.expect(responseJson).to.haveOwnProperty('message');\r", + " pm.expect(responseJson).to.haveOwnProperty('data');\r", + "\r", + " pm.expect(responseJson.status).to.equals('success');\r", + " pm.expect(responseJson.message).to.equals('User profile data updated successfully');\r", + " pm.expect(responseJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('when get detail user', () => {\r", + " pm.sendRequest({\r", + " url: `${pm.environment.get('host')}/api/v1/users/${pm.environment.get('userId')}/profile`,\r", + " auth: {\r", + " type: \"bearer\",\r", + " bearer: [\r", + " {\r", + " key: \"token\",\r", + " value: `${pm.environment.get('accessToken')}`,\r", + " type: \"string\"\r", + " }\r", + " ]\r", + " },\r", + " headers: {\r", + " \"Content-Type\": \"application/json\"\r", + " }\r", + " }, (error, response) => {\r", + " const responseJson = response.json();\r", + " const { data: { profile } } = responseJson;\r", + "\r", + " pm.test('profile object should contain updated values', () => {\r", + " pm.expect(profile).to.haveOwnProperty('id');\r", + " pm.expect(profile).to.haveOwnProperty('maritalStatus');\r", + "\r", + " pm.expect(profile.id).to.equals(pm.environment.get('profileId'));\r", + " pm.expect(profile.maritalStatus).to.equals(pm.environment.get('updateUserProfileMaritalStatus'));\r", + " });\r", + " });\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"maritalStatus\": \"{{updateUserProfileMaritalStatus}}\"\r\n // \"workId\": \"{{workId}}\",\r\n // \"educationId\": \"{{educationId}}\",\r\n // \"certifiedStatus\": null\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/api/v1/users/{{userId}}/profile", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "users", + "{{userId}}", + "profile" + ] + } + }, + "response": [] + }, + { + "name": "Delete User Profile by User Id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('status code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should be an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body should have correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " \r", + " pm.expect(responseJson).to.haveOwnProperty('status');\r", + " pm.expect(responseJson).to.haveOwnProperty('message');\r", + " pm.expect(responseJson).to.haveOwnProperty('data')\r", + "\r", + " pm.expect(responseJson.status).to.equals('success');\r", + " pm.expect(responseJson.message).to.equals('Successfully delete user profile data');\r", + " pm.expect(responseJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('when get detail users', () => {\r", + " pm.sendRequest({\r", + " url: `${pm.environment.get('host')}/api/v1/users/${pm.environment.get('userId')}/profile`,\r", + " auth: {\r", + " type: \"bearer\",\r", + " bearer: [\r", + " {\r", + " key: \"token\",\r", + " value: `${pm.environment.get('accessToken')}`,\r", + " type: \"string\"\r", + " }\r", + " ]\r", + " },\r", + " headers: {\r", + " \"Content-Type\": \"application/json\"\r", + " }\r", + " }, (error, response) => {\r", + " const responseJson = response.json();\r", + " const { data: { profile } } = responseJson;\r", + "\r", + " pm.test('The profile should be null value', () => {\r", + " pm.expect(profile).to.haveOwnProperty('id');\r", + " pm.expect(profile).to.haveOwnProperty('maritalStatus');\r", + "\r", + " pm.expect(profile.id).to.equals(pm.environment.get('profileId'));\r", + " pm.expect(profile.maritalStatus).to.equals(null);\r", + " });\r", + " });\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "jwt", + "jwt": [ + { + "key": "addTokenTo", + "value": "header", + "type": "string" + }, + { + "key": "headerPrefix", + "value": "Bearer", + "type": "string" + }, + { + "key": "algorithm", + "value": "HS256", + "type": "string" + }, + { + "key": "isSecretBase64Encoded", + "value": false, + "type": "boolean" + }, + { + "key": "payload", + "value": "{}", + "type": "string" + }, + { + "key": "queryParamKey", + "value": "token", + "type": "string" + }, + { + "key": "header", + "value": "{}", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}", + "type": "text" + } + ], + "url": { + "raw": "{{host}}/api/v1/users/{{userId}}/profile", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "users", + "{{userId}}", + "profile" + ] + } + }, + "response": [] + }, + { + "name": "Update avatar user", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('status code should be 200', () => {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('response header Content-Type should be application/json', () => {\r", + " pm.expect(pm.response.headers.get('Content-Type')).to.equals('application/json; charset=utf-8');\r", + "});\r", + "\r", + "pm.test('response body should be an object', () => {\r", + " const responseJson = pm.response.json();\r", + " pm.expect(responseJson).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('response body should have correct property and value', () => {\r", + " const responseJson = pm.response.json();\r", + " \r", + " pm.expect(responseJson).to.haveOwnProperty('status');\r", + " pm.expect(responseJson).to.haveOwnProperty('message');\r", + " pm.expect(responseJson).to.haveOwnProperty('data');\r", + "\r", + " pm.expect(responseJson.status).to.equals('success');\r", + " pm.expect(responseJson.message).to.equals('Avatar user updated successfully');\r", + " pm.expect(responseJson.data).to.be.an('object');\r", + "});\r", + "\r", + "pm.test('when get detail user', () => {\r", + " pm.sendRequest({\r", + " url: `${pm.environment.get('host')}/api/v1/users/${pm.environment.get('userId')}/profile`,\r", + " auth: {\r", + " type: \"bearer\",\r", + " bearer: [\r", + " {\r", + " key: \"token\",\r", + " value: `${pm.environment.get('accessToken')}`,\r", + " type: \"string\"\r", + " }\r", + " ]\r", + " },\r", + " headers: {\r", + " \"Content-Type\": \"application/json\"\r", + " }\r", + " }, (error, response) => {\r", + " const responseJson = response.json();\r", + " const { data: { profile } } = responseJson;\r", + "\r", + " pm.test('profile object should contain updated values', () => {\r", + " pm.expect(profile).to.haveOwnProperty('id');\r", + " pm.expect(profile).to.haveOwnProperty('avatar');\r", + "\r", + " pm.expect(profile.id).to.equals(pm.environment.get('profileId'));\r", + " const timestamp = pm.environment.get('timestamp');\r", + " const avatarUrl = `https://storage.googleapis.com/finboost-avatar-user/${pm.environment.get('userId')}-${timestamp}.png`;\r", + " pm.expect(profile.avatar).to.equals(avatarUrl);\r", + " });\r", + " });\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "// Pre-request Script\r", + "const moment = require('moment');\r", + "\r", + "// Generate a timestamp in the desired format\r", + "const timestamp = moment().format('YYYYMMDD-HHmmss');\r", + "\r", + "// Set the timestamp as an environment variable\r", + "pm.environment.set('timestamp', timestamp);\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}", + "type": "text" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "avatar", + "type": "file", + "src": "/D:/Pictures/Me/me-circle.png" + } + ] + }, + "url": { + "raw": "{{host}}/api/v1/users/{{userId}}/profile/avatar", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "users", + "{{userId}}", + "profile", + "avatar" + ] + } + }, + "response": [] + }, + { + "name": "Chat Predict", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{accessToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"question\": \"Apa finansial?\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/api/v1/chats/ai/predict", + "host": [ + "{{host}}" + ], + "path": [ + "api", + "v1", + "chats", + "ai", + "predict" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/postman/Finboost Back-End.postman_environment.json b/postman/Finboost Back-End.postman_environment.json new file mode 100644 index 0000000..73c1d83 --- /dev/null +++ b/postman/Finboost Back-End.postman_environment.json @@ -0,0 +1,183 @@ +{ + "id": "e6a9576b-0b70-45f9-9f6c-27e73cda858f", + "name": "Finboost Back-End", + "values": [ + { + "key": "host", + "value": "https://dev---finboost-backend-rtalegstna-et.a.run.app", + "type": "default", + "enabled": true + }, + { + "key": "accessToken", + "value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ijk0ODY5ZDlkLTJlZmEtNDVmMC1iZTEzLWIyY2IxMmNhMzFjNCIsImZ1bGxOYW1lIjoiSm9obiBEb2UiLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwicm9sZSI6IlVzZXIiLCJpYXQiOjE3MTY5OTY0OTMsImV4cCI6MTcxNzAwMDA5M30.t5Wuv_kjtpr9B9RuJAItLiPyh1w-iY15b_KR0Ewi3ug", + "type": "default", + "enabled": true + }, + { + "key": "expiredAccessToken", + "value": "", + "type": "any", + "enabled": true + }, + { + "key": "refreshToken", + "value": "", + "type": "any", + "enabled": true + }, + { + "key": "userId", + "value": "d4ff2f36-99e3-4ccc-b5e2-5e893a736647", + "type": "default", + "enabled": true + }, + { + "key": "roleId", + "value": "613122ab-e205-4eda-a18b-758fd4bbe78a", + "type": "default", + "enabled": true + }, + { + "key": "newRoleName", + "value": "Test Role", + "type": "default", + "enabled": true + }, + { + "key": "updateRoleName", + "value": "Test Role Update", + "type": "default", + "enabled": true + }, + { + "key": "workId", + "value": "6564fab3-cddb-4362-8a4b-ea44e4064131", + "type": "default", + "enabled": true + }, + { + "key": "newWorkName", + "value": "Test Work", + "type": "default", + "enabled": true + }, + { + "key": "updateWorkName", + "value": "Test Work Update", + "type": "default", + "enabled": true + }, + { + "key": "educationId", + "value": "98dc90c9-db50-49ed-bbe2-e139593e473d", + "type": "default", + "enabled": true + }, + { + "key": "newEducationName", + "value": "Test Education", + "type": "default", + "enabled": true + }, + { + "key": "updateEducationName", + "value": "Test Education Update", + "type": "default", + "enabled": true + }, + { + "key": "newUserFullName", + "value": "Jane Doe", + "type": "default", + "enabled": true + }, + { + "key": "updateUserFullName", + "value": "Jane Doe Update", + "type": "default", + "enabled": true + }, + { + "key": "newUserEmail", + "value": "jane.doe@example.com", + "type": "default", + "enabled": true + }, + { + "key": "updateUserEmail", + "value": "jane.doe.update@example.com", + "type": "default", + "enabled": true + }, + { + "key": "newUserGender", + "value": "Perempuan", + "type": "default", + "enabled": true + }, + { + "key": "newUserAge", + "value": "23", + "type": "default", + "enabled": true + }, + { + "key": "updateUserAge", + "value": "24", + "type": "default", + "enabled": true + }, + { + "key": "newUserPhoneNumber", + "value": "081234", + "type": "default", + "enabled": true + }, + { + "key": "updateUserPhoneNumber", + "value": "0987654321", + "type": "default", + "enabled": true + }, + { + "key": "updatePartialUserPhoneNumber", + "value": "01234598765", + "type": "default", + "enabled": true + }, + { + "key": "newUserPassword", + "value": "12345", + "type": "default", + "enabled": true + }, + { + "key": "updateUserPassword", + "value": "54321", + "type": "default", + "enabled": true + }, + { + "key": "profileId", + "value": "", + "type": "any", + "enabled": true + }, + { + "key": "updateUserProfileMaritalStatus", + "value": "Menikah", + "type": "default", + "enabled": true + }, + { + "key": "timestamp", + "value": "", + "type": "any", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2024-06-14T08:22:33.293Z", + "_postman_exported_using": "Postman/10.24.25" +} \ No newline at end of file diff --git a/prisma/migrations/20240529075810_add_phone_number_on_user/migration.sql b/prisma/migrations/20240529075810_add_phone_number_on_user/migration.sql new file mode 100644 index 0000000..3306637 --- /dev/null +++ b/prisma/migrations/20240529075810_add_phone_number_on_user/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `phoneNumber` to the `users` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE `users` ADD COLUMN `phoneNumber` VARCHAR(191) NOT NULL; diff --git a/prisma/migrations/20240605121648_change_nullable_user_profile_field/migration.sql b/prisma/migrations/20240605121648_change_nullable_user_profile_field/migration.sql new file mode 100644 index 0000000..d2b804b --- /dev/null +++ b/prisma/migrations/20240605121648_change_nullable_user_profile_field/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE `profiles` MODIFY `avatar` VARCHAR(191) NULL, + MODIFY `maritalStatus` ENUM('Lajang', 'Menikah', 'Cerai') NULL, + MODIFY `certifiedStatus` VARCHAR(191) NULL; diff --git a/prisma/migrations/20240605122745_allow_null_for_profile_relations/migration.sql b/prisma/migrations/20240605122745_allow_null_for_profile_relations/migration.sql new file mode 100644 index 0000000..2732002 --- /dev/null +++ b/prisma/migrations/20240605122745_allow_null_for_profile_relations/migration.sql @@ -0,0 +1,15 @@ +-- DropForeignKey +ALTER TABLE `profiles` DROP FOREIGN KEY `profiles_educationId_fkey`; + +-- DropForeignKey +ALTER TABLE `profiles` DROP FOREIGN KEY `profiles_workId_fkey`; + +-- AlterTable +ALTER TABLE `profiles` MODIFY `workId` VARCHAR(191) NULL, + MODIFY `educationId` VARCHAR(191) NULL; + +-- AddForeignKey +ALTER TABLE `profiles` ADD CONSTRAINT `profiles_workId_fkey` FOREIGN KEY (`workId`) REFERENCES `works`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `profiles` ADD CONSTRAINT `profiles_educationId_fkey` FOREIGN KEY (`educationId`) REFERENCES `educations`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20240605131036_add_cascade_delete/migration.sql b/prisma/migrations/20240605131036_add_cascade_delete/migration.sql new file mode 100644 index 0000000..fc05826 --- /dev/null +++ b/prisma/migrations/20240605131036_add_cascade_delete/migration.sql @@ -0,0 +1,5 @@ +-- DropForeignKey +ALTER TABLE `profiles` DROP FOREIGN KEY `profiles_userId_fkey`; + +-- AddForeignKey +ALTER TABLE `profiles` ADD CONSTRAINT `profiles_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20240608071749_create_chat_rooms_table/migration.sql b/prisma/migrations/20240608071749_create_chat_rooms_table/migration.sql new file mode 100644 index 0000000..f821885 --- /dev/null +++ b/prisma/migrations/20240608071749_create_chat_rooms_table/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE `chat_rooms` ( + `id` VARCHAR(191) NOT NULL, + `type` ENUM('Expert', 'AI') NOT NULL, + `userId` VARCHAR(191) NOT NULL, + `expertId` VARCHAR(191) NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `chat_rooms` ADD CONSTRAINT `chat_rooms_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `chat_rooms` ADD CONSTRAINT `chat_rooms_expertId_fkey` FOREIGN KEY (`expertId`) REFERENCES `users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20240615093224_add_field_on_profile/migration.sql b/prisma/migrations/20240615093224_add_field_on_profile/migration.sql new file mode 100644 index 0000000..3b95f0d --- /dev/null +++ b/prisma/migrations/20240615093224_add_field_on_profile/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE `profiles` ADD COLUMN `incomePerMonth` INTEGER NULL, + ADD COLUMN `insurance` ENUM('Saham', 'Reksadana', 'Obligasi', 'Emas', 'Cryptocurrency') NULL, + ADD COLUMN `investment` ENUM('Saham', 'Reksadana', 'Obligasi', 'Emas', 'Cryptocurrency') NULL, + ADD COLUMN `totalDebt` INTEGER NULL, + ADD COLUMN `totalSaving` INTEGER NULL; diff --git a/prisma/migrations/20240615093721_add_about_field_on_profile/migration.sql b/prisma/migrations/20240615093721_add_about_field_on_profile/migration.sql new file mode 100644 index 0000000..2cd2b6a --- /dev/null +++ b/prisma/migrations/20240615093721_add_about_field_on_profile/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `profiles` ADD COLUMN `about` VARCHAR(191) NULL; diff --git a/prisma/migrations/20240615101729_change_int_to_bigint/migration.sql b/prisma/migrations/20240615101729_change_int_to_bigint/migration.sql new file mode 100644 index 0000000..89b64f5 --- /dev/null +++ b/prisma/migrations/20240615101729_change_int_to_bigint/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE `profiles` MODIFY `incomePerMonth` BIGINT NULL, + MODIFY `totalDebt` BIGINT NULL, + MODIFY `totalSaving` BIGINT NULL; diff --git a/prisma/migrations/20240615103211_change_bigint_to_decimal/migration.sql b/prisma/migrations/20240615103211_change_bigint_to_decimal/migration.sql new file mode 100644 index 0000000..2d2d262 --- /dev/null +++ b/prisma/migrations/20240615103211_change_bigint_to_decimal/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE `profiles` MODIFY `incomePerMonth` DECIMAL(65, 30) NULL, + MODIFY `totalDebt` DECIMAL(65, 30) NULL, + MODIFY `totalSaving` DECIMAL(65, 30) NULL; diff --git a/prisma/migrations/20240615105605_formatting_decimal/migration.sql b/prisma/migrations/20240615105605_formatting_decimal/migration.sql new file mode 100644 index 0000000..b3f6e12 --- /dev/null +++ b/prisma/migrations/20240615105605_formatting_decimal/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - You are about to alter the column `incomePerMonth` on the `profiles` table. The data in that column could be lost. The data in that column will be cast from `Decimal(65,30)` to `Decimal(25,2)`. + - You are about to alter the column `totalDebt` on the `profiles` table. The data in that column could be lost. The data in that column will be cast from `Decimal(65,30)` to `Decimal(25,2)`. + - You are about to alter the column `totalSaving` on the `profiles` table. The data in that column could be lost. The data in that column will be cast from `Decimal(65,30)` to `Decimal(25,2)`. + +*/ +-- AlterTable +ALTER TABLE `profiles` MODIFY `incomePerMonth` DECIMAL(25, 2) NULL, + MODIFY `totalDebt` DECIMAL(25, 2) NULL, + MODIFY `totalSaving` DECIMAL(25, 2) NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 83ea5d7..896b890 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -20,6 +20,7 @@ model User { email String @unique gender UserGender age Int + phoneNumber String password String refreshToken String? @db.VarChar(512) createdAt DateTime @default(now()) @@ -30,6 +31,9 @@ model User { profile Profile? + UserChatRoom ChatRoom[] @relation("UserChatRoom") + ExpertChatRoom ChatRoom[] @relation("ExpertChatRoom") + @@map("users") } @@ -45,20 +49,27 @@ model Role { } model Profile { - id String @id @default(uuid()) - avatar String - maritalStatus ProfileMarital - certifiedStatus String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - work Work @relation(fields: [workId], references: [id]) - workId String - - education Education @relation(fields: [educationId], references: [id]) - educationId String - - user User @relation(fields: [userId], references: [id]) + id String @id @default(uuid()) + avatar String? + about String? + maritalStatus ProfileMarital? + certifiedStatus String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + work Work? @relation(fields: [workId], references: [id]) + workId String? + + education Education? @relation(fields: [educationId], references: [id]) + educationId String? + + investment ProfileInvestmentAndInsurance? + insurance ProfileInvestmentAndInsurance? + incomePerMonth Decimal? @db.Decimal(25, 2) + totalSaving Decimal? @db.Decimal(25, 2) + totalDebt Decimal? @db.Decimal(25, 2) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) userId String @unique @@map("profiles") @@ -86,6 +97,22 @@ model Education { @@map("educations") } +model ChatRoom { + id String @id @default(uuid()) + type ChatRoomType + + user User @relation("UserChatRoom", fields: [userId], references: [id]) + userId String + + expert User? @relation("ExpertChatRoom", fields: [expertId], references: [id]) + expertId String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("chat_rooms") +} + enum UserGender { Laki_laki @map("Laki-laki") Perempuan @@ -96,3 +123,16 @@ enum ProfileMarital { Menikah Cerai } + +enum ProfileInvestmentAndInsurance { + Saham + Reksadana + Obligasi + Emas + Cryptocurrency +} + +enum ChatRoomType { + Expert + AI +} diff --git a/prisma/seed.js b/prisma/seed.js index 91f04d9..89e1ede 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -1,10 +1,16 @@ import prisma from "../db/prisma.js"; +import { seedEducations } from "./seed/educations.seed.js"; import { cleanDatabase } from "./seed/helper/clean.seedHelper.js"; import { seedRoles } from "./seed/roles.seed.js"; +import { seedUsers } from "./seed/users.seed.js"; +import { seedWorks } from "./seed/works.seed.js"; async function main() { await cleanDatabase(); await seedRoles(); + await seedWorks(); + await seedEducations(); + await seedUsers(10); } main() diff --git a/prisma/seed/educations.seed.js b/prisma/seed/educations.seed.js new file mode 100644 index 0000000..585f192 --- /dev/null +++ b/prisma/seed/educations.seed.js @@ -0,0 +1,18 @@ +import prisma from "../../db/prisma.js"; + +const seedEducations = async () => { + await prisma.education.createMany({ + data: [ + { name: "SD" }, + { name: "SMP / Sederajat" }, + { name: "SMA / Sederajat" }, + { name: "Diploma" }, + { name: "S1" }, + { name: "S2" }, + { name: "S3" }, + { name: "Lainnya" }, + ], + }); +}; + +export { seedEducations }; diff --git a/prisma/seed/helper/clean.seedHelper.js b/prisma/seed/helper/clean.seedHelper.js index 42bff46..582e325 100644 --- a/prisma/seed/helper/clean.seedHelper.js +++ b/prisma/seed/helper/clean.seedHelper.js @@ -2,8 +2,14 @@ import prisma from "../../../db/prisma.js"; const cleanDatabase = async () => { console.log(`\nStart cleaning database...`); - await prisma.role.deleteMany(); + console.log(`\nDeleting table users`); + await prisma.user.deleteMany(); console.log(`\nDeleting table roles`); + await prisma.role.deleteMany(); + console.log(`\nDeleting table works`); + await prisma.work.deleteMany(); + console.log(`\nDeleting table educations`); + await prisma.education.deleteMany(); }; export { cleanDatabase }; diff --git a/prisma/seed/users.seed.js b/prisma/seed/users.seed.js new file mode 100644 index 0000000..8d4a566 --- /dev/null +++ b/prisma/seed/users.seed.js @@ -0,0 +1,134 @@ +import prisma from "../../db/prisma.js"; +import { faker } from "@faker-js/faker"; +import bcrypt from "bcrypt"; +import { getPublicUrl } from "../../src/utils/bucket.util.js"; + +const seedUsers = async (count) => { + try { + const roles = await prisma.role.findMany(); + const educations = await prisma.education.findMany(); + const works = await prisma.work.findMany(); + + if (roles.length === 0) { + throw new Error("No roles found in database"); + } + + for (let i = 0; i < count; i++) { + const randomRole = roles[Math.floor(Math.random() * roles.length)]; + const roleId = randomRole.id; + + const salt = await bcrypt.genSalt(); + const hashPassword = await bcrypt.hash("12345", salt); + + const fullName = faker.person.fullName(); + const email = faker.internet.email(); + const gender = faker.helpers.arrayElement([ + "Laki_laki", + "Perempuan", + ]); + const age = faker.number.int({ min: 18, max: 80 }); + const phoneNumber = faker.phone.number(); + const password = hashPassword; + const avatarDefault = + gender === "Laki_laki" + ? getPublicUrl("male.png") + : getPublicUrl("female.png"); + const about = faker.lorem.paragraph(1); + + const maritalStatus = faker.helpers.arrayElement([ + "Lajang", + "Menikah", + "Cerai", + ]); + + const randomEducation = + educations[Math.floor(Math.random() * educations.length)]; + const educationId = randomEducation.id; + + const randomWork = works[Math.floor(Math.random() * works.length)]; + const workId = randomWork.id; + + const investment = faker.helpers.arrayElement([ + "Saham", + "Reksadana", + "Obligasi", + "Emas", + "Cryptocurrency", + ]); + + const insurance = faker.helpers.arrayElement([ + "Saham", + "Reksadana", + "Obligasi", + "Emas", + "Cryptocurrency", + ]); + + const incomePerMonth = faker.number.int({ + min: 0, + max: 25000000, + }); + + const totalSaving = faker.number.int({ + min: 0, + max: 144000000, + }); + + const totalDebt = faker.number.int({ + min: 0, + max: 144000000, + }); + + await prisma.user.create({ + data: { + fullName, + email, + gender, + age, + phoneNumber, + password, + roleId, + profile: { + create: { + avatar: avatarDefault, + about, + maritalStatus, + certifiedStatus: + randomRole.name === "Expert" + ? "Certified Financial Planner (CFP)" + : null, + work: { + connect: { + id: workId, + }, + }, + education: { + connect: { + id: educationId, + }, + }, + investment: + randomRole.name === "User" ? investment : null, + insurance: + randomRole.name === "User" ? insurance : null, + incomePerMonth: + randomRole.name === "User" + ? incomePerMonth + : null, + totalSaving: + randomRole.name === "User" ? totalSaving : null, + totalDebt: + randomRole.name === "User" ? totalDebt : null, + }, + }, + }, + }); + + console.log(`${count} users have been seeded successfully`); + } + } catch (error) { + console.error(`Error seeding users: ${error}`); + } +}; + +export { seedUsers }; diff --git a/prisma/seed/works.seed.js b/prisma/seed/works.seed.js new file mode 100644 index 0000000..4b2de41 --- /dev/null +++ b/prisma/seed/works.seed.js @@ -0,0 +1,22 @@ +import prisma from "../../db/prisma.js"; + +const seedWorks = async () => { + await prisma.work.createMany({ + data: [ + { name: "Pelajar / Mahasiswa" }, + { name: "Ibu Rumah Tangga" }, + { name: "Wirausaha / Wiraswasta" }, + { name: "Pegawai Negeri Sipil" }, + { name: "TNI / Polri" }, + { name: "Pensiunan" }, + { name: "Guru" }, + { name: "Pegawai Swasta" }, + { name: "Pegawai Otoritas / Lembaga / BUMN / BUMD" }, + { name: "Profesional" }, + { name: "Pekerja Seni" }, + { name: "Lainnya" }, + ], + }); +}; + +export { seedWorks }; diff --git a/src/app.js b/src/app.js index 49a9f05..0686bba 100644 --- a/src/app.js +++ b/src/app.js @@ -4,6 +4,7 @@ import cookieParser from "cookie-parser"; import routes from "./routes/api.js"; import swaggerUi from "swagger-ui-express"; import { readFile } from "fs/promises"; +import multer from "multer"; const app = express(); const port = process.env.EXPRESS_PORT || 8080; @@ -21,6 +22,7 @@ app.use( app.use(cookieParser()); app.use(express.json()); +app.use(express.urlencoded({ extended: true })); app.use( `${process.env.API_VERSION}/docs`, @@ -30,6 +32,25 @@ app.use( app.use(`${process.env.API_VERSION}`, routes); +app.use((err, req, res, next) => { + let status; + let message = ""; + if (err instanceof multer.MulterError) { + if (err.code === "LIMIT_FILE_SIZE") { + status = 400; + message = "File too large. Maximum size is 2MB."; + } + } else { + status = err.status || 500; + message = err.message || "Internal Server Error"; + } + console.log(message); + res.status(status).send({ + status: "fail", + message, + }); +}); + app.listen(port, () => { console.log(`Server listening on port: ${port}`); }); diff --git a/src/controller/auths.controller.js b/src/controller/auths.controller.js new file mode 100644 index 0000000..cdb3b6c --- /dev/null +++ b/src/controller/auths.controller.js @@ -0,0 +1,154 @@ +import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; +import { SignInUserSchema, SignUpUserSchema } from "../schema/auths.schema.js"; +import { editRefreshTokenUser, signUpUser } from "../service/auths.service.js"; +import { handleZodError } from "../exceptions/zod.exception.js"; +import { + BadRequestError, + ConflictError, + NotFoundError, + handleBadRequestError, +} from "../exceptions/client.exception.js"; +import { + getUserByEmail, + getUserByRefreshToken, +} from "../service/users.service.js"; +import { handleServerError } from "../exceptions/server.exception.js"; +import { generateJwtToken } from "../utils/jwt.util.js"; +import { updateNullRefreshTokenUser } from "../repository/auths.repository.js"; +import { + MissingRefreshTokenError, + VerifyOwnerTokenError, + handleMissingRefreshTokenError, +} from "../exceptions/auth.exception.js"; + +export const signUpUserHandler = async (req, res) => { + try { + req.body.age = parseInt(req.body.age, 10); + const validateData = SignUpUserSchema.parse(req.body); + + const salt = await bcrypt.genSalt(); + const hashPassword = await bcrypt.hash(validateData.password, salt); + + validateData.password = hashPassword; + + const newUserData = await signUpUser(validateData, res); + + res.status(201).send({ + status: "success", + message: "User signup successfully", + data: { + userId: newUserData.id, + }, + }); + } catch (error) { + try { + console.log(error); + handleZodError(error, res); + } catch (err) { + if (err instanceof ConflictError) { + console.log(err); + return; + } + console.log(err); + handleServerError(err, res); + } + } +}; + +export const signInUserHandler = async (req, res) => { + try { + const validateData = SignInUserSchema.parse(req.body); + + const user = await getUserByEmail(validateData.email, res); + + const match = await bcrypt.compare( + validateData.password, + user.password + ); + + if (!match) { + handleBadRequestError("Wrong password", res); + } + + const payloadJwt = { + id: user.id, + fullName: user.fullName, + email: user.email, + role: user.role.name, + }; + + const accessToken = generateJwtToken( + payloadJwt, + process.env.JWT_ACCESS_TOKEN_SECRET, + { expiresIn: "1h" } + ); + + const refreshToken = generateJwtToken( + payloadJwt, + process.env.JWT_REFRESH_TOKEN_SECRET, + { expiresIn: "30d" } + ); + + await editRefreshTokenUser(user.id, refreshToken, res); + + res.cookie("refreshToken", refreshToken, { + httpOnly: true, + maxAge: 30 * 24 * 60 * 60 * 1000, // 30 day in miliseconds + secure: process.env.NODE_ENV === "production", // this is for HTTPS + }); + + res.status(200).send({ + status: "success", + message: "Login successfully", + data: { + accessToken, + }, + }); + } catch (error) { + try { + console.log(error); + handleZodError(error, res); + } catch (err) { + if ( + err instanceof BadRequestError || + err instanceof NotFoundError + ) { + console.log(err); + return; + } + console.log(err); + handleServerError(err, res); + } + } +}; + +export const signOutUserHandler = async (req, res) => { + try { + const refreshToken = req.cookies.refreshToken; + + if (!refreshToken) { + handleMissingRefreshTokenError(undefined, res); + } + + const user = await getUserByRefreshToken(refreshToken, res); + + await updateNullRefreshTokenUser(user.id); + res.clearCookie("refreshToken"); + + res.status(200).send({ + status: "success", + message: "Logout successfully", + }); + } catch (error) { + if ( + error instanceof MissingRefreshTokenError || + error instanceof VerifyOwnerTokenError + ) { + console.log(error); + return; + } + console.log(error); + handleServerError(error, res); + } +}; diff --git a/src/controller/chat-ai.controller.js b/src/controller/chat-ai.controller.js new file mode 100644 index 0000000..0d7b2e6 --- /dev/null +++ b/src/controller/chat-ai.controller.js @@ -0,0 +1,44 @@ +import { handleServerError } from "../exceptions/server.exception.js"; +import { handleZodError } from "../exceptions/zod.exception.js"; +import { GenerativeAiInput, SugesstionQuestionInput } from "../schema/chat-ai.schema.js"; +import { getGenerativeAi, getSugesstionQuestion } from "../service/chat-ai.service.js"; + +export const getGenerativeAiHandler = async (req, res) => { + try { + const validateData = GenerativeAiInput.parse(req.body); + + const data = await getGenerativeAi(validateData); + + res.status(200).send({ + status: "success", + message: "Successfully get generative AI", + data, + }); + } catch (error) { + try { + handleZodError(error, res); + } catch (err) { + handleServerError(err, res); + } + } +}; + +export const getSugesstionQuestionHandler = async (req, res) => { + try { + const validateData = SugesstionQuestionInput.parse(req.body); + + const data = await getSugesstionQuestion(validateData, req.email); + + res.status(200).send({ + status: "success", + message: "Successfully get sugesstion question", + data, + }); + } catch (error) { + try { + handleZodError(error, res); + } catch (err) { + handleServerError(err, res); + } + } +} \ No newline at end of file diff --git a/src/controller/chat-rooms.controller.js b/src/controller/chat-rooms.controller.js new file mode 100644 index 0000000..7feb0ca --- /dev/null +++ b/src/controller/chat-rooms.controller.js @@ -0,0 +1,131 @@ +import { handleServerError } from "../exceptions/server.exception.js"; +import { handleZodError } from "../exceptions/zod.exception.js"; +import { NotFoundError } from "../exceptions/client.exception.js"; +import { + editChatRoomById, + getAllChatRooms, + getAllChatRoomsByUserId, + getChatRoomById, + insertChatRoom, + removeChatRoomById, +} from "../service/chat-rooms.service.js"; +import { + EditChatRoomSchema, + InsertChatRoomSchema, +} from "../schema/chat-rooms.schema.js"; + +export const getAllChatRoomsHandler = async (req, res) => { + try { + const userId = req.query.userId; + + let chatRooms; + if (userId) { + chatRooms = await getAllChatRoomsByUserId(userId); + } else { + chatRooms = await getAllChatRooms(); + } + + res.status(200).send({ + status: "success", + message: "Get all chat room profile user", + data: { + chatRooms, + }, + }); + } catch (error) { + console.log(error); + handleServerError(error, res); + } +}; + +export const insertChatRoomHandler = async (req, res) => { + try { + const validateData = InsertChatRoomSchema.parse(req.body); + + const newChatRoomData = await insertChatRoom(validateData); + + res.status(201).send({ + status: "success", + message: "Chat room created successfully", + data: { + id: newChatRoomData.id, + }, + }); + } catch (error) { + try { + handleZodError(error, res); + } catch (err) { + handleServerError(err, res); + } + } +}; + +export const getChatRoomByIdHandler = async (req, res) => { + try { + const chatRoomId = req.params.chatRoomId; + const chatRoom = await getChatRoomById(chatRoomId, res); + + res.status(200).send({ + status: "success", + message: "Get chat room data by id", + data: { + chatRoom, + }, + }); + } catch (error) { + if (error instanceof NotFoundError) { + return; + } + handleServerError(error, res); + } +}; + +export const editChatRoomByIdHandler = async (req, res) => { + try { + const chatRoomId = req.params.chatRoomId; + const validateData = EditChatRoomSchema.parse(req.body); + + const newChatRoomData = await editChatRoomById( + chatRoomId, + validateData, + res + ); + + res.status(200).send({ + status: "success", + message: "Chat room updated successfully", + data: { + id: newChatRoomData.id, + }, + }); + } catch (error) { + try { + handleZodError(error, res); + } catch (err) { + if (err instanceof NotFoundError) { + return; + } + handleServerError(err, res); + } + } +}; + +export const removeChatRoomByIdHandler = async (req, res) => { + try { + const chatRoomId = req.params.chatRoomId; + await removeChatRoomById(chatRoomId, res); + + res.status(200).send({ + status: "success", + message: "Chat room deleted successfully", + data: { + id: chatRoomId, + }, + }); + } catch (error) { + if (error instanceof NotFoundError) { + return; + } + handleServerError(error, res); + } +}; \ No newline at end of file diff --git a/src/controller/educations.controller.js b/src/controller/educations.controller.js new file mode 100644 index 0000000..0566bf2 --- /dev/null +++ b/src/controller/educations.controller.js @@ -0,0 +1,131 @@ +import { handleServerError } from "../exceptions/server.exception.js"; +import { handleZodError } from "../exceptions/zod.exception.js"; +import { NotFoundError } from "../exceptions/client.exception.js"; +import { + editEducationById, + getAllEducations, + getAllEducationsByName, + getEducationById, + insertEducation, + removeEducationById, +} from "../service/educations.service.js"; +import { + EditEducationSchema, + InsertEducationSchema, +} from "../schema/educations.schema.js"; + +export const getAllEducationsHandler = async (req, res) => { + try { + const name = req.query.name; + + let educations; + if (name) { + educations = await getAllEducationsByName(name); + } else { + educations = await getAllEducations(); + } + + res.status(200).send({ + status: "success", + message: "Get all education profile user", + data: { + educations, + }, + }); + } catch (error) { + console.log(error); + handleServerError(error, res); + } +}; + +export const insertEducationHandler = async (req, res) => { + try { + const validateData = InsertEducationSchema.parse(req.body); + + const newEducationData = await insertEducation(validateData); + + res.status(201).send({ + status: "success", + message: "Education created successfully", + data: { + educationId: newEducationData.id, + }, + }); + } catch (error) { + try { + handleZodError(error, res); + } catch (err) { + handleServerError(err, res); + } + } +}; + +export const getEducationByIdHandler = async (req, res) => { + try { + const educationId = req.params.educationId; + const education = await getEducationById(educationId, res); + + res.status(200).send({ + status: "success", + message: "Get education data by id", + data: { + education, + }, + }); + } catch (error) { + if (error instanceof NotFoundError) { + return; + } + handleServerError(error, res); + } +}; + +export const editEducationByIdHandler = async (req, res) => { + try { + const educationId = req.params.educationId; + const validateData = EditEducationSchema.parse(req.body); + + const newEducationData = await editEducationById( + educationId, + validateData, + res + ); + + res.status(200).send({ + status: "success", + message: "Education updated successfully", + data: { + educationId: newEducationData.id, + }, + }); + } catch (error) { + try { + handleZodError(error, res); + } catch (err) { + if (err instanceof NotFoundError) { + return; + } + handleServerError(err, res); + } + } +}; + +export const removeEducationByIdHandler = async (req, res) => { + try { + const educationId = req.params.educationId; + await removeEducationById(educationId, res); + + res.status(200).send({ + status: "success", + message: "Education deleted successfully", + data: { + educationId: educationId, + }, + }); + } catch (error) { + if (error instanceof NotFoundError) { + return; + } + handleServerError(error, res); + } +}; diff --git a/src/controller/roles.controller.js b/src/controller/roles.controller.js index 4ef94f4..06dfba7 100644 --- a/src/controller/roles.controller.js +++ b/src/controller/roles.controller.js @@ -1,15 +1,26 @@ import { handleServerError } from "../exceptions/server.exception.js"; import { handleZodError } from "../exceptions/zod.exception.js"; -import { getAllRoles, insertRole } from "../service/roles.service.js"; -import { z, string, object } from "zod"; - -const roleSchema = object({ - name: string(), -}); +import { NotFoundError } from "../exceptions/client.exception.js"; +import { + editRoleById, + getAllRoles, + getAllRolesByName, + getRoleById, + insertRole, + removeRoleById, +} from "../service/roles.service.js"; +import { EditRoleSchema, InsertRoleSchema } from "../schema/roles.schema.js"; export const getAllRolesHandler = async (req, res) => { try { - const roles = await getAllRoles(); + const name = req.query.name; + + let roles; + if (name) { + roles = await getAllRolesByName(name); + } else { + roles = await getAllRoles(); + } res.status(200).send({ status: "success", @@ -19,13 +30,14 @@ export const getAllRolesHandler = async (req, res) => { }, }); } catch (error) { + console.log(error); handleServerError(error, res); } }; export const insertRoleHandler = async (req, res) => { try { - const validateData = roleSchema.parse(req.body); + const validateData = InsertRoleSchema.parse(req.body); const newRoleData = await insertRole(validateData); @@ -33,14 +45,88 @@ export const insertRoleHandler = async (req, res) => { status: "success", message: "Role created successfully", data: { - id: newRoleData.id, + roleId: newRoleData.id, + }, + }); + } catch (error) { + try { + console.log(error); + handleZodError(error, res); + } catch (err) { + console.log(err); + handleServerError(err, res); + } + } +}; + +export const getRoleByIdHandler = async (req, res) => { + try { + const roleId = req.params.roleId; + const role = await getRoleById(roleId, res); + + res.status(200).send({ + status: "success", + message: "Get role data by id", + data: { + role, + }, + }); + } catch (error) { + if (error instanceof NotFoundError) { + console.log(error); + return; + } + console.log(error); + handleServerError(error, res); + } +}; + +export const editRoleByIdHandler = async (req, res) => { + try { + const roleId = req.params.roleId; + const validateData = EditRoleSchema.parse(req.body); + + const newRoleData = await editRoleById(roleId, validateData, res); + + res.status(200).send({ + status: "success", + message: "Role updated successfully", + data: { + roleId: newRoleData.id, }, }); } catch (error) { try { + console.log(error); handleZodError(error, res); } catch (err) { + if (err instanceof NotFoundError) { + console.log(err); + return; + } handleServerError(err, res); } } }; + +export const removeRoleByIdHandler = async (req, res) => { + try { + const roleId = req.params.roleId; + await removeRoleById(roleId, res); + + res.status(200).send({ + status: "success", + message: "Role deleted successfully", + data: { + roleId: roleId, + }, + }); + } catch (error) { + if (error instanceof NotFoundError) { + console.log(error); + return; + } + console.log(error); + handleServerError(error, res); + } +}; diff --git a/src/controller/tokens.controller.js b/src/controller/tokens.controller.js new file mode 100644 index 0000000..090b088 --- /dev/null +++ b/src/controller/tokens.controller.js @@ -0,0 +1,66 @@ +import jwt from "jsonwebtoken"; +import { + MissingRefreshTokenError, + VerifyOwnerTokenError, + VerifyTokenError, + handleMissingRefreshTokenError, + handleVerifyTokenError, +} from "../exceptions/auth.exception.js"; +import { getUserByRefreshToken } from "../service/users.service.js"; +import { generateJwtToken } from "../utils/jwt.util.js"; +import { handleServerError } from "../exceptions/server.exception.js"; + +export const refreshTokenHandler = async (req, res) => { + try { + const refreshToken = req.cookies.refreshToken; + + if (!refreshToken) { + handleMissingRefreshTokenError(undefined, res); + } + + const user = await getUserByRefreshToken(refreshToken, res); + + jwt.verify( + refreshToken, + process.env.JWT_REFRESH_TOKEN_SECRET, + (err, decoded) => { + if (err) { + console.log(err); + handleVerifyTokenError(undefined, res); + } + + const payloadJwt = { + id: user.id, + fullName: user.fullName, + email: user.email, + role: user.role.name, + }; + + const accessToken = generateJwtToken( + payloadJwt, + process.env.JWT_ACCESS_TOKEN_SECRET, + { expiresIn: "1h" } + ); + + res.status(200).send({ + status: "success", + message: "Refresh token successfully", + data: { + accessToken, + }, + }); + } + ); + } catch (error) { + if ( + error instanceof MissingRefreshTokenError || + error instanceof VerifyOwnerTokenError || + error instanceof VerifyTokenError + ) { + console.log(error); + return; + } + console.log(error); + handleServerError(error, res); + } +}; diff --git a/src/controller/users.controller.js b/src/controller/users.controller.js new file mode 100644 index 0000000..47fc5ea --- /dev/null +++ b/src/controller/users.controller.js @@ -0,0 +1,337 @@ +import bcrypt from "bcrypt"; +import { NotFoundError } from "../exceptions/client.exception.js"; +import { handleServerError } from "../exceptions/server.exception.js"; +import { handleZodError } from "../exceptions/zod.exception.js"; +import { + UpdateAllFieldUserSchema, + UpdatePartialFieldUserProfileSchema, + UpdatePartialFieldUserSchema, +} from "../schema/users.schema.js"; +import { + editAvatarUser, + editUserById, + editUserProfileByUserId, + getAllUsers, + getUserById, + getUserProfileByUserId, + removeUserById, + removeUserProfileByUserId, +} from "../service/users.service.js"; +import { findUserProfileByUserId } from "../repository/users.repository.js"; +import { + deleteFileFromBucket, + getFileNameFromUrl, + getPublicUrl, +} from "../utils/bucket.util.js"; +import { Decimal } from "@prisma/client/runtime/library"; + +export const getAllUsersHandler = async (req, res) => { + try { + const { role, fullName } = req.query; + + const filters = { + role, + fullName, + }; + + const users = await getAllUsers(filters); + + res.status(200).send({ + status: "success", + message: "Get all user data", + data: { + users, + }, + }); + } catch (error) { + console.log(error); + handleServerError(error, res); + } +}; + +export const getUserByIdHandler = async (req, res) => { + try { + const userId = req.params.userId; + + const user = await getUserById(userId, res); + + res.status(200).send({ + status: "success", + message: "Get user data by id", + data: { + user, + }, + }); + } catch (error) { + if (error instanceof NotFoundError) { + console.log(error); + return; + } + console.log(error); + handleServerError(error, res); + } +}; + +export const editUserAllFieldByIdHandler = async (req, res) => { + try { + const userId = req.params.userId; + if (req.body.age) { + req.body.age = parseInt(req.body.age, 10); + } + + const validateData = UpdateAllFieldUserSchema.parse(req.body); + + const user = await editUserById(userId, validateData, res); + + res.status(200).send({ + status: "success", + message: "User data updated successfully", + data: { + userId: user.id, + }, + }); + } catch (error) { + try { + console.log(error); + handleZodError(error, res); + } catch (err) { + if (err instanceof NotFoundError) { + console.log(err); + return; + } + console.log(err); + handleServerError(err, res); + } + } +}; + +export const editUserPartialFieldByIdHandler = async (req, res) => { + try { + const userId = req.params.userId; + if (req.body.age) { + req.body.age = parseInt(req.body.age, 10); + } + + let validateData = UpdatePartialFieldUserSchema.parse(req.body); + + if (validateData.password) { + const salt = await bcrypt.genSalt(); + validateData.password = await bcrypt.hash( + validateData.password, + salt + ); + } + + const user = await editUserById(userId, validateData, res); + + res.status(200).send({ + status: "success", + message: "User data partially updated successfully", + data: { + userId: user.id, + }, + }); + } catch (error) { + try { + console.log(error); + handleZodError(error, res); + } catch (err) { + if (err instanceof NotFoundError) { + console.log(err); + return; + } + console.log(err); + handleServerError(err, res); + } + } +}; + +export const editUserProfilePartialFieldByUserIdHandler = async (req, res) => { + try { + const userId = req.params.userId; + + const validateData = UpdatePartialFieldUserProfileSchema.parse( + req.body + ); + + if (req.body.incomePerMonth) { + req.body.incomePerMonth = new Decimal(req.body.incomePerMonth); + } + + if (req.body.totalSaving) { + req.body.totalSaving = new Decimal(req.body.totalSaving); + } + + if (req.body.totalDebt) { + req.body.totalDebt = new Decimal(req.body.totalDebt); + } + + const user = await editUserProfileByUserId(userId, validateData, res); + + res.status(200).send({ + status: "success", + message: "User profile data updated successfully", + data: { + userId: user.id, + }, + }); + } catch (error) { + try { + console.log(error); + handleZodError(error, res); + } catch (err) { + if (err instanceof NotFoundError) { + console.log(err); + return; + } + console.log(err); + handleServerError(err, res); + } + } +}; + +export const removeUserProfileByUserIdHandler = async (req, res) => { + try { + const userId = req.params.userId; + + const userProfile = await getUserProfileByUserId(userId, res); + const userAvatar = userProfile?.avatar; + const user = await getUserById(userId, res); + const userGender = user.gender === "Laki_laki" ? "male" : "female"; + const userGenderAvatarUrl = getPublicUrl(`${userGender}.png`); + + const fileName = getFileNameFromUrl(userAvatar); + const maleAvatarUrl = getPublicUrl("male.png"); + const femaleAvatarUrl = getPublicUrl("female.png"); + + if ( + userAvatar && + userAvatar !== maleAvatarUrl && + userAvatar !== femaleAvatarUrl + ) { + await deleteFileFromBucket(fileName); + } + + await removeUserProfileByUserId(userId, res); + await editAvatarUser(userId, userGenderAvatarUrl); + + res.status(200).send({ + status: "success", + message: "Successfully delete user profile data", + data: { + userId: userId, + }, + }); + } catch (error) { + if (error instanceof NotFoundError) { + console.log(error); + return; + } + console.log(error); + handleServerError(error, res); + } +}; + +export const removeUserByIdHandler = async (req, res) => { + try { + const userId = req.params.userId; + + const userProfile = await getUserProfileByUserId(userId, res); + const userAvatar = userProfile?.avatar; + + const fileName = getFileNameFromUrl(userAvatar); + const maleAvatarUrl = getPublicUrl("male.png"); + const femaleAvatarUrl = getPublicUrl("female.png"); + + if ( + userAvatar && + userAvatar !== maleAvatarUrl && + userAvatar !== femaleAvatarUrl + ) { + await deleteFileFromBucket(fileName); + } + + await removeUserById(userId, res); + + res.status(200).send({ + status: "success", + message: "Successfully delete user data", + data: { + userId: userId, + }, + }); + } catch (error) { + if (error instanceof NotFoundError) { + console.log(error); + return; + } + console.log(error); + handleServerError(error, res); + } +}; + +export const getUserProfileByUserIdHandler = async (req, res) => { + try { + const userId = req.params.userId; + + const profile = await getUserProfileByUserId(userId, res); + + res.status(200).send({ + status: "success", + message: "Get user data by id", + data: { + profile, + }, + }); + } catch (error) { + if (error instanceof NotFoundError) { + console.log(error); + return; + } + console.log(error); + handleServerError(error, res); + } +}; + +export const editAvatarUserHandler = async (req, res) => { + try { + const userId = req.params.userId; + let imageUrl = ""; + + if (req.file && req.file.cloudStoragePublicUrl) { + imageUrl = req.file.cloudStoragePublicUrl; + } + + const userProfile = await getUserProfileByUserId(userId, res); + const oldAvatar = userProfile?.avatar; + + const fileName = getFileNameFromUrl(oldAvatar); + const maleAvatarUrl = getPublicUrl("male.png"); + const femaleAvatarUrl = getPublicUrl("female.png"); + + if ( + oldAvatar && + oldAvatar !== maleAvatarUrl && + oldAvatar !== femaleAvatarUrl + ) { + await deleteFileFromBucket(fileName); + } + + const newAvatarUserData = await editAvatarUser(userId, imageUrl, res); + + res.status(200).send({ + status: "success", + message: "Avatar user updated successfully", + data: { + userId: newAvatarUserData.userId, + }, + }); + } catch (error) { + if (error instanceof NotFoundError) { + console.log(error); + return; + } + console.log(error); + handleServerError(error, res); + } +}; diff --git a/src/controller/works.controller.js b/src/controller/works.controller.js new file mode 100644 index 0000000..582991a --- /dev/null +++ b/src/controller/works.controller.js @@ -0,0 +1,134 @@ +import { handleServerError } from "../exceptions/server.exception.js"; +import { handleZodError } from "../exceptions/zod.exception.js"; +import { NotFoundError } from "../exceptions/client.exception.js"; +import { + editWorkById, + getAllWorks, + getAllWorksByName, + getWorkById, + insertWork, + removeWorkById, +} from "../service/works.service.js"; +import { EditWorkSchema, InsertWorkSchema } from "../schema/works.schema.js"; + +export const getAllWorksHandler = async (req, res) => { + try { + const name = req.query.name; + + let works; + if (name) { + works = await getAllWorksByName(name); + } else { + works = await getAllWorks(); + } + + res.status(200).send({ + status: "success", + message: "Get all work profile user", + data: { + works, + }, + }); + } catch (error) { + console.log(error); + handleServerError(error, res); + } +}; + +export const insertWorkHandler = async (req, res) => { + try { + const validateData = InsertWorkSchema.parse(req.body); + + const newWorkData = await insertWork(validateData); + + res.status(201).send({ + status: "success", + message: "Work created successfully", + data: { + workId: newWorkData.id, + }, + }); + } catch (error) { + try { + console.log(error); + handleZodError(error, res); + } catch (err) { + console.log(err); + handleServerError(err, res); + } + } +}; + +export const getWorkByIdHandler = async (req, res) => { + try { + const workId = req.params.workId; + const work = await getWorkById(workId, res); + + res.status(200).send({ + status: "success", + message: "Get work data by id", + data: { + work, + }, + }); + } catch (error) { + if (error instanceof NotFoundError) { + console.log(error); + return; + } + console.log(error); + handleServerError(error, res); + } +}; + +export const editWorkByIdHandler = async (req, res) => { + try { + const workId = req.params.workId; + const validateData = EditWorkSchema.parse(req.body); + + const newWorkData = await editWorkById(workId, validateData, res); + + res.status(200).send({ + status: "success", + message: "Work updated successfully", + data: { + workId: newWorkData.id, + }, + }); + } catch (error) { + try { + console.log(error); + handleZodError(error, res); + } catch (err) { + if (err instanceof NotFoundError) { + console.log(err); + return; + } + console.log(err); + handleServerError(err, res); + } + } +}; + +export const removeWorkByIdHandler = async (req, res) => { + try { + const workId = req.params.workId; + await removeWorkById(workId, res); + + res.status(200).send({ + status: "success", + message: "Work deleted successfully", + data: { + workId: workId, + }, + }); + } catch (error) { + console.log(error); + if (error instanceof NotFoundError) { + console.log(error); + return; + } + console.log(error); + handleServerError(error, res); + } +}; diff --git a/src/exceptions/auth.exception.js b/src/exceptions/auth.exception.js index ef704d8..f4ae041 100644 --- a/src/exceptions/auth.exception.js +++ b/src/exceptions/auth.exception.js @@ -1,24 +1,75 @@ -export const handleAuthError = (error, res) => { - if (error.name === "VerifyTokenError") { - return res.status(403).send({ - status: "fail", - message: - "Forbidden. Can't decode or compare token with secret (expired or invalid token)", - }); +export const handleVerifyTokenError = ( + message = "Forbidden. Can't decode or compare token with secret (expired or invalid token)", + res +) => { + res.status(403).send({ + status: "fail", + message: message, + }); + + throw new VerifyTokenError(message); +}; + +export const handleMissingAccessTokenError = ( + message = "Unauthorized. Please provide Bearer `accessToken` on Authorization header", + res +) => { + res.status(401).send({ + status: "fail", + message: message, + }); + + throw new MissingAccessTokenError(message); +}; + +export const handleMissingRefreshTokenError = ( + message = "Unauthorized. Please provide `refreshToken` on cookies before request", + res +) => { + res.status(401).send({ + status: "fail", + message: message, + }); + + throw new MissingRefreshTokenError(message); +}; + +export const handleVerifyOwnerTokenError = ( + message = "Forbidden. Can't find who the owner `refreshToken`", + res +) => { + res.status(403).send({ + status: "fail", + message: message, + }); + + throw new VerifyOwnerTokenError(message); +}; + +export class VerifyTokenError extends Error { + constructor(message) { + super(message); + this.name = "VerifyTokenError"; } +} - if (error.name === "MissingTokenError") { - return res.status(401).send({ - status: "fail", - message: - "Unauthorized. Please provide Bearer `accessToken` on Authorization header", - }); +export class MissingAccessTokenError extends Error { + constructor(message) { + super(message); + this.name = "MissingAccessTokenError"; } +} - if (error.name === "VerifyOwnerTokenError") { - return res.status(403).send({ - status: "fail", - message: "Fobidden. Can't find who the owner `refreshToken`", - }); +export class MissingRefreshTokenError extends Error { + constructor(message) { + super(message); + this.name = "MissingRefreshTokenError"; } -}; +} + +export class VerifyOwnerTokenError extends Error { + constructor(message) { + super(message); + this.name = "VerifyOwnerTokenError"; + } +} diff --git a/src/exceptions/client.exception.js b/src/exceptions/client.exception.js index 0b19f45..982a9f9 100644 --- a/src/exceptions/client.exception.js +++ b/src/exceptions/client.exception.js @@ -1,6 +1,50 @@ -export const handleNotFoundError = (res, message = "Resource not found") => { +export const handleNotFoundError = (message = "Resource not found", res) => { res.status(404).send({ status: "fail", message: message, }); + + throw new NotFoundError(message); }; + +export const handleConflictError = (message = "Resource are conflict", res) => { + res.status(409).send({ + status: "fail", + message: message, + }); + + throw new ConflictError(message); +}; + +export const handleBadRequestError = ( + message = "Resource are bad request", + res +) => { + res.status(400).send({ + status: "fail", + message: message, + }); + + throw new BadRequestError(message); +}; + +export class NotFoundError extends Error { + constructor(message) { + super(message); + this.name = "NotFoundError"; + } +} + +export class ConflictError extends Error { + constructor(message) { + super(message); + this.name = "ConflictError"; + } +} + +export class BadRequestError extends Error { + constructor(message) { + super(message); + this.name = "BadRequestError"; + } +} diff --git a/src/middlewares/images.middleware.js b/src/middlewares/images.middleware.js new file mode 100644 index 0000000..bbb0449 --- /dev/null +++ b/src/middlewares/images.middleware.js @@ -0,0 +1,80 @@ +import multer from "multer"; +import dateFormat from "dateformat"; +import { Storage } from "@google-cloud/storage"; +import path from "path"; +import { getPublicUrl } from "../utils/bucket.util.js"; +import { getUserById } from "../service/users.service.js"; +import { NotFoundError } from "../exceptions/client.exception.js"; + +const serviceAccountPathKey = path.resolve("./serviceaccount.json"); + +const gcs = new Storage({ + projectId: process.env.GCLOUD_PROJECT_ID, +}); + +const bucketName = process.env.GCLOUD_BUCKET_NAME; +const bucket = gcs.bucket(bucketName); + +const multerStorage = multer.memoryStorage(); + +// Validasi file extensions +const fileFilter = (req, file, cb) => { + const allowedExtensions = ["image/jpeg", "image/png"]; + if (!allowedExtensions.includes(file.mimetype)) { + const error = new Error( + "Invalid file type. Only JPG, PNG are allowed!" + ); + error.status = 400; + return cb(error); + } + cb(null, true); +}; + +export const uploadAvatar = multer({ + storage: multerStorage, + limits: { + fileSize: 2 * 1024 * 1024, + }, + fileFilter, +}); + +export const uploadToGcs = async (req, res, next) => { + try { + if (!req.file) { + return next(); + } + + await getUserById(req.params.userId, res); + + const extension = req.file.mimetype.split("/")[1]; + const fileName = `${req.params.userId}-${dateFormat( + new Date(), + "yyyymmdd-HHMMss" + )}.${extension}`; + const file = bucket.file(fileName); + + const stream = file.createWriteStream({ + metadata: { + contentType: req.file.mimetype, + }, + }); + + stream.on("error", (err) => { + req.file.cloudStorageError = err; + next(err); + }); + + stream.on("finish", () => { + req.file.cloudStorageObject = fileName; + req.file.cloudStoragePublicUrl = getPublicUrl(fileName); + next(); + }); + + stream.end(req.file.buffer); + } catch (error) { + if (error instanceof NotFoundError) { + console.log(error); + return; + } + } +}; diff --git a/src/middlewares/tokens.middleware.js b/src/middlewares/tokens.middleware.js new file mode 100644 index 0000000..96df6cd --- /dev/null +++ b/src/middlewares/tokens.middleware.js @@ -0,0 +1,42 @@ +import jwt from "jsonwebtoken"; +import { + MissingAccessTokenError, + VerifyTokenError, + handleMissingAccessTokenError, + handleVerifyTokenError, +} from "../exceptions/auth.exception.js"; +import { handleServerError } from "../exceptions/server.exception.js"; + +export const verifyAccessTokenHandler = (req, res, next) => { + try { + const authHeader = req.headers["authorization"]; + const accessToken = authHeader && authHeader.split(" ")[1]; // empty token && take token + + if (accessToken == null) { + handleMissingAccessTokenError(undefined, res); + } + + jwt.verify( + accessToken, + process.env.JWT_ACCESS_TOKEN_SECRET, + (err, decoded) => { + if (err) { + handleVerifyTokenError(undefined, res); + } + + req.email = decoded.email; + next(); + } + ); + } catch (error) { + if ( + error instanceof MissingAccessTokenError || + error instanceof VerifyTokenError + ) { + console.log(error); + return; + } + console.log(error); + handleServerError(error, res); + } +}; diff --git a/src/repository/auths.repository.js b/src/repository/auths.repository.js new file mode 100644 index 0000000..f33e271 --- /dev/null +++ b/src/repository/auths.repository.js @@ -0,0 +1,42 @@ +import prisma from "../../db/prisma.js"; + +export const createUser = async (newUserData) => { + const newUser = await prisma.user.create({ + data: { + ...newUserData, + profile: { + create: { + avatar: newUserData.profile.create.avatar, + }, + }, + }, + }); + + return newUser; +}; + +export const updateRefreshTokenUser = async (userId, newRefreshToken) => { + const user = await prisma.user.update({ + where: { + id: userId, + }, + data: { + refreshToken: newRefreshToken, + }, + }); + + return user; +}; + +export const updateNullRefreshTokenUser = async (userId) => { + const user = await prisma.user.update({ + where: { + id: userId, + }, + data: { + refreshToken: null, + }, + }); + + return user; +}; diff --git a/src/repository/chat-rooms.repository.js b/src/repository/chat-rooms.repository.js new file mode 100644 index 0000000..0228dd8 --- /dev/null +++ b/src/repository/chat-rooms.repository.js @@ -0,0 +1,136 @@ +import prisma from "../../db/prisma.js"; + +export const findChatRooms = async () => { + const chatRooms = await prisma.chatRoom.findMany({ + select: { + id: true, + type: true, + userId: true, + user: { + select: { + id: true, + fullName: true, + email: true, + createdAt: true, + updatedAt: true, + }, + }, + expertId: true, + expert: { + select: { + id: true, + fullName: true, + email: true, + createdAt: true, + updatedAt: true, + }, + }, + createdAt: true, + updatedAt: true, + } + }); + + return chatRooms; +}; + +export const findChatRoomsByUserId = async (userId) => { + const chatRooms = await prisma.chatRoom.findMany({ + where: { + userId, + }, + select: { + id: true, + type: true, + userId: true, + user: { + select: { + id: true, + fullName: true, + email: true, + createdAt: true, + updatedAt: true, + }, + }, + expertId: true, + expert: { + select: { + id: true, + fullName: true, + email: true, + createdAt: true, + updatedAt: true, + }, + }, + createdAt: true, + updatedAt: true, + }, + }); + + return chatRooms; +}; + +export const createChatRoom = async (newChatRoomData) => { + const newChatRoom = await prisma.chatRoom.create({ + data: newChatRoomData, + }); + + return newChatRoom; +}; + +export const findChatRoomById = async (chatRoomId) => { + const chatRoom = await prisma.chatRoom.findUnique({ + where: { + id: chatRoomId, + }, + select: { + id: true, + type: true, + userId: true, + user: { + select: { + id: true, + fullName: true, + email: true, + createdAt: true, + updatedAt: true, + }, + }, + expertId: true, + expert: { + select: { + id: true, + fullName: true, + email: true, + createdAt: true, + updatedAt: true, + }, + }, + createdAt: true, + updatedAt: true, + }, + }); + + return chatRoom; +}; + +export const updateChatRoomById = async (chatRoomId, newChatRoomData) => { + console.log(chatRoomId, newChatRoomData); + const chatRoom = await prisma.chatRoom.update({ + where: { + id: chatRoomId, + }, + data: newChatRoomData, + }); + + return chatRoom; +}; + +export const deleteChatRoomById = async (chatRoomId) => { + const chatRoom = await prisma.chatRoom.delete({ + where: { + id: chatRoomId, + }, + }); + + return chatRoom; +}; \ No newline at end of file diff --git a/src/repository/educations.repository.js b/src/repository/educations.repository.js new file mode 100644 index 0000000..65aa53b --- /dev/null +++ b/src/repository/educations.repository.js @@ -0,0 +1,69 @@ +import prisma from "../../db/prisma.js"; + +export const findEducations = async () => { + const educations = await prisma.education.findMany({ + select: { + id: true, + name: true, + createdAt: true, + updatedAt: true, + }, + }); + + return educations; +}; + +export const findEducationsByName = async (name) => { + const educations = await prisma.education.findMany({ + where: { + name, + }, + }); + + return educations; +}; + +export const createEducation = async (newEducationData) => { + const newEducation = await prisma.education.create({ + data: newEducationData, + }); + + return newEducation; +}; + +export const findEducationById = async (educationId) => { + const education = await prisma.education.findUnique({ + where: { + id: educationId, + }, + select: { + id: true, + name: true, + createdAt: true, + updatedAt: true, + }, + }); + + return education; +}; + +export const updateEducationById = async (educationId, newEducationData) => { + const education = await prisma.education.update({ + where: { + id: educationId, + }, + data: newEducationData, + }); + + return education; +}; + +export const deleteEducationById = async (educationId) => { + const education = await prisma.education.delete({ + where: { + id: educationId, + }, + }); + + return education; +}; diff --git a/src/repository/roles.repository.js b/src/repository/roles.repository.js index a29371a..0af7821 100644 --- a/src/repository/roles.repository.js +++ b/src/repository/roles.repository.js @@ -13,12 +13,57 @@ export const findRoles = async () => { return roles; }; +export const findRolesByName = async (name) => { + const roles = await prisma.role.findMany({ + where: { + name, + }, + }); + + return roles; +}; + export const createRole = async (newRoledata) => { const newRole = await prisma.role.create({ - data: { - nama: newRoledata, - }, + data: newRoledata, }); return newRole; }; + +export const findRoleById = async (roleId) => { + const role = await prisma.role.findUnique({ + where: { + id: roleId, + }, + select: { + id: true, + name: true, + createdAt: true, + updatedAt: true, + }, + }); + + return role; +}; + +export const updateRole = async (roleId, newRoleData) => { + const role = await prisma.role.update({ + where: { + id: roleId, + }, + data: newRoleData, + }); + + return role; +}; + +export const deleteRole = async (roleId) => { + const role = await prisma.role.delete({ + where: { + id: roleId, + }, + }); + + return role; +}; diff --git a/src/repository/users.repository.js b/src/repository/users.repository.js new file mode 100644 index 0000000..ea32469 --- /dev/null +++ b/src/repository/users.repository.js @@ -0,0 +1,364 @@ +import prisma from "../../db/prisma.js"; + +export const findUsers = async (filters) => { + const { role, fullName } = filters; + + const users = await prisma.user.findMany({ + where: { + AND: [ + role + ? { + role: { + name: { contains: role }, + }, + } + : {}, + fullName ? { fullName: { contains: fullName } } : {}, + ], + }, + select: { + id: true, + fullName: true, + email: true, + gender: true, + age: true, + phoneNumber: true, + createdAt: true, + updatedAt: true, + role: true, + profile: { + select: { + id: true, + about: true, + avatar: true, + maritalStatus: true, + certifiedStatus: true, + work: { + select: { + id: true, + name: true, + createdAt: true, + updatedAt: true, + }, + }, + education: { + select: { + id: true, + name: true, + createdAt: true, + updatedAt: true, + }, + }, + investment: true, + insurance: true, + incomePerMonth: true, + totalSaving: true, + totalDebt: true, + createdAt: true, + updatedAt: true, + }, + }, + }, + }); + + return users; +}; + +export const findUserById = async (userId) => { + const user = await prisma.user.findUnique({ + where: { + id: userId, + }, + select: { + id: true, + fullName: true, + email: true, + gender: true, + age: true, + phoneNumber: true, + createdAt: true, + updatedAt: true, + role: true, + profile: { + select: { + id: true, + avatar: true, + about: true, + maritalStatus: true, + certifiedStatus: true, + work: { + select: { + id: true, + name: true, + createdAt: true, + updatedAt: true, + }, + }, + education: { + select: { + id: true, + name: true, + createdAt: true, + updatedAt: true, + }, + }, + investment: true, + insurance: true, + incomePerMonth: true, + totalSaving: true, + totalDebt: true, + createdAt: true, + updatedAt: true, + }, + }, + }, + }); + + return user; +}; + +export const findUserByEmail = async (email) => { + const user = await prisma.user.findUnique({ + where: { + email, + }, + select: { + id: true, + fullName: true, + email: true, + gender: true, + age: true, + phoneNumber: true, + password: true, + createdAt: true, + updatedAt: true, + role: true, + profile: { + select: { + id: true, + avatar: true, + about: true, + maritalStatus: true, + certifiedStatus: true, + work: { + select: { + id: true, + name: true, + createdAt: true, + updatedAt: true, + }, + }, + education: { + select: { + id: true, + name: true, + createdAt: true, + updatedAt: true, + }, + }, + investment: true, + insurance: true, + incomePerMonth: true, + totalSaving: true, + totalDebt: true, + createdAt: true, + updatedAt: true, + }, + }, + }, + }); + + return user; +}; + +export const findUserByRefreshToken = async (refreshToken) => { + const user = await prisma.user.findFirst({ + where: { + refreshToken, + }, + select: { + id: true, + fullName: true, + email: true, + gender: true, + age: true, + phoneNumber: true, + password: true, + createdAt: true, + updatedAt: true, + role: true, + profile: { + select: { + id: true, + avatar: true, + about: true, + maritalStatus: true, + certifiedStatus: true, + work: { + select: { + id: true, + name: true, + createdAt: true, + updatedAt: true, + }, + }, + education: { + select: { + id: true, + name: true, + createdAt: true, + updatedAt: true, + }, + }, + investment: true, + insurance: true, + incomePerMonth: true, + totalSaving: true, + totalDebt: true, + createdAt: true, + updatedAt: true, + }, + }, + }, + }); + + return user; +}; + +export const updateUserById = async (userId, userData) => { + const user = await prisma.user.update({ + where: { + id: userId, + }, + data: userData, + }); + + return user; +}; + +export const deleteUserById = async (userId) => { + const user = await prisma.user.delete({ + where: { + id: userId, + }, + }); + + return user; +}; + +export const findUserProfileByUserId = async (userId) => { + const profile = await prisma.profile.findUnique({ + where: { + userId, + }, + select: { + id: true, + about: true, + avatar: true, + maritalStatus: true, + certifiedStatus: true, + work: true, + education: true, + investment: true, + insurance: true, + incomePerMonth: true, + totalSaving: true, + totalDebt: true, + createdAt: true, + updatedAt: true, + }, + }); + + return profile; +}; + +export const updateUserProfileByUserId = async (userId, userData) => { + const newUserProfile = await prisma.profile.update({ + where: { + userId, + }, + data: { + ...(userData.about && { + about: userData.about, + }), + ...(userData.maritalStatus && { + maritalStatus: userData.maritalStatus, + }), + ...(userData.certifiedStatus && { + certifiedStatus: userData.certifiedStatus, + }), + ...(userData.workId && { + work: { connect: { id: userData.workId } }, + }), + ...(userData.educationId && { + education: { connect: { id: userData.educationId } }, + }), + ...(userData.investment && { + investment: userData.investment, + }), + ...(userData.insurance && { + insurance: userData.insurance, + }), + ...(userData.incomePerMonth && { + incomePerMonth: userData.incomePerMonth, + }), + ...(userData.totalSaving && { + totalSaving: userData.totalSaving, + }), + ...(userData.totalDebt && { + totalDebt: userData.totalDebt, + }), + }, + }); + + return newUserProfile; +}; + +export const deleteUserProfileByUserId = async (userId) => { + const userProfile = await prisma.profile.update({ + where: { + userId, + }, + data: { + about: null, + maritalStatus: null, + certifiedStatus: null, + workId: null, + educationId: null, + investment: null, + insurance: null, + incomePerMonth: null, + totalSaving: null, + totalDebt: null, + }, + }); + + return userProfile; +}; + +export const createBlankProfileWithUserId = async (userId) => { + const user = await prisma.profile.create({ + data: { + user: { + connect: { + id: userId, + }, + }, + }, + }); + + return user; +}; + +export const updateAvatarUserByUserId = async (userId, imageUrl) => { + const user = await prisma.profile.update({ + where: { + userId, + }, + data: { + avatar: imageUrl, + }, + }); + + return user; +}; diff --git a/src/repository/works.repository.js b/src/repository/works.repository.js new file mode 100644 index 0000000..3f80412 --- /dev/null +++ b/src/repository/works.repository.js @@ -0,0 +1,69 @@ +import prisma from "../../db/prisma.js"; + +export const findWorks = async () => { + const works = await prisma.work.findMany({ + select: { + id: true, + name: true, + createdAt: true, + updatedAt: true, + }, + }); + + return works; +}; + +export const findWorksByName = async (name) => { + const works = await prisma.work.findMany({ + where: { + name, + }, + }); + + return works; +}; + +export const createWork = async (newWorkData) => { + const newWork = await prisma.work.create({ + data: newWorkData, + }); + + return newWork; +}; + +export const findWorkById = async (workId) => { + const work = await prisma.work.findUnique({ + where: { + id: workId, + }, + select: { + id: true, + name: true, + createdAt: true, + updatedAt: true, + }, + }); + + return work; +}; + +export const updateWorkById = async (workId, newWorkData) => { + const work = await prisma.work.update({ + where: { + id: workId, + }, + data: newWorkData, + }); + + return work; +}; + +export const deleteWorkById = async (workId) => { + const work = await prisma.work.delete({ + where: { + id: workId, + }, + }); + + return work; +}; diff --git a/src/routes/api.js b/src/routes/api.js index d7a88d2..98609c0 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -1,8 +1,22 @@ import express from "express"; import rolesRouter from "./router/roles.router.js"; +import authsRouter from "./router/auths.router.js"; +import tokensRouter from "./router/tokens.router.js"; +import usersRouter from "./router/users.router.js"; +import worksRouter from "./router/works.router.js"; +import educationsRouter from "./router/educations.router.js"; +import chatRoomsRouter from "./router/chat-rooms.router.js"; +import chatAiRouter from "./router/chat-ai.router.js"; const app = express(); app.use(rolesRouter); +app.use(authsRouter); +app.use(tokensRouter); +app.use(usersRouter); +app.use(worksRouter); +app.use(educationsRouter); +app.use(chatRoomsRouter) +app.use(chatAiRouter) export default app; diff --git a/src/routes/router/auths.router.js b/src/routes/router/auths.router.js new file mode 100644 index 0000000..2a16cd4 --- /dev/null +++ b/src/routes/router/auths.router.js @@ -0,0 +1,14 @@ +import express from "express"; +import { + signInUserHandler, + signOutUserHandler, + signUpUserHandler, +} from "../../controller/auths.controller.js"; + +const router = express.Router(); + +router.post("/auth/signup", signUpUserHandler); +router.post("/auth/signin", signInUserHandler); +router.delete("/auth/signout", signOutUserHandler); + +export default router; diff --git a/src/routes/router/chat-ai.router.js b/src/routes/router/chat-ai.router.js new file mode 100644 index 0000000..2556db7 --- /dev/null +++ b/src/routes/router/chat-ai.router.js @@ -0,0 +1,19 @@ +import express from "express"; +import { verifyAccessTokenHandler } from "../../middlewares/tokens.middleware.js"; +import { getGenerativeAiHandler, getSugesstionQuestionHandler } from "../../controller/chat-ai.controller.js"; + +const router = express.Router(); + +router.post( + "/chats/ai/predict", + verifyAccessTokenHandler, + getGenerativeAiHandler +); + +router.post( + "/chats/ai/suggestions", + verifyAccessTokenHandler, + getSugesstionQuestionHandler +); + +export default router; \ No newline at end of file diff --git a/src/routes/router/chat-rooms.router.js b/src/routes/router/chat-rooms.router.js new file mode 100644 index 0000000..35b1f13 --- /dev/null +++ b/src/routes/router/chat-rooms.router.js @@ -0,0 +1,42 @@ +import express from "express"; +import { + editChatRoomByIdHandler, + getAllChatRoomsHandler, + getChatRoomByIdHandler, + insertChatRoomHandler, + removeChatRoomByIdHandler, +} from "../../controller/chat-rooms.controller.js"; +import { verifyAccessTokenHandler } from "../../middlewares/tokens.middleware.js"; + +const router = express.Router(); + +router.get( + "/chat-rooms", + verifyAccessTokenHandler, + getAllChatRoomsHandler +); +router.post( + "/chat-rooms", + verifyAccessTokenHandler, + insertChatRoomHandler +); + +router.get( + "/chat-rooms/:chatRoomId", + verifyAccessTokenHandler, + getChatRoomByIdHandler +); + +router.put( + "/chat-rooms/:chatRoomId", + verifyAccessTokenHandler, + editChatRoomByIdHandler +); + +router.delete( + "/chat-rooms/:chatRoomId", + verifyAccessTokenHandler, + removeChatRoomByIdHandler +); + +export default router; \ No newline at end of file diff --git a/src/routes/router/educations.router.js b/src/routes/router/educations.router.js new file mode 100644 index 0000000..4f0cf22 --- /dev/null +++ b/src/routes/router/educations.router.js @@ -0,0 +1,18 @@ +import express from "express"; +import { + editEducationByIdHandler, + getAllEducationsHandler, + getEducationByIdHandler, + insertEducationHandler, + removeEducationByIdHandler, +} from "../../controller/educations.controller.js"; + +const router = express.Router(); + +router.get("/educations", getAllEducationsHandler); +router.post("/educations", insertEducationHandler); +router.get("/educations/:educationId", getEducationByIdHandler); +router.put("/educations/:educationId", editEducationByIdHandler); +router.delete("/educations/:educationId", removeEducationByIdHandler); + +export default router; diff --git a/src/routes/router/roles.router.js b/src/routes/router/roles.router.js index 09ba5a7..4cd84c8 100644 --- a/src/routes/router/roles.router.js +++ b/src/routes/router/roles.router.js @@ -1,12 +1,18 @@ import express from "express"; import { + editRoleByIdHandler, getAllRolesHandler, + getRoleByIdHandler, insertRoleHandler, + removeRoleByIdHandler, } from "../../controller/roles.controller.js"; const router = express.Router(); router.get("/roles", getAllRolesHandler); router.post("/roles", insertRoleHandler); +router.get("/roles/:roleId", getRoleByIdHandler); +router.put("/roles/:roleId", editRoleByIdHandler); +router.delete("/roles/:roleId", removeRoleByIdHandler); export default router; diff --git a/src/routes/router/tokens.router.js b/src/routes/router/tokens.router.js new file mode 100644 index 0000000..a35aee5 --- /dev/null +++ b/src/routes/router/tokens.router.js @@ -0,0 +1,8 @@ +import express from "express"; +import { refreshTokenHandler } from "../../controller/tokens.controller.js"; + +const router = express.Router(); + +router.get("/token/refresh", refreshTokenHandler); + +export default router; diff --git a/src/routes/router/users.router.js b/src/routes/router/users.router.js new file mode 100644 index 0000000..4b80a90 --- /dev/null +++ b/src/routes/router/users.router.js @@ -0,0 +1,61 @@ +import express from "express"; +import { verifyAccessTokenHandler } from "../../middlewares/tokens.middleware.js"; +import { + editAvatarUserHandler, + editUserAllFieldByIdHandler, + editUserPartialFieldByIdHandler, + editUserProfilePartialFieldByUserIdHandler, + getAllUsersHandler, + getUserByIdHandler, + getUserProfileByUserIdHandler, + removeUserByIdHandler, + removeUserProfileByUserIdHandler, +} from "../../controller/users.controller.js"; +import { + uploadAvatar, + uploadToGcs, +} from "../../middlewares/images.middleware.js"; + +const router = express.Router(); + +router.get("/users", verifyAccessTokenHandler, getAllUsersHandler); +router.get("/users/:userId", verifyAccessTokenHandler, getUserByIdHandler); +router.put( + "/users/:userId", + verifyAccessTokenHandler, + editUserAllFieldByIdHandler +); +router.patch( + "/users/:userId", + verifyAccessTokenHandler, + editUserPartialFieldByIdHandler +); +router.delete( + "/users/:userId", + verifyAccessTokenHandler, + removeUserByIdHandler +); +router.get( + "/users/:userId/profile", + verifyAccessTokenHandler, + getUserProfileByUserIdHandler +); +router.patch( + "/users/:userId/profile", + verifyAccessTokenHandler, + editUserProfilePartialFieldByUserIdHandler +); +router.delete( + "/users/:userId/profile", + verifyAccessTokenHandler, + removeUserProfileByUserIdHandler +); +router.put( + "/users/:userId/profile/avatar", + verifyAccessTokenHandler, + uploadAvatar.single("avatar"), + uploadToGcs, + editAvatarUserHandler +); + +export default router; diff --git a/src/routes/router/works.router.js b/src/routes/router/works.router.js new file mode 100644 index 0000000..a722b22 --- /dev/null +++ b/src/routes/router/works.router.js @@ -0,0 +1,18 @@ +import express from "express"; +import { + editWorkByIdHandler, + getAllWorksHandler, + getWorkByIdHandler, + insertWorkHandler, + removeWorkByIdHandler, +} from "../../controller/works.controller.js"; + +const router = express.Router(); + +router.get("/works", getAllWorksHandler); +router.post("/works", insertWorkHandler); +router.get("/works/:workId", getWorkByIdHandler); +router.put("/works/:workId", editWorkByIdHandler); +router.delete("/works/:workId", removeWorkByIdHandler); + +export default router; diff --git a/src/schema/auths.schema.js b/src/schema/auths.schema.js new file mode 100644 index 0000000..4a17d52 --- /dev/null +++ b/src/schema/auths.schema.js @@ -0,0 +1,16 @@ +import { z } from "zod"; + +export const SignUpUserSchema = z.object({ + fullName: z.string(), + email: z.string().email(), + gender: z.enum(["Laki_laki", "Perempuan"]), + age: z.number(), + phoneNumber: z.string(), + password: z.string(), + roleId: z.string(), +}); + +export const SignInUserSchema = z.object({ + email: z.string().email(), + password: z.string(), +}); diff --git a/src/schema/chat-ai.schema.js b/src/schema/chat-ai.schema.js new file mode 100644 index 0000000..6746255 --- /dev/null +++ b/src/schema/chat-ai.schema.js @@ -0,0 +1,10 @@ +import { z } from "zod"; + +export const GenerativeAiInput = z.object({ + prompt: z.string(), +}); + +export const SugesstionQuestionInput = z.object({ + user_input: z.string(), + total_questions: z.string(), +}); \ No newline at end of file diff --git a/src/schema/chat-rooms.schema.js b/src/schema/chat-rooms.schema.js new file mode 100644 index 0000000..56b4614 --- /dev/null +++ b/src/schema/chat-rooms.schema.js @@ -0,0 +1,13 @@ +import { z } from "zod"; + +export const InsertChatRoomSchema = z.object({ + type: z.enum(['Expert', 'AI']), + userId: z.string(), + expertId: z.string().optional(), +}); + +export const EditChatRoomSchema = z.object({ + type: z.enum(['Expert', 'AI']), + userId: z.string(), + expertId: z.string().optional(), +}); \ No newline at end of file diff --git a/src/schema/educations.schema.js b/src/schema/educations.schema.js new file mode 100644 index 0000000..1043295 --- /dev/null +++ b/src/schema/educations.schema.js @@ -0,0 +1,9 @@ +import { z } from "zod"; + +export const InsertEducationSchema = z.object({ + name: z.string(), +}); + +export const EditEducationSchema = z.object({ + name: z.string(), +}); diff --git a/src/schema/roles.schema.js b/src/schema/roles.schema.js new file mode 100644 index 0000000..0009544 --- /dev/null +++ b/src/schema/roles.schema.js @@ -0,0 +1,9 @@ +import { z } from "zod"; + +export const InsertRoleSchema = z.object({ + name: z.string(), +}); + +export const EditRoleSchema = z.object({ + name: z.string(), +}); diff --git a/src/schema/users.schema.js b/src/schema/users.schema.js new file mode 100644 index 0000000..ed86462 --- /dev/null +++ b/src/schema/users.schema.js @@ -0,0 +1,37 @@ +import { z } from "zod"; + +export const UpdateAllFieldUserSchema = z.object({ + fullName: z.string(), + email: z.string().email(), + age: z.number(), + phoneNumber: z.string(), +}); + +export const UpdatePartialFieldUserSchema = z + .object({ + fullName: z.string().optional(), + email: z.string().email().optional(), + age: z.number().optional(), + phoneNumber: z.string().optional(), + password: z.string().optional(), + }) + .strict(); + +export const UpdatePartialFieldUserProfileSchema = z + .object({ + about: z.string().optional(), + maritalStatus: z.enum(["Lajang", "Menikah", "Cerai"]).optional(), + certifiedStatus: z.string().nullable().optional(), + workId: z.string().optional(), + educationId: z.string().optional(), + investment: z + .enum(["Saham", "Reksadana", "Obligasi", "Emas", "Cryptocurrency"]) + .optional(), + insurance: z + .enum(["Saham", "Reksadana", "Obligasi", "Emas", "Cryptocurrency"]) + .optional(), + incomePerMonth: z.union([z.string(), z.number()]).optional(), + totalSaving: z.union([z.string(), z.number()]).optional(), + totalDebt: z.union([z.string(), z.number()]).optional(), + }) + .strict(); diff --git a/src/schema/works.schema.js b/src/schema/works.schema.js new file mode 100644 index 0000000..37f2e8f --- /dev/null +++ b/src/schema/works.schema.js @@ -0,0 +1,9 @@ +import { z } from "zod"; + +export const InsertWorkSchema = z.object({ + name: z.string(), +}); + +export const EditWorkSchema = z.object({ + name: z.string(), +}); diff --git a/src/service/auths.service.js b/src/service/auths.service.js new file mode 100644 index 0000000..7171fb6 --- /dev/null +++ b/src/service/auths.service.js @@ -0,0 +1,38 @@ +import { handleConflictError } from "../exceptions/client.exception.js"; +import { + createUser, + updateRefreshTokenUser, +} from "../repository/auths.repository.js"; +import { findUserByEmail } from "../repository/users.repository.js"; +import { getPublicUrl } from "../utils/bucket.util.js"; +import { getUserById } from "./users.service.js"; + +export const signUpUser = async (newUserData, res) => { + const findUser = await findUserByEmail(newUserData.email); + + if (findUser) { + handleConflictError("Email already exist", res); + } + + const avatarDefault = + newUserData.gender === "Laki_laki" + ? getPublicUrl("male.png") + : getPublicUrl("female.png"); + + const newUser = await createUser({ + ...newUserData, + profile: { + create: { + avatar: avatarDefault, + }, + }, + }); + + return newUser; +}; + +export const editRefreshTokenUser = async (userId, newRefreshToken, res) => { + await getUserById(userId, res); + + updateRefreshTokenUser(userId, newRefreshToken); +}; diff --git a/src/service/chat-ai.service.js b/src/service/chat-ai.service.js new file mode 100644 index 0000000..bb57e57 --- /dev/null +++ b/src/service/chat-ai.service.js @@ -0,0 +1,49 @@ +import { findUserByEmail } from "../repository/users.repository.js"; + +const GENERATIVE_AI_SERVICE_URL = process.env.GENERATIVE_AI_SERVICE_URL || ""; +const SUGGESTION_AI_SERVICE_URL = process.env.SUGGESTION_AI_SERVICE_URL || ""; + +export const getGenerativeAi = async (data) => { + const response = await fetch(GENERATIVE_AI_SERVICE_URL , { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + ...data, + }), + }); + + const res = await response.json(); + + return res; +} + +export const getSugesstionQuestion = async (data, emailUser) => { + + const userData = await findUserByEmail(emailUser); + + const profileData = userData?.profile ? { + incomePerMonth: userData.profile.incomePerMonth, + investments: userData.profile.investment, + totalSavings: userData.profile.totalSaving, + totalDebts: userData.profile.totalDebt, + insurances: userData.profile.insurance, + } : {}; + + const response = await fetch(SUGGESTION_AI_SERVICE_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + ...data, + total_questions: parseInt(data.total_questions), + ...profileData, + }), + }); + + const res = await response.json(); + + return res; +} diff --git a/src/service/chat-rooms.service.js b/src/service/chat-rooms.service.js new file mode 100644 index 0000000..d377ccc --- /dev/null +++ b/src/service/chat-rooms.service.js @@ -0,0 +1,52 @@ +import { + NotFoundError, + handleNotFoundError, +} from "../exceptions/client.exception.js"; +import { + createChatRoom, + deleteChatRoomById, + findChatRoomById, + findChatRooms, + findChatRoomsByUserId, + updateChatRoomById, +} from "../repository/chat-rooms.repository.js"; + +export const getAllChatRooms = async () => { + const chatRooms = await findChatRooms(); + + return chatRooms; +}; + +export const getAllChatRoomsByUserId = async (userId) => { + const chatRooms = await findChatRoomsByUserId(userId); + + return chatRooms; +}; + +export const insertChatRoom = async (newChatRoomData) => { + const newChatRoom = await createChatRoom(newChatRoomData); + + return newChatRoom; +}; + +export const getChatRoomById = async (chatRoomId, res) => { + const chatRoom = await findChatRoomById(chatRoomId); + + if (!chatRoom) { + handleNotFoundError("Chat Room not found", res); + } + + return chatRoom; +}; + +export const editChatRoomById = async (chatRoomId, newChatRoomData, res) => { + await getChatRoomById(chatRoomId, res); + const chatRoom = await updateChatRoomById(chatRoomId, newChatRoomData); + + return chatRoom; +}; + +export const removeChatRoomById = async (chatRoomId, res) => { + await getChatRoomById(chatRoomId, res); + await deleteChatRoomById(chatRoomId); +}; \ No newline at end of file diff --git a/src/service/educations.service.js b/src/service/educations.service.js new file mode 100644 index 0000000..a559368 --- /dev/null +++ b/src/service/educations.service.js @@ -0,0 +1,52 @@ +import { + NotFoundError, + handleNotFoundError, +} from "../exceptions/client.exception.js"; +import { + createEducation, + deleteEducationById, + findEducationById, + findEducations, + findEducationsByName, + updateEducationById, +} from "../repository/educations.repository.js"; + +export const getAllEducations = async () => { + const educations = await findEducations(); + + return educations; +}; + +export const getAllEducationsByName = async (name) => { + const educations = await findEducationsByName(name); + + return educations; +}; + +export const insertEducation = async (newEducationData) => { + const newEducation = await createEducation(newEducationData); + + return newEducation; +}; + +export const getEducationById = async (educationId, res) => { + const education = await findEducationById(educationId); + + if (!education) { + handleNotFoundError("Edcuation not found", res); + } + + return education; +}; + +export const editEducationById = async (educationId, newEducationData, res) => { + await getEducationById(educationId, res); + const education = await updateEducationById(educationId, newEducationData); + + return education; +}; + +export const removeEducationById = async (educationId, res) => { + await getEducationById(educationId, res); + await deleteEducationById(educationId); +}; diff --git a/src/service/roles.service.js b/src/service/roles.service.js index e4d9aba..d4cb6de 100644 --- a/src/service/roles.service.js +++ b/src/service/roles.service.js @@ -1,4 +1,15 @@ -import { createRole, findRoles } from "../repository/roles.repository.js"; +import { + NotFoundError, + handleNotFoundError, +} from "../exceptions/client.exception.js"; +import { + createRole, + deleteRole, + findRoleById, + findRoles, + findRolesByName, + updateRole, +} from "../repository/roles.repository.js"; export const getAllRoles = async () => { const roles = await findRoles(); @@ -6,8 +17,36 @@ export const getAllRoles = async () => { return roles; }; +export const getAllRolesByName = async (name) => { + const roles = await findRolesByName(name); + + return roles; +}; + export const insertRole = async (newRoleData) => { const newRole = await createRole(newRoleData); return newRole; }; + +export const getRoleById = async (roleId, res) => { + const role = await findRoleById(roleId); + + if (!role) { + handleNotFoundError("Role not found", res); + } + + return role; +}; + +export const editRoleById = async (roleId, newRoleData, res) => { + await getRoleById(roleId, res); + const role = await updateRole(roleId, newRoleData); + + return role; +}; + +export const removeRoleById = async (roleId, res) => { + await getRoleById(roleId, res); + await deleteRole(roleId); +}; diff --git a/src/service/users.service.js b/src/service/users.service.js new file mode 100644 index 0000000..da34825 --- /dev/null +++ b/src/service/users.service.js @@ -0,0 +1,98 @@ +import { handleVerifyOwnerTokenError } from "../exceptions/auth.exception.js"; +import { handleNotFoundError } from "../exceptions/client.exception.js"; +import { + createBlankProfileWithUserId, + deleteUserById, + deleteUserProfileByUserId, + findUserByEmail, + findUserById, + findUserByRefreshToken, + findUserProfileByUserId, + findUsers, + updateAvatarUserByUserId, + updateUserById, + updateUserProfileByUserId, +} from "../repository/users.repository.js"; + +export const getAllUsers = async (filters) => { + const users = await findUsers(filters); + + return users; +}; + +export const getUserById = async (userId, res) => { + const user = await findUserById(userId); + + if (!user) { + handleNotFoundError("User not found", res); + } + + return user; +}; + +export const getUserByEmail = async (email, res) => { + const user = await findUserByEmail(email); + + if (!user) { + handleNotFoundError("Email not found", res); + } + + return user; +}; + +export const getUserByRefreshToken = async (refreshToken, res) => { + const user = await findUserByRefreshToken(refreshToken); + + if (!user) { + handleVerifyOwnerTokenError(undefined, res); + } + + return user; +}; + +export const editUserById = async (userId, userData, res) => { + await getUserById(userId, res); + + const user = await updateUserById(userId, userData); + + return user; +}; + +export const removeUserById = async (userId, res) => { + await getUserById(userId, res); + await deleteUserById(userId); +}; + +export const getUserProfileByUserId = async (userId, res) => { + await getUserById(userId, res); + const profile = await findUserProfileByUserId(userId); + + return profile; +}; + +export const editUserProfileByUserId = async (userId, userData, res) => { + await getUserById(userId, res); + const newUserProfile = await updateUserProfileByUserId(userId, userData); + + return newUserProfile; +}; + +export const removeUserProfileByUserId = async (userId, res) => { + await getUserById(userId, res); + const profile = await deleteUserProfileByUserId(userId); + + return profile; +}; + +export const editAvatarUser = async (userId, imageUrl, res) => { + await getUserById(userId, res); + const userProfile = await findUserProfileByUserId(userId); + + if (!userProfile) { + await createBlankProfileWithUserId(userId); + } + + const updateAvatarUser = await updateAvatarUserByUserId(userId, imageUrl); + + return updateAvatarUser; +}; diff --git a/src/service/works.service.js b/src/service/works.service.js new file mode 100644 index 0000000..28b633a --- /dev/null +++ b/src/service/works.service.js @@ -0,0 +1,52 @@ +import { + NotFoundError, + handleNotFoundError, +} from "../exceptions/client.exception.js"; +import { + createWork, + deleteWorkById, + findWorkById, + findWorks, + findWorksByName, + updateWorkById, +} from "../repository/works.repository.js"; + +export const getAllWorks = async () => { + const works = await findWorks(); + + return works; +}; + +export const getAllWorksByName = async (name) => { + const works = await findWorksByName(name); + + return works; +}; + +export const insertWork = async (newWorkData) => { + const newWork = await createWork(newWorkData); + + return newWork; +}; + +export const getWorkById = async (workId, res) => { + const work = await findWorkById(workId); + + if (!work) { + handleNotFoundError("Work not found", res); + } + + return work; +}; + +export const editWorkById = async (workId, newWorkData, res) => { + await getWorkById(workId, res); + const work = await updateWorkById(workId, newWorkData); + + return work; +}; + +export const removeWorkById = async (workId, res) => { + await getWorkById(workId, res); + await deleteWorkById(workId); +}; diff --git a/src/utils/bucket.util.js b/src/utils/bucket.util.js new file mode 100644 index 0000000..ce7ecf1 --- /dev/null +++ b/src/utils/bucket.util.js @@ -0,0 +1,27 @@ +import { Storage } from "@google-cloud/storage"; +import path from "path"; + +const serviceAccountPathKey = path.resolve("./serviceaccount.json"); + +const gcs = new Storage({ + projectId: process.env.GCLOUD_PROJECT_ID, +}); + +const bucketName = process.env.GCLOUD_BUCKET_NAME; +const bucket = gcs.bucket(bucketName); + +export const deleteFileFromBucket = async (fileName) => { + try { + await bucket.file(fileName).delete(); + } catch (error) { + throw error; + } +}; + +export const getPublicUrl = (fileName) => { + return `https://storage.googleapis.com/${bucketName}/${fileName}`; +}; + +export const getFileNameFromUrl = (url) => { + return url.split("/").pop(); +}; diff --git a/src/utils/jwt.util.js b/src/utils/jwt.util.js new file mode 100644 index 0000000..81e84d2 --- /dev/null +++ b/src/utils/jwt.util.js @@ -0,0 +1,7 @@ +import jwt from "jsonwebtoken"; + +export const generateJwtToken = (payload, secret, expire) => { + const token = jwt.sign(payload, secret, expire); + + return token; +}; diff --git a/yarn.lock b/yarn.lock index 4856fc1..e2bd1a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -61,46 +61,46 @@ semver "^7.3.5" tar "^6.1.11" -"@prisma/client@^5.14.0": - version "5.14.0" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.14.0.tgz#dadca5bb1137ddcebb454bbdaf89423823d3363f" - integrity sha512-akMSuyvLKeoU4LeyBAUdThP/uhVP3GuLygFE3MlYzaCb3/J8SfsYBE5PkaFuLuVpLyA6sFoW+16z/aPhNAESqg== - -"@prisma/debug@5.14.0": - version "5.14.0" - resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.14.0.tgz#1227c705893c38284f7c63d72441480ebaa12605" - integrity sha512-iq56qBZuFfX3fCxoxT8gBX33lQzomBU0qIUaEj1RebsKVz1ob/BVH1XSBwwwvRVtZEV1b7Fxx2eVu34Ge/mg3w== - -"@prisma/engines-version@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48": - version "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48.tgz#019c3c75a5c3276e580685fe48cdbfd181176858" - integrity sha512-ip6pNkRo1UxWv+6toxNcYvItNYaqQjXdFNGJ+Nuk2eYtRoEdoF13wxo7/jsClJFFenMPVNVqXQDV0oveXnR1cA== - -"@prisma/engines@5.14.0": - version "5.14.0" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.14.0.tgz#2ee91dd2220a726c27c906fbea788bbb3efdac6e" - integrity sha512-lgxkKZ6IEygVcw6IZZUlPIfLQ9hjSYAtHjZ5r64sCLDgVzsPFCi2XBBJgzPMkOQ5RHzUD4E/dVdpn9+ez8tk1A== - dependencies: - "@prisma/debug" "5.14.0" - "@prisma/engines-version" "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48" - "@prisma/fetch-engine" "5.14.0" - "@prisma/get-platform" "5.14.0" - -"@prisma/fetch-engine@5.14.0": - version "5.14.0" - resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.14.0.tgz#45297c118d4ec3fea55129886edd5a429da1f6da" - integrity sha512-VrheA9y9DMURK5vu8OJoOgQpxOhas3qF0IBHJ8G/0X44k82kc8E0w98HCn2nhnbOOMwbWsJWXfLC2/F8n5u0gQ== - dependencies: - "@prisma/debug" "5.14.0" - "@prisma/engines-version" "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48" - "@prisma/get-platform" "5.14.0" - -"@prisma/get-platform@5.14.0": - version "5.14.0" - resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.14.0.tgz#69112d3dde61905f59a65ed818f153e153ca40f0" - integrity sha512-/yAyBvcEjRv41ynZrhdrPtHgk47xLRRq/o5eWGcUpBJ1YrUZTYB8EoPiopnP7iQrMATK8stXQdPOoVlrzuTQZw== - dependencies: - "@prisma/debug" "5.14.0" +"@prisma/client@^5.15.0": + version "5.15.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.15.0.tgz#a9443ace9b8a8d57aff70647168e95f2f55c5dc9" + integrity sha512-wPTeTjbd2Q0abOeffN7zCDCbkp9C9cF+e9HPiI64lmpehyq2TepgXE+sY7FXr7Rhbb21prLMnhXX27/E11V09w== + +"@prisma/debug@5.15.0": + version "5.15.0" + resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.15.0.tgz#a4c1d8dbca9cf29aab1c82a56a65224ed3e05f13" + integrity sha512-QpEAOjieLPc/4sMny/WrWqtpIAmBYsgqwWlWwIctqZO0AbhQ9QcT6x2Ut3ojbDo/pFRCCA1Z1+xm2MUy7fAkZA== + +"@prisma/engines-version@5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022": + version "5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022.tgz#4469a372b74088db05c0fc8cff65f229b804fa51" + integrity sha512-3BEgZ41Qb4oWHz9kZNofToRvNeS4LZYaT9pienR1gWkjhky6t6K1NyeWNBkqSj2llgraUNbgMOCQPY4f7Qp5wA== + +"@prisma/engines@5.15.0": + version "5.15.0" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.15.0.tgz#bddf1973b5b0d2ebed473ed445b1a7c8dd23300b" + integrity sha512-hXL5Sn9hh/ZpRKWiyPA5GbvF3laqBHKt6Vo70hYqqOhh5e0ZXDzHcdmxNvOefEFeqxra2DMz2hNbFoPvqrVe1w== + dependencies: + "@prisma/debug" "5.15.0" + "@prisma/engines-version" "5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022" + "@prisma/fetch-engine" "5.15.0" + "@prisma/get-platform" "5.15.0" + +"@prisma/fetch-engine@5.15.0": + version "5.15.0" + resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.15.0.tgz#f5bafd6aed3f58c41b5d0d6f832d652aa5d4cde7" + integrity sha512-z6AY5yyXxc20Klj7wwnfGP0iIUkVKzybqapT02zLYR/nf9ynaeN8bq73WRmi1TkLYn+DJ5Qy+JGu7hBf1pE78A== + dependencies: + "@prisma/debug" "5.15.0" + "@prisma/engines-version" "5.15.0-29.12e25d8d06f6ea5a0252864dd9a03b1bb51f3022" + "@prisma/get-platform" "5.15.0" + +"@prisma/get-platform@5.15.0": + version "5.15.0" + resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.15.0.tgz#d39fbe8458432f76afeb6c9199bffae73db4f5cc" + integrity sha512-1GULDkW4+/VQb73vihxCBSc4Chc2x88MA+O40tcZFjmBzG4/fF44PaXFxUqKSFltxU9L9GIMLhh0Gfkk/pUbtg== + dependencies: + "@prisma/debug" "5.15.0" "@tootallnate/once@2": version "2.0.0" @@ -415,6 +415,11 @@ cors@^2.8.5: object-assign "^4" vary "^1" +dateformat@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-5.0.3.tgz#fe2223eff3cc70ce716931cb3038b59a9280696e" + integrity sha512-Kvr6HmPXUMerlLcLF+Pwq3K7apHpYmGDVqrxcDasBg86UcKeTSNWbEzU8bwdXnxnR44FtMhJAxI4Bov6Y/KUfA== + debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -1193,12 +1198,12 @@ picomatch@^2.0.4, picomatch@^2.2.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -prisma@^5.14.0: - version "5.14.0" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.14.0.tgz#ffc4696a43b044b636c3303b7aa98c13c2ade4dd" - integrity sha512-gCNZco7y5XtjrnQYeDJTiVZmT/ncqCr5RY1/Cf8X2wgLRmyh9ayPAGBNziI4qEE4S6SxCH5omQLVo9lmURaJ/Q== +prisma@^5.15.0: + version "5.15.0" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.15.0.tgz#887c295caa1b81b8849d94a2751cc0e0994f86d1" + integrity sha512-JA81ACQSCi3a7NUOgonOIkdx8PAVkO+HbUOxmd00Yb8DgIIEpr2V9+Qe/j6MLxIgWtE/OtVQ54rVjfYRbZsCfw== dependencies: - "@prisma/engines" "5.14.0" + "@prisma/engines" "5.15.0" process-nextick-args@~2.0.0: version "2.0.1"