From 07e1c609a1b169aacec05c3c3cd015d531ccdf18 Mon Sep 17 00:00:00 2001 From: Justin <40401142+Justin-Fernbaugh@users.noreply.github.com> Date: Thu, 23 May 2024 17:13:38 -0700 Subject: [PATCH] Containerize backend app and MySQL database. (#230) * Containerize backend and, integrate with MySQL container using docker-compose. * Add default password to compose environment * Update documentation to include Docker * Update documentation headers and command outline * Update docker documentation to have bolder commands * Add warning about Docker container volume * Modify init.sql to use legacy authentication * Modify .env.example default values to be consistent * Assign db.config.js values to be consistent with .env.example * Add legacy authentication to MySQL * Update .env dev database name * Remove mysql_native_password option in init.sql in favor of configuration option. --- server/.dockerignore | 4 ++ server/.env.example | 10 ++-- server/Dockerfile | 20 ++++++++ server/README.md | 94 ++++++++++++++++++++++++++++++++++++-- server/compose.yml | 41 +++++++++++++++++ server/config/db.config.js | 59 ++++++++++++------------ server/db/init.sql | 9 ++++ server/entrypoint.sh | 18 ++++++++ 8 files changed, 218 insertions(+), 37 deletions(-) create mode 100644 server/.dockerignore create mode 100644 server/Dockerfile create mode 100644 server/compose.yml create mode 100644 server/db/init.sql create mode 100755 server/entrypoint.sh diff --git a/server/.dockerignore b/server/.dockerignore new file mode 100644 index 00000000..edd83c04 --- /dev/null +++ b/server/.dockerignore @@ -0,0 +1,4 @@ +node_modules +npm-debug.log +Dockerfile +.dockerignore \ No newline at end of file diff --git a/server/.env.example b/server/.env.example index bffa5108..4388c146 100644 --- a/server/.env.example +++ b/server/.env.example @@ -1,10 +1,10 @@ -DEV_DB_NAME='myclassroom' -DEV_DB_USER='admin' -DEV_DB_PASS='Password_1' +DEV_DB_NAME='myclassroom_development' +DEV_DB_USER='dev_admin' +DEV_DB_PASS='password' TEST_DB_NAME='myclassroom_test' -TEST_DB_USER='testadmin' -TEST_DB_PASS='Password_2' +TEST_DB_USER='test_admin' +TEST_DB_PASS='password' PORT='3001' diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 00000000..18bb355d --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,20 @@ +# Use the official Node.js image as a base image +FROM node:latest + +# Set the working directory in the container +WORKDIR /app + +# Copy package.json and package-lock.json files to the working directory +COPY package*.json ./ + +# Install the dependencies +RUN npm install + +# Copy the rest of the application code to the working directory but ignore node_modules +COPY . . + +# Expose the port the app runs on +EXPOSE 3001 + +# Command to run the application +CMD ["npm", "run", "start:dev"] diff --git a/server/README.md b/server/README.md index 50784364..f4be1219 100644 --- a/server/README.md +++ b/server/README.md @@ -1,30 +1,39 @@ # My-Classroom-Backend + ## Dependencies + - node: 16.13.0 - npm: 9.1.2 - mysql: 8.0.31 ## Cloning Repo and Installing Dependencies + Install MySQL + - Refer to the [MySQL Getting Started Guide](https://dev.mysql.com/doc/mysql-getting-started/en/) for installing and troubleshooting MySQL. Clone the GitHub Repository + ``` git clone git@github.com:OSU-MC/MyClassroom.git ``` Navigate to the Server Directory + ``` cd MyClassroom/server ``` Install the Application Dependencies + ``` npm install ``` ## Configuring Local Environment + Rename the .env.example file to setup environment configuration + ``` mv .env.example .env ``` @@ -32,119 +41,190 @@ mv .env.example .env The server application can be configured by modifying the `/server/.env` file. The `DEV_DB_...` and `TEST_DB_...` values should match those in the database/user creation commands listed in the setup steps below. Additionally, `CLIENT_URL` should be set to the front end application URL. For basic testing, the default values can be used. ## Create and Migrate the Database + Connect to the MySQL Database using the Root User + ``` mysql -u root -p ``` Create the Application Database + ``` CREATE DATABASE myclassroom; ``` Create the Administrative Database User + ``` CREATE USER 'admin'@'localhost' IDENTIFIED BY 'Password_1'; ``` Grant the Administrative User Access to the Application Database + ``` GRANT ALL PRIVILEGES ON myclassroom.* TO 'admin'@'localhost'; ``` Disconnect from the MySQL Database + ``` exit ``` Migrate the Database using Sequelize + ``` npx sequelize-cli db:migrate ``` ## Setup Backend Testing Environment + Connect to the MySQL Database using the Root User + ``` mysql -u root -p ``` Create the Test Application Database + ``` CREATE DATABASE myclassroom_test; ``` Create the Testing Administrative Database User + ``` CREATE USER 'testadmin'@'localhost' IDENTIFIED BY 'Password_2'; ``` Grant the Testing Administrative User Access to the Test Application Database + ``` GRANT ALL PRIVILEGES ON myclassroom_test.* TO 'testadmin'@'localhost'; ``` Disconnect from the MySQL Database + ``` exit ``` Migrate the Test Database using Sequelize + ``` npx sequelize-cli db:migrate --env test ``` Seed the Test Database using Sequelize + ``` npx sequelize-cli db:seed:all --env test ``` ## Resetting/Rolling Back Databases + (Append `npx` commands with `--env test` to run on the test database) Undo Database Migrations + ``` npx sequelize-cli db:migrate:undo:all ``` Undo Test Database Seeding + ``` npx sequelize-cli db:seed:undo:all ``` Reset Local Database + ``` mysql -u root -p ``` + ``` DROP DATABASE myclassroom; ``` + or + ``` DROP DATABASE myclassroom_test; ``` -## Starting the Application +## Docker + +The backend NodeJS app and MySQL database can be deployed in containers using Docker. The Dockerfile will build the backend NodeJS app as a standalone whilst the compose.yml file will build and run the backend and MySQL containers. If you plan to re-initialize the database from scratch remember to delete the docker volume for it as well. + +### Build the backend Dockerfile alone (Optional) + ``` +docker build -t myclassroom_backend . +docker run -p 3001:3001: myclassroom_backend +``` + +### Utilizing docker-compose (Preferred) + +Build and start then detach with '-d' + +``` +docker-compose up --build -d +``` + +After first build we can start and stop using docker-compose + +``` +docker-compose up -d +docker-compose down +``` + +Inspecting detached containers + +``` +docker-compose logs +``` + +Optionally, the MySQL database can be started and stopped individually after the first build + +``` +docker container start myclasroom_db +``` + +## Starting the Application (Conventional NodeJS) + +``` + npm run start + ``` ## Testing the Application + Testing the application is easy. The Jest testing framework is used to write tests for the system. A script has been added to the package.json file to run tests locally: + ``` + npm run test + ``` If you run into issues, ensure you have done the following: + 1. Created a local test database 2. Properly instantiated all env variables for the test environment ## Application Authentication & Session -The application uses cookie-based authentication once a user session has been created (i.e. a user has logged in). A user's session will have a specific XSRF token value associated with it to protect against XSRF attacks. As such, the value of that token will be sent back as a cookie, and the application expects to recieve with each authenticated request a custom X-XSRF-TOKEN header with that value, along with the traditional authentication cookie _myclassroom_session which the application generated as part of initial session creation. + +The application uses cookie-based authentication once a user session has been created (i.e. a user has logged in). A user's session will have a specific XSRF token value associated with it to protect against XSRF attacks. As such, the value of that token will be sent back as a cookie, and the application expects to recieve with each authenticated request a custom X-XSRF-TOKEN header with that value, along with the traditional authentication cookie \_myclassroom_session which the application generated as part of initial session creation. A user's session is valid for a minimum of 4 hours, and as long as the user is active within 4 hours of last activity, the session can be valid for as long as 24 hours. In other words, users will be asked to login again after 4 hours of inactivity or 24 hours since they last provided their credentials. ## Configuring Services + ### Emailer + The application is configured to use Courier notification infastructure to message users. In order to use the application's mailer, create an account at https://www.courier.com/. Follow Courier's setup instructions and prompts. The process should yield a bearer token in the HTTPS request Courier generates. Copy this token, and paste it in the application environment as `COURIER_AUTH_TOKEN`. Also set `EABLE_EMAIL='true'`. That's it! You should be able to interact with the configured emailer through Courier. @@ -152,6 +232,7 @@ The process should yield a bearer token in the HTTPS request Courier generates. It's worth noting that the application is only configured for email use through Courier, but Courier supports a variety of modern notification methods. ## Roadmap + - Learning Management System (LMS) Integration - Expanded Question Type Support - WebSocket Open Polling Connection for Live Updates and Feedback @@ -160,14 +241,21 @@ It's worth noting that the application is only configured for email use through - Request Rate Limiting ## Database Schema -![Schema](https://github.com/OSU-MC/MyClassroom/assets/25465133/d987e780-fd0e-4ea5-bd18-c72de5d8c32c) +![Schema](https://github.com/OSU-MC/MyClassroom/assets/25465133/d987e780-fd0e-4ea5-bd18-c72de5d8c32c) ## Endpoints + [API Endpoints Doc](/API%20Endpoints%20MyClassroom.pdf) ## Getting Involved + Feel free to open an issue for feature requests or bugs. We openly accept pull requests for bug fixes. ## Licensing + GNU General Public License v3.0 + +``` + +``` diff --git a/server/compose.yml b/server/compose.yml new file mode 100644 index 00000000..04e57b52 --- /dev/null +++ b/server/compose.yml @@ -0,0 +1,41 @@ +version: "3.8" + +services: + backend: + container_name: myclassroom_backend + build: + context: . + dockerfile: Dockerfile + depends_on: + db: + condition: service_healthy + environment: + DEV_DB_HOST: db + DEV_DB_PASS: password + TEST_DB_HOST: db + TEST_DB_PASS: password + entrypoint: ["./entrypoint.sh"] + ports: + - "3001:3001" + expose: + - "3001" + + db: + image: mysql:latest + container_name: myclassroom_db + command: ["mysqld", "--mysql-native-password=ON"] + environment: + MYSQL_ROOT_PASSWORD: rootpassword + ports: + - "3306:3306" + volumes: + - db_data:/var/lib/mysql + - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -prootpassword"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + db_data: diff --git a/server/config/db.config.js b/server/config/db.config.js index 25d9d62d..c0aff104 100644 --- a/server/config/db.config.js +++ b/server/config/db.config.js @@ -1,32 +1,33 @@ -const env = process.env.NODE_ENV || 'development' -if (env === 'development' || env === 'test') { // dotenv file will only be used in dev and testing - require('dotenv').config({ override: false}); // will not override current environment variables if they exist +const env = process.env.NODE_ENV || "development"; +if (env === "development" || env === "test") { + // dotenv file will only be used in dev and testing + require("dotenv").config({ override: false }); // will not override current environment variables if they exist } module.exports = { - "development": { - "username": process.env.DEV_DB_USER || 'myclassroom', - "password": process.env.DEV_DB_PASS || null, - "database": process.env.DEV_DB_NAME || 'myclassroom_development', - "host": process.env.DEV_DB_HOST || 'localhost', - "port": process.env.DEV_DB_PORT || 3306, - "dialect": "mysql" - }, - "test": { - "username": process.env.TEST_DB_USER || 'myclassroom', - "password": process.env.TEST_DB_PASS || null, - "database": process.env.TEST_DB_NAME || 'myclassroom_test', - "host": process.env.TEST_DB_HOST || 'localhost', - "port": process.env.TEST_DB_PORT || 3306, - "dialect": "mysql" - }, - "production": { - "username": process.env.RDS_USERNAME, - "password": process.env.RDS_PASSWORD, - "database": process.env.RDS_DB_NAME, - "host": process.env.RDS_HOSTNAME, - "dialect": "mysql", - "ssl": 'Amazon RDS', - "port": process.env.RDS_PORT || 3306 - } -} + development: { + username: process.env.DEV_DB_USER || "dev_admin", + password: process.env.DEV_DB_PASS || "password", + database: process.env.DEV_DB_NAME || "myclassroom_development", + host: process.env.DEV_DB_HOST || "127.0.0.1", + port: process.env.DEV_DB_PORT || 3306, + dialect: "mysql", + }, + test: { + username: process.env.TEST_DB_USER || "test_admin", + password: process.env.TEST_DB_PASS || "password", + database: process.env.TEST_DB_NAME || "myclassroom_test", + host: process.env.TEST_DB_HOST || "127.0.0.1", + port: process.env.TEST_DB_PORT || 3306, + dialect: "mysql", + }, + production: { + username: process.env.RDS_USERNAME, + password: process.env.RDS_PASSWORD, + database: process.env.RDS_DB_NAME, + host: process.env.RDS_HOSTNAME, + dialect: "mysql", + ssl: "Amazon RDS", + port: process.env.RDS_PORT || 3306, + }, +}; diff --git a/server/db/init.sql b/server/db/init.sql new file mode 100644 index 00000000..be7e978f --- /dev/null +++ b/server/db/init.sql @@ -0,0 +1,9 @@ +CREATE DATABASE IF NOT EXISTS myclassroom_development; +CREATE USER IF NOT EXISTS 'dev_admin'@'%' IDENTIFIED BY 'password'; +GRANT ALL PRIVILEGES ON myclassroom_development.* TO 'dev_admin'@'%'; +FLUSH PRIVILEGES; + +CREATE DATABASE IF NOT EXISTS myclassroom_test; +CREATE USER IF NOT EXISTS 'test_admin'@'%' IDENTIFIED BY 'password'; +GRANT ALL PRIVILEGES ON myclassroom_test.* TO 'test_admin'@'%'; +FLUSH PRIVILEGES; diff --git a/server/entrypoint.sh b/server/entrypoint.sh new file mode 100755 index 00000000..772fbf12 --- /dev/null +++ b/server/entrypoint.sh @@ -0,0 +1,18 @@ +#!/bin/sh +# This script checks if the container is started for the first time. + +CONTAINER_FIRST_STARTUP="CONTAINER_FIRST_STARTUP" +if [ ! -e /$CONTAINER_FIRST_STARTUP ]; then + touch /$CONTAINER_FIRST_STARTUP + echo "Container first start intializing database..." + # place your script that you only want to run on first startup. + npx sequelize-cli db:migrate --env test && + npx sequelize-cli db:migrate --env development && + npx sequelize-cli db:seed:all --env test && + npx sequelize-cli db:seed:all --env development + echo "Completed migrating and seeding databases." + npm run start:dev +else + echo "Not running first start up script." + npm run start:dev +fi \ No newline at end of file