diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 101f60e6..91b59cc3 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,24 +1,35 @@ FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:16 -COPY config/ieee-money.sql /tmp/config/ +# Copy configuration files +COPY config/boilerbooks.sql /tmp/config/ +COPY .devcontainer/sql-presetup.sql /tmp/config/ COPY config/sql-setup.sql /tmp/config/ COPY config/nginx-dev.conf /tmp/config/ +# Copy files for development COPY .devcontainer/sample-data.sql /tmp/config/sample-data.sql COPY .devcontainer/aliases /etc/aliases -RUN echo "postfix postfix/mailname string localhost" | debconf-set-selections\ +# Preselect some options for postfix +RUN echo "postfix postfix/mailname string localhost" | debconf-set-selections \ && echo "postfix postfix/main_mailer_type string 'Internet Site'" | debconf-set-selections +# Install required packages RUN apt update && export DEBIAN_FRONTEND=noninteractive \ && apt -y install git mariadb-server-10.5 nginx postfix -RUN service mariadb start && mariadb < /tmp/config/ieee-money.sql && mariadb < /tmp/config/sql-setup.sql -RUN mv /tmp/config/nginx-dev.conf /etc/nginx/sites-enabled/default \ - && service nginx reload + +# Setup the database +RUN service mariadb start && mariadb < /tmp/config/boilerbooks.sql && mariadb < /tmp/config/sql-presetup.sql && mariadb < /tmp/config/sql-setup.sql + +# Setup NGINX +RUN mv /tmp/config/nginx-dev.conf /etc/nginx/sites-enabled/default && nginx -t + +# Setup Postfix RUN service postfix start && newaliases +# Create a sample dataset RUN mkdir /var/log/boilerbooks && chown node:node /var/log/boilerbooks RUN mkdir /var/www/receipts && chown node:node /var/www/receipts - -RUN service mariadb start && mariadb ieee-money < /tmp/config/sample-data.sql +RUN service mariadb start && mariadb boilerbooks < /tmp/config/sample-data.sql ADD .devcontainer/receipts /var/www/receipts/ +ADD .devcontainer/assets /var/www/assets/ diff --git a/.devcontainer/assets/pieee-kite.svg b/.devcontainer/assets/pieee-kite.svg new file mode 100644 index 00000000..bdcdf925 --- /dev/null +++ b/.devcontainer/assets/pieee-kite.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + diff --git a/.devcontainer/sample-data.sql b/.devcontainer/sample-data.sql index 45b7ea80..7fa00645 100644 --- a/.devcontainer/sample-data.sql +++ b/.devcontainer/sample-data.sql @@ -1,6 +1,6 @@ -- MariaDB dump 10.19 Distrib 10.5.15-MariaDB, for debian-linux-gnu (x86_64) -- --- Host: localhost Database: ieee-money +-- Host: localhost Database: boilerbooks -- ------------------------------------------------------ -- Server version 10.5.15-MariaDB-0+deb11u1 @@ -20,7 +20,7 @@ LOCK TABLES `Budget` WRITE; /*!40000 ALTER TABLE `Budget` DISABLE KEYS */; -INSERT INTO `Budget` VALUES (1,'General Items',100.00,1,8,'Approved'),(2,'Specific Items',100.00,1,8,'Approved'),(3,'General Items',100.00,2,8,'Approved'),(4,'Specific Items',100.00,2,8,'Approved'),(5,'General Items',100.00,3,8,'Approved'),(6,'Specific Items',100.00,3,8,'Approved'),(7,'General Items',100.00,4,8,'Approved'),(8,'Specific Items ',100.00,4,8,'Approved'),(11,'General Items',100.00,6,8,'Submitted'),(12,'Specific Items',100.00,6,8,'Submitted'),(15,'General Items',100.00,7,8,'Submitted'),(16,'Specific Items ',100.00,7,8,'Submitted'),(17,'General Items',100.00,5,8,'Submitted'),(18,'Specific Items',100.00,5,8,'Submitted'); +INSERT INTO `Budget` VALUES (1,'General Items',100.00,2,8,'Approved'),(2,'Specific Items',100.00,2,8,'Approved'),(3,'General Items',100.00,3,8,'Approved'),(4,'Specific Items',100.00,3,8,'Approved'),(5,'General Items',100.00,4,8,'Approved'),(6,'Specific Items',100.00,4,8,'Approved'),(7,'General Items',100.00,5,8,'Approved'),(8,'Specific Items ',100.00,5,8,'Approved'),(11,'General Items',100.00,7,8,'Submitted'),(12,'Specific Items',100.00,7,8,'Submitted'),(15,'General Items',100.00,8,8,'Submitted'),(16,'Specific Items ',100.00,8,8,'Submitted'),(17,'General Items',100.00,56,8,'Submitted'),(18,'Specific Items',100.00,6,8,'Submitted'); /*!40000 ALTER TABLE `Budget` ENABLE KEYS */; UNLOCK TABLES; @@ -50,7 +50,7 @@ UNLOCK TABLES; LOCK TABLES `Purchases` WRITE; /*!40000 ALTER TABLE `Purchases` DISABLE KEYS */; -INSERT INTO `Purchases` VALUES (1,'2022-11-13 06:18:00','pp',NULL,'Test Item 1','General Reason Given','Some Vendor',1,'General Items',NULL,20.00,'Denied',NULL,NULL,'','Pick-up',8),(2,'2022-11-13 06:07:27','pp',NULL,'Test Item 2','Specific Reason Given','A different vendor',1,'Specific Items',NULL,4.60,'Requested',NULL,NULL,'','Mailed',8),(3,'2022-11-13 06:30:00','pp',NULL,'Technical Items','Creating project components','Another vendor',2,'General Items',NULL,43.23,'Approved','trainboi','Cash','Approver changed funding source','Pick-up',8),(4,'2022-11-13 06:30:31','pp','2022-11-13 00:00:00','Challenging Supplies','Future Preparedness, changed by approver','One more vendor',3,'Specific Items','/receipts/Computer_Society_pp_Challenging_Supplies_4.jpg',10.00,'Purchased','mdma','BOSO','https://www.youtube.com/watch?v=dQw4w9WgXcQ','Mailed',8),(5,'2022-11-13 06:28:28','mdma',NULL,'Paper Products','Supplies for Events','Big Box Store',3,'General Items',NULL,30.00,'Denied','mdma','BOSO','','Mailed',8),(6,'2023-03-31 01:45:10','mdma','/receipts/MTT-S_trainboi_A_pile_of_wires_6.jpg','A pile of wires','To build radio equipment','The Wire Store',5,'General Items',NULL,25.76,'Processing Reimbursement','trainboi','SOGA','','Mailed',8); +INSERT INTO `Purchases` VALUES (1,'2022-11-13 06:18:00','pp',NULL,'Test Item 1','General Reason Given','Some Vendor',2,'General Items',NULL,20.00,'Denied',NULL,NULL,'','Pick-up',8),(2,'2022-11-13 06:07:27','pp',NULL,'Test Item 2','Specific Reason Given','A different vendor',2,'Specific Items',NULL,4.60,'Requested',NULL,NULL,'','Mailed',8),(3,'2022-11-13 06:30:00','pp',NULL,'Technical Items','Creating project components','Another vendor',3,'General Items',NULL,43.23,'Approved','trainboi','Cash','Approver changed funding source','Pick-up',8),(4,'2022-11-13 06:30:31','pp','2022-11-13 00:00:00','Challenging Supplies','Future Preparedness, changed by approver','One more vendor',4,'Specific Items','/receipts/Computer_Society_pp_Challenging_Supplies_4.jpg',10.00,'Purchased','mdma','BOSO','https://www.youtube.com/watch?v=dQw4w9WgXcQ','Mailed',8),(5,'2022-11-13 06:28:28','mdma',NULL,'Paper Products','Supplies for Events','Big Box Store',4,'General Items',NULL,30.00,'Denied','mdma','BOSO','','Mailed',8),(6,'2023-03-31 01:45:10','mdma','/receipts/MTT-S_trainboi_A_pile_of_wires_6.jpg','A pile of wires','To build radio equipment','The Wire Store',6,'General Items',NULL,25.76,'Processing Reimbursement','trainboi','SOGA','','Mailed',8); /*!40000 ALTER TABLE `Purchases` ENABLE KEYS */; UNLOCK TABLES; @@ -70,7 +70,7 @@ UNLOCK TABLES; LOCK TABLES `approval` WRITE; /*!40000 ALTER TABLE `approval` DISABLE KEYS */; -INSERT INTO `approval` VALUES (2,'pain','Treasurer',1,1000000,'*',6),(3,'pain','Treasurer',2,0,'*',6),(4,'pain','Treasurer',3,0,'*',6),(5,'pain','Treasurer',4,0,'*',6),(6,'pain','Treasurer',5,0,'*',6),(7,'pain','Treasurer',6,0,'*',6),(8,'pain','Treasurer',7,0,'*',6),(9,'pain','Treasurer',8,0,'*',6),(10,'mdma','Committee Chair',3,1000000,'*',4),(11,'trainboi','Project Lead',2,100,'*',2); +INSERT INTO `approval` VALUES (2,'pain','Treasurer',2,1000000,'*',6),(3,'pain','Treasurer',3,0,'*',6),(4,'pain','Treasurer',4,0,'*',6),(5,'pain','Treasurer',5,0,'*',6),(6,'pain','Treasurer',6,0,'*',6),(7,'pain','Treasurer',7,0,'*',6),(8,'pain','Treasurer',8,0,'*',6),(9,'pain','Treasurer',9,0,'*',6),(10,'mdma','Committee Chair',4,1000000,'*',4),(11,'trainboi','Project Lead',3,100,'*',2); /*!40000 ALTER TABLE `approval` ENABLE KEYS */; UNLOCK TABLES; @@ -92,7 +92,7 @@ UNLOCK TABLES; LOCK TABLES `committees` WRITE; /*!40000 ALTER TABLE `committees` DISABLE KEYS */; -INSERT INTO `committees` VALUES (2,'Aerial Robotics','aerial','Active','Active'),(3,'Computer Society','csociety','Active','Active'),(4,'EMBS','embs','Active','Active'),(5,'MTT-S','mtt-s','Active','Active'),(6,'Racing','racing','Active','Active'),(7,'ROV','rov','Active','Active'),(8,'SOGA','soga','Active','Inactive'),(9,'Growth & Engagement','ge','Inactive','Active'),(10,'Learning','learning','Inactive','Active'),(11,'Social','social','Inactive','Active'),(12,'Software Saturdays','swsat','Inactive','Active'); +INSERT INTO `committees` VALUES (2,'General IEEE','general','Active','Active'),(3,'Aerial Robotics','aerial','Active','Active'),(4,'Computer Society','csociety','Active','Active'),(5,'EMBS','embs','Active','Active'),(6,'MTT-S','mtt-s','Active','Active'),(7,'Racing','racing','Active','Active'),(8,'ROV','rov','Active','Active'),(9,'SOGA','soga','Active','Inactive'),(10,'Growth & Engagement','ge','Inactive','Active'),(11,'Learning','learning','Inactive','Active'),(12,'Social','social','Inactive','Active'),(13,'Software Saturdays','swsat','Inactive','Active'); /*!40000 ALTER TABLE `committees` ENABLE KEYS */; UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; diff --git a/.devcontainer/sql-presetup.sql b/.devcontainer/sql-presetup.sql new file mode 100644 index 00000000..045a42b3 --- /dev/null +++ b/.devcontainer/sql-presetup.sql @@ -0,0 +1,5 @@ +-- Create a user for the database, but only for development setups + +CREATE USER 'boilerbooks'@'localhost' IDENTIFIED BY 'testpassword'; +GRANT INSERT,UPDATE,DELETE,SELECT ON `boilerbooks`.* TO 'boilerbooks'@'localhost'; +FLUSH PRIVILEGES; diff --git a/README.md b/README.md index 042c0517..4d4bc53f 100755 --- a/README.md +++ b/README.md @@ -1,19 +1,28 @@ # Boiler Books -The ultimate IEEE financial recordkeeping system! Boiler Books is used to track income, expenses, dues, and more across all of Purdue IEEE. Originally written with PHP in 2016, it has since been rewritten in JavaScript in 2022. +The ultimate student organization financial recordkeeping system! +Boiler Books is used to track income, expenses, dues, and more across an entire club. +Originally written with PHP in 2016, it has since been rewritten in JavaScript in 2022. This system has three components: the database, the API, and the UI. The database is a standard MySQL installation, the API is an Express server, and the UI is a Vue SPA. -Boiler Books is hosted at [money.purdueieee.org](https://money.purdueieee.org). +## Public Demos -## Documentation +A public demo Boiler Books instance _will_ be hosted at [fake-money.purdueieee.org](https://fake-money.purdueieee.org). +This demo will reset itself every 12 hours. -All the documentation is available in the `docs/` folder, broken up by topic. For a full reference of server infrastructure, refer to the **Purdue IEEE Infrastructure Guide** stored with the Purdue IEEE president. +The following usernames / passwords will be active for the demo instance: -* Updating the Fiscal Year to match the current one: [updating_fiscalyear.md](docs/updating_fiscalyear.md) -* Setting up a development environment: [development.md](docs/development.md) -* Deploying app for production: [deployment.md](docs/deployment.md) -* API endpoint documentation: [api_endpoint.md](docs/api_endpoints.md) +* username / password / role +* master / password / Admin Account +* treas / password / Treasurer +* officer / password / Officer +* internal / password / Internal Leader +* member / password / Regular Member + +
+ +Purdue IEEE's Boiler Books instance is hosted at [money.purdueieee.org](https://money.purdueieee.org). ## Features @@ -27,4 +36,35 @@ All users can see the dues they have paid thus far and officers can view all due Officers must submit budgets to the treasurer who can approve the budget request for the current fiscal year. Once approved, any user can create purchase requests. -The treasurer can add and remove authorized committee members, officers, and other treasurers from the permission list. +The treasurer or admin can add and remove authorized committee members, officers, and other treasurers from the permission list. + +The treasurer or admin can create new committees and fiscal years right from the interface. + +## Documentation + +All the documentation is available in the `docs/` folder, broken up by topic. + +* Updating the Fiscal Year to match the current one: [updating_fiscalyear.md](docs/updating_fiscalyear.md) +* Setting up a development environment: [development.md](docs/development.md) +* Deploying app for production: [deployment.md](docs/deployment.md) +* API endpoint documentation: [api_endpoint.md](docs/api_endpoints.md) + +## Before You Deploy + +The prebuilt UI Docker image expects you to be using the Purdue IEEE SSO system. +Because the constants are baked in at build time, if you are not utilizing this system you will need to build the image from scratch after modifications. + +Also, the committee with ID #2 is assumed to be a general fund. Treasurers will only be assigned purchase approval powers on this general fund. + +### Docker Image Tag Policy + +We tag and maintain a few series of Docker image tags: +* `latest`, `2.3`, `2` : latest release +* `master`, `master-dev` (UI only) : latest master commit + +The Boiler Books release cadence is around 2 times a year. +If there is an urgent fix you need, the `master` tags on the prebuilt Docker images are always built from the latest master. + +## Support + +If you encounter a bug or want to suggest a new feature, create a [new issue](https://github.com/PurdueIEEE/boilerbooks/issues/new). If you have an urgent problem, reach out to our infrastructure team via email at [ieee-infrastructure@purdueieeeorg](mailto:ieee-infrastructure@purdueieee.org). diff --git a/api/.env.git b/api/.env.git index abe2ced8..65855db9 100644 --- a/api/.env.git +++ b/api/.env.git @@ -20,6 +20,7 @@ SEND_MAIL=yes # Details for the SMTP server designated for emails SMTP_HOST=localhost SMTP_PORT=25 +SMTP_FROM=boilerbooks@example.com # The URL pointing to this server HTTP_HOST=localhost @@ -34,3 +35,13 @@ OIDC_SERVER=https://example.com OIDC_CLIENT_ID=example-client OIDC_CLIENT_SECRET=client-secret-here OIDC_REDIRECT_URI=http://example.com/callback + +# UI settings +UI_NAV_TEXT=Club Name +UI_NAV_IMAGE=/path/to/image.ext +UI_NAV_LINK=https://example.com +# - valid options are 'password', 'oidc' +UI_LOGIN_TYPE=password +UI_LOGIN_OIDC_NAME=Example Provider +# - url for OIDC profile management +UI_LOGIN_OIDC_PROFILE=https://sso.example.com/profile diff --git a/api/package.json b/api/package.json index 29220e96..9a815be8 100644 --- a/api/package.json +++ b/api/package.json @@ -1,7 +1,7 @@ { "name": "boilerbooks-api", - "version": "2.0.0", - "description": "Backend API for the Purdue IEEE boilerbooks accounting software", + "version": "2.2", + "description": "Backend API for the Boiler Books accounting software", "main": "src/server.js", "private": true, "type": "module", diff --git a/api/src/app.js b/api/src/app.js index dcddea1b..6d0dd137 100644 --- a/api/src/app.js +++ b/api/src/app.js @@ -35,6 +35,7 @@ app.use("/access", routes.access); app.use("/dues", routes.dues); app.use("/search", routes.search); app.use("/infra", routes.infra); +app.use("/ui/", routes.ui); // conditionally mount login routes if (process.env.USE_OIDC === "true") { diff --git a/api/src/controllers/oidc.js b/api/src/controllers/oidc.js index de4e1647..1bfebe37 100644 --- a/api/src/controllers/oidc.js +++ b/api/src/controllers/oidc.js @@ -231,7 +231,7 @@ async function post_oidc_register(req, res, next) { // Username already exists (should not ever get hit) if (err.code === "ER_DUP_ENTRY") { logger.error("DUPLICATE USERNAME/EMAIL:" + req.body.uname); - res.status(400).send("Unexpected Error: Please contact Purdue IEEE!"); + res.status(400).send("Unexpected Error: Please contact the system administrator!"); return next(); } else { logger.error(err.stack); diff --git a/api/src/middleware/checkAPI.js b/api/src/middleware/checkAPI.js index 5b63f5e8..a7a1aa1b 100644 --- a/api/src/middleware/checkAPI.js +++ b/api/src/middleware/checkAPI.js @@ -17,9 +17,20 @@ import Models from "../models/index.js"; import { logger } from "../utils/logging.js"; +const unprivileged_endpoints = ["/login", "/oidc", "/ui"]; + +function is_endpoint_protected(url) { + for (let endpoint of unprivileged_endpoints) { + if (url.startsWith(endpoint)) { + return false; + } + } + return true; +} + async function checkAPI(req, res, next) { - // If we are attempting to go to the /, /login, or /oidc endpoints, don't authenticate - if (req.originalUrl.startsWith("/login") || req.originalUrl.startsWith("/oidc") || req.originalUrl == "/") { + // If we are attempting to go to an unprotected endpoint, don't authenticate + if (req.originalUrl === "/" || !is_endpoint_protected(req.originalUrl)) { req.context = {}; next(); } else { diff --git a/api/src/middleware/logging.js b/api/src/middleware/logging.js index f8135f32..2b3a2717 100644 --- a/api/src/middleware/logging.js +++ b/api/src/middleware/logging.js @@ -19,9 +19,15 @@ import { logger } from "../utils/logging.js"; async function apiLogger(req, res, next) { // Log every route and it's result // does not catch invalid API keys + if (req.originalUrl === "/") { return next(); // Don't clutter logs with a key check } + + if (req.originalUrl.startsWith("/ui")) { + return next(); // Don't clutter logs with UI loading calls + } + logger.info(`[${req.context.request_user_id ? req.context.request_user_id : ""}] - Return ${res.statusCode} - "${req.method} ${req.originalUrl}"`); next(); } diff --git a/api/src/models/account.js b/api/src/models/account.js index 4010eef6..4cc13262 100644 --- a/api/src/models/account.js +++ b/api/src/models/account.js @@ -74,6 +74,16 @@ async function getUserTreasurer(user) { ); } +async function getUserTreasurerButExcludeAdmin(user) { + return db_conn.promise().execute( + `SELECT COUNT(U3.username) as validuser FROM Users U3 + INNER JOIN approval A ON U3.username = A.username + WHERE A.privilege_level >= ? AND U3.username = ? + AND A.committee != 1`, + [ACCESS_LEVEL.treasurer, user] + ); +} + async function getUserApprovalCommittees(user) { return db_conn.promise().execute( "SELECT committee FROM approval WHERE username = ? AND privilege_level > ?", @@ -205,6 +215,7 @@ export default { updatePassword, getUserApprovals, getUserTreasurer, + getUserTreasurerButExcludeAdmin, getUserApprovalCommittees, setPasswordResetDetails, checkResetTime, diff --git a/api/src/models/purchase.js b/api/src/models/purchase.js index fd1f1a5c..2f170c20 100644 --- a/api/src/models/purchase.js +++ b/api/src/models/purchase.js @@ -162,11 +162,13 @@ async function getTreasurer(id) { (SELECT CONCAT(U.first, ' ', U.last) FROM Users U WHERE U.username = p.username) purchasedby, (SELECT CONCAT(U2.first, ' ', U2.last) FROM Users U2 WHERE U2.username = p.approvedby) approvedby FROM Purchases p - WHERE p.status in ('Purchased','Processing Reimbursement') - AND ? in ( - SELECT U3.username FROM Users U3 - INNER JOIN approval A ON U3.username = A.username - WHERE (A.privilege_level >= ?)) + WHERE p.status IN ('Purchased','Processing Reimbursement') + AND ? IN ( + SELECT U3.username FROM Users U3 + INNER JOIN approval A ON U3.username = A.username + WHERE (A.privilege_level >= ?) + AND A.committee != 1 + ) ORDER BY p.purchasedate DESC`, [id, ACCESS_LEVEL.treasurer] ); diff --git a/api/src/routes/access.js b/api/src/routes/access.js index 1116edbd..96d7fb84 100644 --- a/api/src/routes/access.js +++ b/api/src/routes/access.js @@ -23,6 +23,10 @@ import { committee_id_to_display_readonly_included } from "../utils/committees.j const router = Router(); +// We assume that committee 2 is a general fund +// so that the Treasurer gets assigned permissions properly +const ASSUMED_GENERAL_COMMITTEE = "2"; + /* Get all treasurers */ @@ -30,7 +34,7 @@ router.get("/treasurers", async(req, res, next) => { try { // first make sure user is actually a treasurer const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); - if (results.validuser === 0) { + if (results[0].validuser === 0) { res.status(200).send([]); return next(); } @@ -51,7 +55,7 @@ router.get("/officers", async(req, res, next) => { try { // first we make sure user is actually a treasurer const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); - if (results.validuser === 0) { + if (results[0].validuser === 0) { res.status(200).send([]); return next(); } @@ -73,7 +77,7 @@ router.get("/internals", async(req, res, next) => { try { // first we make sure user is actually a treasurer const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); - if (results.validuser === 0) { + if (results[0].validuser === 0) { res.status(200).send([]); return next(); } @@ -109,15 +113,15 @@ router.post("/treasurers", async(req, res, next) => { try { // first make sure user is actually a treasurer const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); - if (results.validuser === 0) { + if (results[0].validuser === 0) { res.status(200).send("Treasurer added"); // silently failed return next(); } // second verify that user doesn't have approvals already - const [results_1] = await Models.access.checkApprovalExists(req.body.username, "General IEEE", true); + const [results_1] = await Models.access.checkApprovalExists(req.body.username, ASSUMED_GENERAL_COMMITTEE, true); if (results_1[0].approvalexists) { - res.status(400).send("User already has approval powers for General IEEE, please remove them before adding more"); + res.status(400).send("User already has approval powers for the general fund, please remove them before adding more"); return next(); } @@ -132,7 +136,7 @@ router.post("/treasurers", async(req, res, next) => { }; for (let committee in committee_id_to_display_readonly_included) { approval.committee = committee; - if (committee_id_to_display_readonly_included[committee] === "General IEEE") { + if (committee === ASSUMED_GENERAL_COMMITTEE) { approval.amount = 1000000; // if they need more than this we have a problem } await Models.access.addApproval(approval); @@ -179,7 +183,7 @@ router.post("/officers", async(req, res, next) => { try { // first make sure user is actually a treasurer const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); - if (results.validuser === 0) { + if (results[0].validuser === 0) { res.status(200).send("Officer added"); // silently failed return next(); } @@ -243,7 +247,7 @@ router.post("/internals", async(req, res, next) => { try { // first make sure user is actually a treasurer const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); - if (results.validuser === 0) { + if (results[0].validuser === 0) { res.status(200).send("Internal Leader added"); // silently failed return next(); } @@ -286,7 +290,7 @@ router.delete("/approvals/:approverID", async(req, res, next) => { try { // first make sure user is actually a treasurer const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); - if (results.validuser === 0) { + if (results[0].validuser === 0) { res.status(200).send("Access removed"); return next(); } @@ -307,7 +311,7 @@ router.delete("/treasurer/:username", async(req, res, next) => { try { // first make sure user is actually a treasurer const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); - if (results.validuser === 0) { + if (results[0].validuser === 0) { res.status(200).send("Access removed"); return next(); } diff --git a/api/src/routes/account.js b/api/src/routes/account.js index 86249fbb..bbe09813 100644 --- a/api/src/routes/account.js +++ b/api/src/routes/account.js @@ -33,7 +33,7 @@ const bcrypt_rounds = 10; */ router.get("/:userID", async(req, res, next) => { try { - const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); + const [results] = await Models.account.getUserTreasurerButExcludeAdmin(req.context.request_user_id); if (results[0].validuser === 0 && req.context.request_user_id !== req.params.userID) { res.status(404).send("User not found"); return next(); @@ -159,11 +159,11 @@ router.post("/:userID", (req, res, next) => { subject: "Boiler Books Password Changed", text: "Your Boiler Books password was recently changed.\n" + "If you made this request, you can safely ignore this message.\n" + - "Otherwise, please reach out to IEEE.\n\n" + + "Otherwise, please reach out to the system administrator.\n\n" + "This email was automatically sent by Boiler Books", html: `

Your Boiler Books password was recently changed.

If you made this request, you can safely ignore this message.

-

Otherwise, please reach out to IEEE.

+

Otherwise, please reach out to the system administrator.


This email was automatically sent by Boiler Books`, }); diff --git a/api/src/routes/budgets.js b/api/src/routes/budgets.js index e0f20514..ad0e4e9b 100644 --- a/api/src/routes/budgets.js +++ b/api/src/routes/budgets.js @@ -143,7 +143,7 @@ router.put("/:comm", async(req, res, next) => { try { // first we make sure user is actually a treasurer - const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); + const [results] = await Models.account.getUserTreasurerButExcludeAdmin(req.context.request_user_id); if (results[0].validuser === 0) { res.status(200).send("Approved Budget"); return next(); @@ -175,7 +175,7 @@ router.put("/:comm", async(req, res, next) => { router.get("/submitted", async(req, res, next) => { try { // first we make sure user is actually a treasurer - const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); + const [results] = await Models.account.getUserTreasurerButExcludeAdmin(req.context.request_user_id); if (results[0].validuser === 0) { res.status(200).send({}); return next(); diff --git a/api/src/routes/dues.js b/api/src/routes/dues.js index 5555d5c2..23bbbdfc 100644 --- a/api/src/routes/dues.js +++ b/api/src/routes/dues.js @@ -146,7 +146,7 @@ router.put("/:duesid", async(req, res, next) => { try { // check the user is a treasurer - const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); + const [results] = await Models.account.getUserTreasurerButExcludeAdmin(req.context.request_user_id); if (results[0].validuser === 0) { res.status(200).send("Member status updated"); // silently fail on no authorization return next(); @@ -184,7 +184,7 @@ router.put("/:duesid", async(req, res, next) => { try { // first make sure user is actually a treasurer - const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); + const [results] = await Models.account.getUserTreasurerButExcludeAdmin(req.context.request_user_id); if (results[0].validuser === 0) { res.status(200).send("Member details updated"); // Silently fail on no authorization return next(); @@ -297,7 +297,7 @@ router.get("/income/:year", async(req, res, next) => { try { // check the user is a treasurer - const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); + const [results] = await Models.account.getUserTreasurerButExcludeAdmin(req.context.request_user_id); if (results[0].validuser === 0) { res.status(200).send([]); // silently failed on no authorization return next(); @@ -324,7 +324,7 @@ router.get("/expected/:year", async(req, res, next) => { try { // check the user is a treasurer - const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); + const [results] = await Models.account.getUserTreasurerButExcludeAdmin(req.context.request_user_id); if (results[0].validuser === 0) { res.status(200).send({total:0,}); // silently failed on no authorization return next(); diff --git a/api/src/routes/income.js b/api/src/routes/income.js index ecc10d74..5b057afc 100644 --- a/api/src/routes/income.js +++ b/api/src/routes/income.js @@ -143,7 +143,7 @@ router.post("/", async(req, res, next) => { router.get("/", async(req, res, next) => { // Check that user is treasurer try { - const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); + const [results] = await Models.account.getUserTreasurerButExcludeAdmin(req.context.request_user_id); if (results[0].validuser === 0) { res.status(200).send([]); return next(); @@ -207,7 +207,7 @@ router.put("/:incomeID", async(req, res, next) => { // Check that user is treasurer try { - const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); + const [results] = await Models.account.getUserTreasurerButExcludeAdmin(req.context.request_user_id); if (results[0].validuser === 0) { res.status(404).send("Income not found"); return next(); diff --git a/api/src/routes/index.js b/api/src/routes/index.js index 0e531e11..e98b7501 100644 --- a/api/src/routes/index.js +++ b/api/src/routes/index.js @@ -26,6 +26,7 @@ import dues from "./dues.js"; import oidc from "./oidc.js"; import search from "./search.js"; import infra from "./infra.js"; +import ui from "./ui.js"; export default { account, @@ -40,4 +41,5 @@ export default { oidc, search, infra, + ui, }; diff --git a/api/src/routes/infra.js b/api/src/routes/infra.js index 25039efc..0945de3b 100644 --- a/api/src/routes/infra.js +++ b/api/src/routes/infra.js @@ -31,7 +31,7 @@ router.get("/committees", async(req, res, next) => { try { // first make sure user is actually a treasurer const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); - if (results.validuser === 0) { + if (results[0].validuser === 0) { res.status(200).send([]); return next(); } @@ -78,7 +78,7 @@ router.post("/committees", async(req, res, next) => { try { // first make sure user is actually a treasurer const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); - if (results.validuser === 0) { + if (results[0].validuser === 0) { res.status(201).send("Saved committee details"); return next(); } @@ -162,7 +162,7 @@ router.put("/committees/:commID", async(req, res, next) => { // first make sure user is actually a treasurer const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); - if (results.validuser === 0) { + if (results[0].validuser === 0) { res.status(200).send("Saved committee details"); return next(); } @@ -198,7 +198,7 @@ router.get("/fiscal", async(req, res, next) => { try { // first make sure user is actually a treasurer const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); - if (results.validuser === 0) { + if (results[0].validuser === 0) { res.status(200).send({}); return next(); } @@ -245,7 +245,7 @@ router.post("/fiscal", async(req, res, next) => { try { // first make sure user is actually a treasurer const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); - if (results.validuser === 0) { + if (results[0].validuser === 0) { res.status(201).send("Created new fiscal year"); return next(); } @@ -274,7 +274,7 @@ router.get("/fiscal", async(req, res, next) => { try { // first make sure user is actually a treasurer const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); - if (results.validuser === 0) { + if (results[0].validuser === 0) { res.status(200).send({}); return next(); } @@ -333,7 +333,7 @@ router.post("/fiscal", async(req, res, next) => { try { // first make sure user is actually a treasurer const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); - if (results.validuser === 0) { + if (results[0].validuser === 0) { res.status(201).send("Created new fiscal year"); return next(); } diff --git a/api/src/routes/login.js b/api/src/routes/login.js index f6b42bd5..ad4ee2a7 100644 --- a/api/src/routes/login.js +++ b/api/src/routes/login.js @@ -375,11 +375,11 @@ router.post("/reset", async(req, res, next) => { subject: "Boiler Books Password Reset", text: "Your Boiler Books password was reset.\n" + "If you made this change, you can safely ignore this message.\n" + - "Otherwise, please reach out to IEEE.\n\n" + + "Otherwise, please reach out to the system administrator.\n\n" + "This email was automatically sent by Boiler Books", html: `

Your Boiler Books password was resent.

If you made this change, you can safely ignore this message.

-

Otherwise, please reach out to IEEE.

+

Otherwise, please reach out to the system administrator.


This email was automatically sent by Boiler Books`, }); diff --git a/api/src/routes/purchase.js b/api/src/routes/purchase.js index 1f0fd369..4e08fe09 100644 --- a/api/src/routes/purchase.js +++ b/api/src/routes/purchase.js @@ -56,7 +56,7 @@ const check_type = ["Pick-up", "Mailed"]; router.get("/", async(req, res, next) => { // Check that user is treasurer try { - const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); + const [results] = await Models.account.getUserTreasurerButExcludeAdmin(req.context.request_user_id); if (results[0].validuser === 0) { res.status(200).send([]); // silently fail on no authorization return next(); @@ -194,12 +194,12 @@ router.post("/", async(req, res, next) => { subject: `New Purchase Request for ${committee_id_to_display[req.body.committee]}`, text: `A request was made by ${cleanUTF8(req.body.user)} for ${cleanUTF8(req.body.item)} costing $${req.body.price}\n` + "Please visit Boiler Books at your earliest convenience to approve or deny the request.\n" + - `You always view the most up-to-date status of the purchase at https://money.purdueieee.org/ui/detail-view?id=${insert_id}.\n\n` + + `You always view the most up-to-date status of the purchase at https://${process.env.HTTP_HOST}/ui/detail-view?id=${insert_id}.\n\n` + "This email was automatically sent by Boiler Books", html: `

New Purchase Request!

A request was made by ${req.body.user} for ${req.body.item} costing $${req.body.price}.

-

Please visit Boiler Books at your earliest convenience to approve or deny the request.

-

You always view the most up-to-date status of the purchase here.

+

Please visit Boiler Books at your earliest convenience to approve or deny the request.

+

You always view the most up-to-date status of the purchase here.


This email was automatically sent by Boiler Books`, }); @@ -231,7 +231,7 @@ router.post("/treasurer", async(req, res, next) => { // Check that user is treasurer try { - const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); + const [results] = await Models.account.getUserTreasurerButExcludeAdmin(req.context.request_user_id); if (results[0].validuser === 0) { res.status(200).send("Purchase(s) updated"); // silently fail on no authorization return next(); @@ -423,7 +423,7 @@ router.put("/:purchaseID", async(req,res, next) => { try { // check the user is a treasurer - const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); + const [results] = await Models.account.getUserTreasurerButExcludeAdmin(req.context.request_user_id); if (results[0].validuser === 0) { res.status(200).send("Updated Purchase"); return next(); @@ -483,7 +483,7 @@ router.delete("/:purchaseID", async(req, res, next) => { router.post("/:purchaseID/expire", async(req, res, next) => { // check that the user is a treasurer and the purchase is valid try { - const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); + const [results] = await Models.account.getUserTreasurerButExcludeAdmin(req.context.request_user_id); if (results[0].validuser === 0) { res.status(404).send("Purchase not found"); return next(); @@ -632,7 +632,7 @@ router.post("/:purchaseID/approve", async(req, res, next) => { "This email was automatically sent by Boiler Books", html: `

Your Purchase Request Was ${purchase_deets[0].status}

Your request to buy ${purchase_deets[0].item} for ${committee_id_to_display_readonly_included[purchase_deets[0].committee]} was ${purchase_deets[0].status}

-

Please visit Boiler Books at your earliest convenience to complete the request.

+

Please visit Boiler Books at your earliest convenience to complete the request.

You always view the most up-to-date status of the purchase here.


This email was automatically sent by Boiler Books`, @@ -809,7 +809,7 @@ router.post("/:purchaseID/complete", fileHandler.single("receipt"), async(req, r `You always view the most up-to-date status of the purchase at https://${process.env.HTTP_HOST}/ui/detail-view?id=${req.params.purchaseID}.\n\n` + "This email was automatically sent by Boiler Books", html: `

${committee_id_to_display[purchase_deets[0].committee]} has purchased ${purchase_deets[0].item} for $${purchase_deets[0].cost}

-

Please visit Boiler Books at your earliest convenience to begin the reimbursement process.

+

Please visit Boiler Books at your earliest convenience to begin the reimbursement process.

You always view the most up-to-date status of the purchase here.


This email was automatically sent by Boiler Books`, @@ -836,7 +836,7 @@ router.post("/:purchaseID/receipt", fileHandler.single("receipt"), async(req, re } try { - const [results] = await Models.account.getUserTreasurer(req.context.request_user_id); + const [results] = await Models.account.getUserTreasurerButExcludeAdmin(req.context.request_user_id); if (results[0].validuser === 0) { fs.unlink(req.file.path); res.status(404).send("Purchase not found"); diff --git a/api/src/routes/ui.js b/api/src/routes/ui.js new file mode 100644 index 00000000..3e74180d --- /dev/null +++ b/api/src/routes/ui.js @@ -0,0 +1,58 @@ +/* + Copyright 2022 Purdue IEEE and Hadi Ahmed + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { Router } from "express"; +import { logger } from "../utils/logging.js"; + +const router = Router(); + +router.get("/text", (req, res, next) => { + res.status(200).send(process.env.UI_NAV_TEXT); + next(); +}); + +router.get("/image", (req, res, next) => { + res.status(200).sendFile(process.env.UI_NAV_IMAGE, (err) => { + if (err) { + if (!res.headersSent) { + res.status(500).send("Internal Server Error"); + logger.error(err.message); + } + } + next(); + }); +}); + +router.get("/link", (req, res, next) => { + res.status(200).send(process.env.UI_NAV_LINK); + next(); +}); + +router.get("/login", (req, res, next) => { + const login_details = { + "type": process.env.UI_LOGIN_TYPE, + }; + + if (login_details.type === "oidc") { + login_details.oidc_name = process.env.UI_LOGIN_OIDC_NAME; + login_details.oidc_profile = process.env.UI_LOGIN_OIDC_PROFILE; + } + + res.status(200).send(login_details); + next(); +}); + +export default router; diff --git a/api/src/utils/mailer.js b/api/src/utils/mailer.js index 8705b0dd..556547d3 100644 --- a/api/src/utils/mailer.js +++ b/api/src/utils/mailer.js @@ -24,7 +24,7 @@ const mailer = nodemailer.createTransport({ secure: false, ignoreTLS: true, },{ - from: "Boiler Books ", + from: `Boiler Books <${process.env.SMTP_FROM}>`, }); const MAX_TRIES = 5; diff --git a/config/ieee-money.sql b/config/boilerbooks.sql similarity index 97% rename from config/ieee-money.sql rename to config/boilerbooks.sql index 0d8f1d43..7ee683e8 100644 --- a/config/ieee-money.sql +++ b/config/boilerbooks.sql @@ -4,8 +4,8 @@ START TRANSACTION; SET time_zone = "+00:00"; -- Create Database -CREATE DATABASE IF NOT EXISTS `ieee-money` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; -USE `ieee-money`; +CREATE DATABASE IF NOT EXISTS `boilerbooks` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +USE `boilerbooks`; -- Setup the tables CREATE TABLE `approval` ( diff --git a/config/docker-compose.sample.yml b/config/docker-compose.sample.yml new file mode 100644 index 00000000..57c59f01 --- /dev/null +++ b/config/docker-compose.sample.yml @@ -0,0 +1,69 @@ +version: '3' + +services: + db: + image: mysql:8.0-debian + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: 'superpassword' + MYSQL_DATABASE: 'boilerbooks' + MYSQL_USER: 'boilerbooks' + MYSQL_PASSWORD: 'boilerbooks-password' + volumes: + - ./mysql-data:/var/lib/mysql + api: + restart: unless-stopped + image: ghcr.io/purdueieee/boilerbooks-api:latest + environment: + PORT: 3000 + ACCOUNT_PIN: 'example' + DB_HOST: 'db' + DB_USER: 'boilerbooks' + DB_PASS: 'boilerbooks-password' + DB_DATABASE: 'boilerbooks' + RECEIPT_BASEDIR: '/example/path' + SEND_MAIL: 'yes' + SMTP_HOST: 'localhost' + SMTP_PORT: 25 + SMTP_FROM: 'boilerbooks@example.com' + HTTP_HOST: 'boilerbooks.example.com' + TREAS_EMAIL: 'treasurer@example.com' + USE_OIDC: 'true' + SESSION_SECRET: 'supersecret' + OIDC_SERVER: 'https://sso.example.com' + OIDC_CLIENT_ID: 'boilerbooks' + OIDC_CLIENT_SECRET: 'anothersecret' + OIDC_REDIRECT_URI: 'https://boilerbooks.example.com/api/v2/oidc/callback' + UI_NAV_TEXT: 'Club Name' + UI_NAV_IMAGE: '/path/to/image.ext' + UI_NAV_LINK: 'https://example.com' + UI_LOGIN_TYPE: 'password' + UI_LOGIN_OIDC_NAME: 'Example Provider' + UI_LOGIN_OIDC_PROFILE: 'https://sso.example.org/profile' + volumes: + - ./receipts:/srv/receipts + - ./assets:/srv/assets + - ./logs:/var/log/boilerbooks + depends_on: + - db + ui_proxy: + restart: unless-stopped + image: ghcr.io/purdueieee/boilerbooks-ui:latest + ports: + - "80:80" + volumes: + - ./boilerbooks/config/nginx-prod.conf:/etc/nginx/nginx.conf + depends_on: + - api + - db + - pma + pma: + image: phpmyadmin:5-apache + restart: unless-stopped + volumes: + - ./phpMyAdmin/config.user.inc.php:/etc/phpmyadmin/config.user.inc.php + environment: + PMA_ABSOLUTE_URI: https://boilerbooks.example.com/pma/ + PMA_HOST: db + depends_on: + - db diff --git a/config/sql-setup.sql b/config/sql-setup.sql index 3462bf33..291547dc 100644 --- a/config/sql-setup.sql +++ b/config/sql-setup.sql @@ -1,10 +1,9 @@ -CREATE USER 'boilerbooks'@'localhost' IDENTIFIED BY 'testpassword'; -GRANT INSERT,UPDATE,DELETE,SELECT ON `ieee-money`.* TO 'boilerbooks'@'localhost'; -FLUSH PRIVILEGES; +-- Create a master user +INSERT INTO `boilerbooks`.`Users` (first, last, email, address, city, state, zip, cert, username, password, passwordreset, apikey) + VALUES ('Infrastructure', 'Account', 'ieee-infrastructure@purdueieee.org', '465 Northwestern Ave', 'West Lafayette', 'IN', '47907', '', 'master', '$2b$10$2SwcYOGA2ltZqSZXMO3r0OunMr.Ff04rU0Hg7PxcUhCz1qZFwv2.W', '', ''); -INSERT INTO `ieee-money`.`Users` (first, last, email, address, city, state, zip, cert, username, password, passwordreset, apikey) - VALUES ('Purdue', 'IEEE', 'ieee-infrastructure@purdueieee.org', '465 Northwestern Ave', 'West Lafayette', 'IN', '47907', '', 'pieee', '$2b$10$2SwcYOGA2ltZqSZXMO3r0OunMr.Ff04rU0Hg7PxcUhCz1qZFwv2.W', '', ''); +-- Create an "Admin" committee +INSERT INTO `boilerbooks`.`committees` (display_name, api_name, bank_status, dues_status) VALUES ('Infrastructure', 'general', 'Inactive', 'Inactive'); -INSERT INTO `ieee-money`.`committees` (display_name, api_name, bank_status, dues_status) VALUES ('General IEEE', 'general', 'Active', 'Inactive'); - -INSERT INTO `ieee-money`.`approval` (username, role, committee, amount, category, privilege_level) VALUES ('pieee', 'Master Account', 1, '0', '*', '6'); +-- Grant the master user permissions on "Admin" committee" +INSERT INTO `boilerbooks`.`approval` (username, role, committee, amount, category, privilege_level) VALUES ('master', 'Master Account', 1, '0', '*', '6'); diff --git a/config/systemd-prod.service b/config/systemd-prod.service index 1aeaccbc..df2f76f4 100644 --- a/config/systemd-prod.service +++ b/config/systemd-prod.service @@ -1,5 +1,5 @@ [Unit] -Description=Purdue IEEE Boiler Books +Description=Boiler Books Documentation=https://github.com/PurdueIEEE/boilerbooks After=network.target diff --git a/docs/api_endpoints.md b/docs/api_endpoints.md index c87148d6..5a118ba3 100644 --- a/docs/api_endpoints.md +++ b/docs/api_endpoints.md @@ -1,5 +1,7 @@ # v2 API Design +**This is really out of date** + ## Note All actions should be able to be completed using only the API. In BBv1, actions are performed from the API and from the UI. To create a separable web application, all UI code should be decoupled from the API. diff --git a/docs/deployment.md b/docs/deployment.md index 54faeb37..09da2124 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -2,7 +2,10 @@ Boiler Books can be used with SystemD to manage processes. A .service file is provided as an example. -Boiler Books can also be deployed using docker-compose to network the db, api, and ui/proxy services. +**\[Preferred\]** Boiler Books can also be deployed with Docker. There are prebuilt Docker containers or build from scratch. + +If using Docker containers, You will need to first use the .sql files in the `config/` directory to properly initialize the database. +The API _will_ fail to start without this setup. ## Deploying with SystemD (or any process manager) @@ -10,33 +13,43 @@ Boiler Books can also be deployed using docker-compose to network the db, api, a * The UI should be in `/srv/boilerbooks/ui` * The API should be in `/srv/boilerbooks/api` 2. Copy the .service file: `cp config/systemd-prod.service /lib/systemd/system/boilerbooks.service` -2. Start the service with `systemctl start boilerbooks.service` 3. Install the service with `systemctl enable boilerbooks.service` +4. Start the service with `systemctl start boilerbooks.service` The deployment can be restarted with `systemctl restart boilerbooks.service`: -## Deploying with Docker-Compoe - -* Create a `docker-compose.yml` file and setup 4 (or 3) services: - * `ui_proxy` for the frontend and internal reverse proxy - * `api` for the actual API - * `db` for the database, probably MySQL - * (optional) `pma` for PHPMyAdmin -* Point the `ui_proxy` and `api` services to build off the given Dockerfiles - * The API uses [api/Dockerfile](/api/Dockerfile) - * The UI can use [ui/Dockerfile.dev](/ui/Dockerfile.dev) for developments builds - * The UI can use [ui/Dockerfile.prod](/ui/Dockerfile.prod) for production builds -* Set the environment variables for the api as defined in [the dotenv file](/api/.env.git) -* Bind mount the necessary logs, database volumes, etc. to the host machine - * Optionally, use Docker volumes instead -* Setup a network link for the SMTP server to the Docker gateway -* Run the command `docker-compose up --build -d` +## Deploying with Docker (from scratch) + +* Copy the sample `docker-compose.sample.yml` file from the `config/` directory to the root deployment folder +* Modify the file to fit your details, including environment variables + * The PHPMyAdmin container is completely optional, and does not need to be included +* Replace the `image:` labels with `build:` labels, pointing to the proper Dockerfiles. An example is under this section + * You can optionally chose to only build certain components, like the UI +* Run the command `docker-compose up -d --build` + +docker-compose.yml replacement example: +```yaml +services: + api: + build: boilerbooks/api + ui: + build: + context: boilerbooks/ui + dockerfile: Dockerfile.prod +``` + +## **\[Preferred\]** Deploying with Docker (prebuilt containers) + +* Copy the sample `docker-compose.sample.yml` file from the `config/` directory to the root deployment folder +* Modify the file to fit your details, including environment variables, image tags, etc. + * The PHPMyAdmin container is completely optional, and does not need to be included +* Run the command `docker-compose up -d` ## IEEE Deploy Information ### Deployment -Deployment is handled with docker-compose and uses the Dockerfiles provided. +Deployment is handled with docker-compose and uses the prebuilt containers. **GitHub Actions will automatically auto-deploy the production application after a push to the master branch.** @@ -48,16 +61,17 @@ To manually redeploy the application, ssh to the server and cd to the service fo Run a few commands to redeploy the application: -``` +```sh cd boilerbooks git pull cd .. -docker-compose up --build -d +docker-compose pull +docker-compose up -d ``` ### Backups -Backups occur weekly and are uploaded automatically to alternate storage. +Backups occur daily and are downloaded automatically to alternate storage. ### SSL diff --git a/docs/development.md b/docs/development.md index 30270189..82a17f17 100644 --- a/docs/development.md +++ b/docs/development.md @@ -6,7 +6,7 @@ The preferred development environment uses Visual Studio Code devcontainers. How ## Important Notes -By default, a bootstrap account is created with the username `pieee` and the password `test`. +By default, a bootstrap account is created with the username `master` and the password `test`. This account has Treasurer permissions and should be used to grant permissions to your actual testing accounts. While this is a local testing platform, changing the default password is highly recommended. @@ -20,7 +20,6 @@ The accounts are: * `mdma` (Mitch Daniels) - `qwertyuiop` - officer (Computer Society) * `pain` (ECE Department) - `qwertyuiop` - treasurer - ## Developing with Visual Studio Code \[Preferred\] Visual Studio Code (VSCode) has a powerful feature called "devcontainers" that allows a seamless and repeatable development environment setup process. @@ -41,6 +40,12 @@ Somewhere on screen, there will be a popup asking to reopen the workspace in a c Accept this, and wait 30 seconds to 3 minutes for all installation and setup instructions. From then, open a split terminal and start both the API and UI servers. +> If you are re-opening the development environment, you may need to first run a startup command: +> +> `.devcontainer/postCreateCommand.sh` +> +> Creating a new devcontainer will automatically run this command. + ```sh cd api npm run serve @@ -71,12 +76,13 @@ cd boilerbooks apt install nginx mysql-server-8.0 nodejs postfix -mysql < config/ieee-money.sql +mysql < config/boilerbooks.sql +mysql < .devcontainer/sql-presetup.sql mysql < config/sql-setup.sql mysql < .devcontainer/sample-data.sql cp config/nginx-dev.conf /etc/nginx/sites-available/ -ln -s /etc/nginx/sites-available/nginx-dev.conf /etc/nginx/sites-enabled/ieee-money-dev.conf +ln -s /etc/nginx/sites-available/nginx-dev.conf /etc/nginx/sites-enabled/boilerbooks-dev.conf service nginx reload mkdir /var/log/boilerbooks diff --git a/ui/package.json b/ui/package.json index b2ee1135..72eaef33 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,7 @@ { "name": "boilerbooks-ui", - "version": "2.0.0", + "version": "2.2", + "description": "Frontend UI for the Boiler Books accounting software", "private": true, "scripts": { "vstring": "echo VITE_VERSION_STRING=$(git describe --tags --abbrev=0)-$(git rev-parse --abbrev-ref HEAD)-$(git rev-parse --short HEAD) > src/environment/.env.local", diff --git a/ui/src/App.vue b/ui/src/App.vue index 4c9780e4..6f193f41 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -3,33 +3,32 @@ -
-

View page on GitHub

-

{{version_string}}

-

Copyright © Purdue IEEE
with Hadi Ahmed and Kyle Rakos

+

Boiler Books {{ version_string }}

+

View project on GitHub

+

Copyright © Purdue IEEE
with Hadi Ahmed and Kyle Rakos

@@ -54,13 +53,17 @@ import auth_state from '@/state'; +import { fetchWrapperTXT } from './api_wrapper'; + export default { name:"BoilerBooks", data() { return { auth_state: auth_state.state, dev: import.meta.env.MODE === "dev", - version_string: import.meta.env.VITE_VERSION_STRING + version_string: import.meta.env.VITE_VERSION_STRING, + nav_text: '', + nav_link: '', } }, computed: { @@ -72,18 +75,31 @@ export default { logout() { auth_state.clearAuthState(); // TODO this should invalidate the API key - if (import.meta.env.VITE_USE_OIDC === "true") { - window.location.href = '/api/v2/oidc/logout'; - } else { - this.$router.push('/login'); - } + this.$router.push('/login'); } }, - mounted() { + async mounted() { if (this.dev) { document.getElementById("favicon").href = `${import.meta.env.BASE_URL}dev-favicon.ico`; document.title = "Boiler Books [DEV]" } + + const ui_text = await fetchWrapperTXT('/api/v2/ui/text', { + 'method': 'get' + }); + + const ui_link = await fetchWrapperTXT('/api/v2/ui/link', { + 'method': 'get' + }) + + if (ui_text.error || ui_link.error) { + this.nav_text = "..."; + this.nav_link = "#"; + return; + } + + this.nav_text = ui_text.response; + this.nav_link = ui_link.response; } } diff --git a/ui/src/environment/.env.dev b/ui/src/environment/.env.dev deleted file mode 100644 index 9162047b..00000000 --- a/ui/src/environment/.env.dev +++ /dev/null @@ -1,2 +0,0 @@ -VITE_USE_OIDC=false -VITE_OIDC_ACCOUNT=https://sso.purdueieee.org/realms/Purdue-IEEE/account/#/ diff --git a/ui/src/environment/.env.prod b/ui/src/environment/.env.prod deleted file mode 100644 index c8517c2b..00000000 --- a/ui/src/environment/.env.prod +++ /dev/null @@ -1,2 +0,0 @@ -VITE_USE_OIDC=true -VITE_OIDC_ACCOUNT=https://sso.purdueieee.org/realms/Purdue-IEEE/account/#/ diff --git a/ui/src/environment/.gitkeep b/ui/src/environment/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/ui/src/views/BoilerBooksHelp.vue b/ui/src/views/BoilerBooksHelp.vue index dd34a442..60414585 100644 --- a/ui/src/views/BoilerBooksHelp.vue +++ b/ui/src/views/BoilerBooksHelp.vue @@ -3,53 +3,18 @@

Boiler Books Help

-

Boiler Books is an expense and income tracking system.

+

Boiler Books is a financial management system.

- It was developed on behalf of IEEE to track all purchases for the organization. - The system allows any IEEE member to create an account and request to purchase - something on behalf of the indicated committee. -

-

- In addition to being a convenient system to request and approve purchases, members - also have the ability to view all of their past purchases. Those with the appropriate - permissions are also able to view purchases and income for their entire committee. + It was developed on behalf of Purdue IEEE to track all purchases for the organization. + The system allows members to track purchases, income and donations, dues, and more.

Boiler Books is open source, licensed under the Apache 2.0 license, and under active - development. If there is a feature you would like to see, send us an email at - ieee-officers@purdueieee.org. + development. If there is a feature you would like to see, open a ticket on the + project issue tracker. If you would like to help development, visit our GitHub!

-
-
-

Purchasing Process

-
    -
  1. Any user makes a purchase request, filling out details about the item and some tracking information
  2. -
  3. The committee chair or a designated committee member will review the request and approve or deny it
  4. -
  5. After successfully purchasing the approved item, the purchaser will complete the request and upload a receipt
  6. -
  7. The treasurer will submit the purchase for reimbursement from the Purdue Business Office for Student Organizations
  8. -
-
-
-
-

Income Process

-

- Report Income and Donations using the 'Income' tab. Income is money given - to your committee. Donations are items or discounts that are non-monetary. -

-

- A quick rule-of-thumb: If you can spend it, it is Income and counts towards your balance. - If you can use it, it is a donation and does not count towards your balance. -

-

- Mark income as 'Expected' if it is being sent by an organization. Mark income as 'Credit' if it will be paid later, like as part of a grant. - The Treasurer will modify the status to track the income across it's lifecycle. -

-

- Direct any questions towards the IEEE Treasurer. -

-
@@ -71,19 +36,12 @@ limitations under the License. */ -import auth_state from '@/state'; - export default { name: 'BoilerBooksHelp', data() { return { - auth_state: auth_state.state, + } }, - computed: { - showIncome() { - return this.auth_state.uname !== '' && this.auth_state.viewFinancials; - } - } } diff --git a/ui/src/views/ForgotUserPass.vue b/ui/src/views/ForgotUserPass.vue index 0c4175ec..1c571f87 100644 --- a/ui/src/views/ForgotUserPass.vue +++ b/ui/src/views/ForgotUserPass.vue @@ -2,7 +2,7 @@

Forgot Username

-

Enter the email associated with the account below.
If you don't remember the email you used, please contact IEEE at ieee@purdue.edu for more help.

+

Enter the email associated with the account below.
If you don't remember the email you used, please contact the system administrator for more help.

{{dispmsg}}

diff --git a/ui/src/views/LoginSignup.vue b/ui/src/views/LoginSignup.vue index bd668c10..b44d2dee 100644 --- a/ui/src/views/LoginSignup.vue +++ b/ui/src/views/LoginSignup.vue @@ -1,7 +1,7 @@ diff --git a/ui/src/views/dues/DuesEdit.vue b/ui/src/views/dues/DuesEdit.vue index 9f0974bb..39b45e5f 100644 --- a/ui/src/views/dues/DuesEdit.vue +++ b/ui/src/views/dues/DuesEdit.vue @@ -95,7 +95,7 @@

-
NOTE: 'Exempt' members are those who have an existing International IEEE membership.
+
NOTE: 'Exempt' members are those who do not need to pay dues.
diff --git a/ui/src/views/dues/DuesFrame.vue b/ui/src/views/dues/DuesFrame.vue index 6ccddcd9..252b4ba4 100644 --- a/ui/src/views/dues/DuesFrame.vue +++ b/ui/src/views/dues/DuesFrame.vue @@ -1,6 +1,6 @@ diff --git a/ui/src/views/financials/FinancialsFrame.vue b/ui/src/views/financials/FinancialsFrame.vue index 0c08066f..fe04f9fd 100644 --- a/ui/src/views/financials/FinancialsFrame.vue +++ b/ui/src/views/financials/FinancialsFrame.vue @@ -1,6 +1,6 @@ diff --git a/ui/src/views/oidc/OIDCRegister.vue b/ui/src/views/oidc/OIDCRegister.vue index 6c9c3595..02d91cc8 100644 --- a/ui/src/views/oidc/OIDCRegister.vue +++ b/ui/src/views/oidc/OIDCRegister.vue @@ -38,8 +38,8 @@
- - + +

Caps Lock is on!

diff --git a/ui/src/views/purchase/PurchaseApprove.vue b/ui/src/views/purchase/PurchaseApprove.vue index 26ba3a68..144b14ee 100644 --- a/ui/src/views/purchase/PurchaseApprove.vue +++ b/ui/src/views/purchase/PurchaseApprove.vue @@ -10,7 +10,7 @@

-
Warning! Purchase cost exceeds committee balance. Please talk to the IEEE Treasurer before approving this purchase
+
Warning! Purchase cost exceeds committee balance. Please talk to the Treasurer before approving this purchase
Warning! Committe balance is low!
diff --git a/ui/src/views/purchase/PurchaseNew.vue b/ui/src/views/purchase/PurchaseNew.vue index e1cc08bc..a8fbf15f 100644 --- a/ui/src/views/purchase/PurchaseNew.vue +++ b/ui/src/views/purchase/PurchaseNew.vue @@ -14,8 +14,8 @@
diff --git a/ui/src/views/purchase/PurchaseReimbursements.vue b/ui/src/views/purchase/PurchaseReimbursements.vue index ef10fdef..1cce969f 100644 --- a/ui/src/views/purchase/PurchaseReimbursements.vue +++ b/ui/src/views/purchase/PurchaseReimbursements.vue @@ -1,7 +1,7 @@