├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ ├── combine-prs.yml │ └── node.js.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── api ├── cloud_api │ ├── Dockerfile │ ├── Dockerfile.dev │ ├── email_templates │ │ ├── blastEmail.js │ │ ├── passwordReset.js │ │ ├── test.js │ │ ├── unsubscribeEmail.js │ │ └── verification.js │ ├── routes │ │ └── Mailer.js │ ├── server.js │ └── util │ │ ├── AuthManager.js │ │ ├── SceGoogleApiHandler.js │ │ └── auth.js ├── config │ └── config.example.json ├── devServer.js ├── main_endpoints │ ├── Dockerfile │ ├── Dockerfile.dev │ ├── models │ │ ├── Advertisement.js │ │ ├── OfficeAccessCard.js │ │ └── User.js │ ├── routes │ │ ├── Advertisement.js │ │ ├── Auth.js │ │ ├── Cleezy.js │ │ ├── LedSign.js │ │ ├── Messages.js │ │ ├── OfficeAccessCard.js │ │ ├── Printer.js │ │ ├── Speaker.js │ │ ├── User.js │ │ └── printing │ │ │ └── keep │ ├── server.js │ └── util │ │ ├── LedSign.js │ │ ├── Speaker.js │ │ ├── captcha.js │ │ ├── emailHelpers.js │ │ ├── passport.js │ │ ├── redis-client.js │ │ ├── token-functions.js │ │ └── userHelpers.js ├── package-lock.json ├── package.json └── util │ ├── CreateUserScript.js │ ├── GenerateTokenScript.js │ ├── PathParser.js │ ├── SceHttpServer.js │ ├── constants.js │ ├── logger.js │ ├── metrics.js │ └── token-verification.js ├── docker-compose.dev.yml ├── docker-compose.yml ├── docker ├── Dockerfile ├── Dockerfile.dev └── nginx.conf ├── nginx.conf ├── nginx.dev.conf ├── package-lock.json ├── package.json ├── prometheus └── prometheus.yml ├── public ├── favicon.ico ├── images │ └── SCE-glow.png ├── index.html ├── manifest.json └── output.css ├── setup.py ├── src ├── APIFunctions │ ├── 2DPrinting.js │ ├── Advertisement.js │ ├── ApiResponses.js │ ├── Auth.js │ ├── Cleezy.js │ ├── LedSign.js │ ├── Mailer.js │ ├── Messaging.js │ ├── Profile.js │ ├── Speaker.js │ └── User.js ├── Components │ ├── Background │ │ ├── background.css │ │ └── background.jsx │ ├── Captcha │ │ └── GoogleRecaptcha.js │ ├── DecisionModal │ │ └── ConfirmationModal.js │ ├── Footer │ │ └── Footer.js │ ├── Navbar │ │ ├── AdminNavbar.js │ │ ├── NavBarWrapper.js │ │ └── UserNavbar.js │ └── Routing │ │ └── PrivateRoute.js ├── Enums.js ├── Pages │ ├── 2DPrinting │ │ ├── 2DPrinting.js │ │ └── PageSelectDropdown.js │ ├── About │ │ └── About.js │ ├── Advertisement │ │ └── AdvertisementAdmin.js │ ├── EmailPreferences │ │ └── EmailPreferences.js │ ├── ForgotPassword │ │ ├── ForgotPassword.js │ │ └── ResetPassword.js │ ├── Home │ │ ├── Home.css │ │ └── Home.js │ ├── LedSign │ │ ├── LedSign.js │ │ └── ledsign.css │ ├── Login │ │ ├── Login.js │ │ └── LoginInput.js │ ├── MembershipApplication │ │ ├── ConfirmationPage.js │ │ ├── GetPlans.js │ │ ├── MajorDropdown.js │ │ ├── MembershipApplication.js │ │ ├── MembershipForm.js │ │ └── VerifyEmail.js │ ├── Messaging │ │ └── Messaging.js │ ├── NotFoundPage │ │ ├── NotFoundPage.js │ │ └── NotFoundPics │ │ │ └── empty_room.png │ ├── Overview │ │ ├── Overview.js │ │ ├── PageNumbers.js │ │ └── SVG.js │ ├── Profile │ │ ├── MemberView │ │ │ ├── ChangePassword.js │ │ │ ├── DeleteAccountModal.js │ │ │ ├── GetApiKeyModal.js │ │ │ ├── InfoCard.js │ │ │ ├── Profile.js │ │ │ └── getPicBySeason.js │ │ └── admin │ │ │ └── SendUnsubscribeEmail.js │ ├── Projects │ │ ├── Components │ │ │ └── ProjectCard.js │ │ └── Projects.js │ ├── Speaker │ │ └── Speaker.js │ ├── URLShortener │ │ ├── SVG.js │ │ └── URLShortener.js │ └── UserManager │ │ ├── EditUserInfo.js │ │ ├── ExpirationDropdown.js │ │ └── RoleDropdown.js ├── Routing.js ├── config │ └── config.example.json ├── index.css ├── index.js └── input.css ├── tailwind.config.js ├── test ├── api │ ├── Advertisement.js │ ├── Auth.js │ ├── LedSign.js │ ├── Mailer.js │ ├── Messages.js │ ├── OfficeAccessCard.js │ ├── Speaker.js │ ├── TokenFunctions.js │ ├── User.js │ └── registerUser.js ├── frontend │ ├── AboutPage.test.js │ ├── DecisionModal.test.js │ ├── MessagingModal.test.js │ └── Routing.test.js └── util │ ├── mocks │ ├── Date.js │ ├── DiscordApiFunction.js │ ├── SceSqsApiHandler.js │ ├── TokenValidFunctions.js │ └── react-router-dom.js │ └── tools │ ├── SceApiTester.js │ └── tools.js └── tunnel ├── Dockerfile ├── README.md └── check_tunnel_status.py /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | "@babel/preset-env" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | api/node_modules/ 2 | api/config/ 3 | src/config/ 4 | node_modules/ 5 | build/ 6 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "globals": { 8 | "Atomics": "readonly", 9 | "SharedArrayBuffer": "readonly" 10 | }, 11 | "parserOptions": { 12 | "babelOptions": { 13 | "configFile": "./.babelrc" 14 | }, 15 | "ecmaFeatures": { 16 | "jsx": true 17 | }, 18 | "ecmaVersion": 2018, 19 | "requireConfigFile": "false", 20 | "sourceType": "module" 21 | }, 22 | "parser": "@babel/eslint-parser", 23 | "plugins": [ 24 | "react" 25 | ], 26 | "rules": { 27 | "indent": [ 28 | "error", 29 | 2 30 | ], 31 | "quotes": [ 32 | "error", 33 | "single" 34 | ], 35 | "semi": [ 36 | "error", 37 | "always" 38 | ], 39 | "camelcase": 2, 40 | "comma-spacing": "error", 41 | "no-console": [ 42 | "error", 43 | { 44 | "allow": [ 45 | "debug" 46 | ] 47 | } 48 | ], 49 | "no-empty": [ 50 | "error", 51 | { 52 | "allowEmptyCatch": true 53 | } 54 | ], 55 | "no-extra-semi": "error", 56 | "eol-last": 2, 57 | "space-before-function-paren": [ 58 | "error", 59 | { 60 | "anonymous": "never", 61 | "named": "never", 62 | "asyncArrow": "always" 63 | } 64 | ], 65 | "no-warning-comments": [ 66 | 2, 67 | { 68 | "terms": [ 69 | "todo", 70 | "fixme", 71 | "any other term" 72 | ], 73 | "location": "anywhere" 74 | } 75 | ], 76 | "spaced-comment": [ 77 | 2, 78 | "always", 79 | { 80 | "markers": [ 81 | "/" 82 | ] 83 | } 84 | ], 85 | "space-infix-ops": [ 86 | "error", 87 | { 88 | "int32Hint": false 89 | } 90 | ], 91 | "no-var": 2, 92 | "no-trailing-spaces": "error", 93 | "brace-style": "error", 94 | "no-use-before-define": "error" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Core-v4 Tests 2 | 3 | on: 4 | pull_request: 5 | branches: [ dev ] 6 | push: 7 | branches: [ dev ] 8 | workflow_dispatch: 9 | branches: [ dev ] 10 | 11 | jobs: 12 | frontend-test: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest] 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: 16 23 | # cache: false 24 | - run: python setup.py 25 | - name: cache frontend dependencies 26 | id: frontend-cache 27 | uses: actions/cache@v4 28 | with: 29 | path: ./node_modules 30 | key: modules-${{ hashFiles('package-lock.json') }} 31 | - name: install frontend dependencies if cache miss 32 | if: steps.frontend-cache.outputs.cache-hit != 'true' 33 | run: npm ci --ignore-scripts 34 | - run: npm run frontend-test 35 | api-test: 36 | runs-on: ${{ matrix.os }} 37 | strategy: 38 | matrix: 39 | os: [ubuntu-latest] 40 | 41 | steps: 42 | - uses: actions/checkout@v2 43 | - name: Use Node.js 44 | uses: actions/setup-node@v2 45 | with: 46 | node-version: 18 47 | # cache: false 48 | - run: python setup.py 49 | - name: Starting MongoDB 50 | uses: supercharge/mongodb-github-action@1.3.0 51 | # the frontend dependencies include mocha, we should 52 | # split api and frontend code completely 53 | - name: cache frontend dependencies 54 | id: frontend-cache 55 | uses: actions/cache@v4 56 | with: 57 | path: ./node_modules 58 | key: modules-${{ hashFiles('package-lock.json') }} 59 | - name: install frontend dependencies if cache miss 60 | if: steps.frontend-cache.outputs.cache-hit != 'true' 61 | run: npm ci --ignore-scripts 62 | - name: cache backend dependencies 63 | id: cache-api 64 | uses: actions/cache@v4 65 | with: 66 | path: ./api/node_modules 67 | key: modules-${{ hashFiles('api/package-lock.json') }} 68 | - name: install backend dependencies if cache miss 69 | if: steps.cache-api.outputs.cache-hit != 'true' 70 | run: cd api; npm ci --ignore-scripts; cd .. 71 | - run: npm run api-test 72 | lint: 73 | runs-on: ${{ matrix.os }} 74 | strategy: 75 | matrix: 76 | os: [ubuntu-latest] 77 | steps: 78 | - uses: actions/checkout@v2 79 | - name: Use Node.js 80 | uses: actions/setup-node@v2 81 | with: 82 | node-version: 16 83 | # cache: false 84 | - run: python setup.py 85 | # the frontend dependencies include eslint 86 | - name: cache frontend dependencies 87 | id: frontend-cache 88 | uses: actions/cache@v4 89 | with: 90 | path: ./node_modules 91 | key: modules-${{ hashFiles('package-lock.json') }} 92 | - name: install frontend dependencies if cache miss 93 | if: steps.frontend-cache.outputs.cache-hit != 'true' 94 | run: npm ci --ignore-scripts 95 | - run: npm run lint 96 | build-frontend: 97 | runs-on: ${{ matrix.os }} 98 | strategy: 99 | matrix: 100 | os: [ubuntu-latest] 101 | steps: 102 | - uses: actions/checkout@v2 103 | - name: Use Node.js 104 | uses: actions/setup-node@v2 105 | with: 106 | node-version: 16 107 | # cache: false 108 | - run: python setup.py 109 | # the frontend dependencies include eslint 110 | - name: cache frontend dependencies 111 | id: frontend-cache 112 | uses: actions/cache@v4 113 | with: 114 | path: ./node_modules 115 | key: modules-${{ hashFiles('package-lock.json') }} 116 | - name: install frontend dependencies if cache miss 117 | if: steps.frontend-cache.outputs.cache-hit != 'true' 118 | run: npm ci --ignore-scripts 119 | - run: npm run build 120 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # **** PROJECT: MEANskills Custom ignore options **** 61 | # Ignore all server log files in the log directory EXCEPT the placeholder 62 | log/* 63 | !log/placeholder 64 | 65 | # Ignore credential files (i.e. have the user create their own credentials) 66 | credentials.json 67 | util/common/trustStore/*.key 68 | util/common/trustStore/*.crt 69 | util/common/trustStore/*.csr 70 | 71 | # For now, ignore the security file so that people can name their own CSRs, Certs, etc. their way 72 | util/common/security.js 73 | 74 | .history 75 | .vscode 76 | *.code-workspace 77 | .DS_Store 78 | .prettierrc.js 79 | 80 | build 81 | 82 | # Config files 83 | config.js 84 | config.json 85 | token.json 86 | .env 87 | 88 | # CSR 89 | *.csr 90 | *.key 91 | *.cer 92 | 93 | # File generated by windows during development 94 | **/gp12-pem* 95 | **/is-ci* 96 | **/mime* 97 | **/nodemon* 98 | **/nodetouch* 99 | **/nopt* 100 | **/rc* 101 | **/semver* 102 | **/uuid* 103 | *.cmd 104 | *.ps1 105 | 106 | api/main_endpoints/routes/printing/* 107 | !api/main_endpoints/routes/printing/keep 108 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Core-v4 Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we, as 6 | contributors and maintainers, pledge to making participation in our project 7 | and our community a harassment-free experience — regardless of age, gender, 8 | expression, level of experience, race, religion, or sexual identity and 9 | orientation. 10 | 11 | ## Our Standards 12 | 13 | Examples of behavior that contributes to creating a positive environment 14 | include: 15 | 16 | * Using welcoming and inclusive language 17 | * Being respectful of differing viewpoints and experiences 18 | * Gracefully accepting constructive criticism 19 | * Focusing on what is best for the community 20 | * Showing empathy towards other community members 21 | * Using threads on Slack 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * Trolling, insulting/derogatory comments, and unconstructive criticsm 26 | * Public or private harassment 27 | * Publishing others' private information without explicit permission 28 | * Not using threads on Slack 29 | 30 | ## Our Responsibilities 31 | 32 | Project maintainers are responsible for clarifying the standards of acceptable 33 | behavior and are expected to take appropriate and fair corrective action in 34 | response to any instances of unacceptable behavior. 35 | 36 | Project maintainers have the right and responsibility to remove, edit, or 37 | reject comments, commits, code, wiki edits, issues, and other contributions 38 | that are not aligned to this Code of Conduct, or to ban temporarily or 39 | permanently any contributor for other behaviors that they deem inappropriate, 40 | threatening, offensive, or harmful. 41 | 42 | ## Scope 43 | 44 | This Code of Conduct applies within project space as well as in the SCE Slack 45 | and Discord. 46 | 47 | ## Enforcement 48 | 49 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 50 | reported by contacting to SCE at sce.sjsu@gmail.com. All 51 | complaints will be reviewed and investigated and will result in a response that 52 | is deemed necessary and appropriate to the circumstances. 53 | 54 | Project maintainers who do not follow or enforce the Code of Conduct in good 55 | faith may face temporary or permanent repercussions as determined by other 56 | members of the project's leadership. 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 SCE-Development 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Core-v4 2 | Welcome to the respository for the Software and Computer Engineering Society's 3 | offical website! 4 | ![image](https://user-images.githubusercontent.com/36345325/78325084-81350300-752b-11ea-8571-032ed04b3018.png) 5 | To learn more, check out our 6 | [wiki page](https://github.com/SCE-Development/Core-v4/wiki)! 7 | You can get started running this project locally 8 | [here](https://github.com/SCE-Development/Core-v4/wiki/Getting-Started). 9 | 10 | -------------------------------------------------------------------------------- /api/cloud_api/Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM node:alpine 3 | 4 | WORKDIR /app 5 | 6 | COPY package.json ./ 7 | 8 | RUN npm install --production 9 | 10 | COPY ./cloud_api /app/cloud_api 11 | 12 | COPY ./util /app/util 13 | 14 | COPY ./config /app/config/ 15 | 16 | EXPOSE 8082 17 | 18 | ARG GENERAL_API_URL 19 | 20 | ARG DATABASE_HOST 21 | 22 | ARG NODE_ENV 23 | 24 | ARG VERIFICATION_BASE_URL 25 | 26 | CMD [ "node", "./cloud_api/server.js" ] 27 | -------------------------------------------------------------------------------- /api/cloud_api/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json ./ 6 | 7 | RUN npm install --production 8 | 9 | RUN npm install -g nodemon 10 | 11 | COPY ./cloud_api /app/cloud_api 12 | 13 | COPY ./util /app/util 14 | 15 | COPY ./config/config.json /app/config/config.json 16 | 17 | EXPOSE 8082 18 | 19 | ARG GENERAL_API_URL 20 | 21 | ARG NODE_ENV 22 | 23 | ARG VERIFICATION_BASE_URL 24 | 25 | ARG DATABASE_HOST 26 | 27 | CMD ["npm", "run", "cloud_api"] 28 | -------------------------------------------------------------------------------- /api/cloud_api/email_templates/blastEmail.js: -------------------------------------------------------------------------------- 1 | function blastEmail(user, recipient, subject, content) { 2 | return new Promise((resolve, reject) => { 3 | const sender = `"SCE SJSU 👻" <${user}>`; 4 | return resolve({ 5 | from: sender, 6 | to: recipient, 7 | subject: subject, 8 | generateTextFromHTML: true, 9 | html: ` 10 | ${content} 11 | `, 12 | }); 13 | }); 14 | } 15 | 16 | module.exports = { blastEmail }; 17 | -------------------------------------------------------------------------------- /api/cloud_api/email_templates/passwordReset.js: -------------------------------------------------------------------------------- 1 | const generateHashedId = require('../util/auth').generateHashedId; 2 | 3 | function passwordReset(user, resetToken, recipient) { 4 | return new Promise((resolve, reject) => { 5 | generateHashedId(recipient) 6 | .then(hashedId => { 7 | const url = 8 | process.env.VERIFICATION_BASE_URL || 'http://localhost:3000'; 9 | const resetLink = 10 | `${url}/reset?id=${hashedId}&resetToken=${resetToken}`; 11 | return resolve({ 12 | from: user, 13 | to: recipient, 14 | subject: 'Reset Password for SCE', 15 | generateTextFromHTML: true, 16 | html: ` 17 | Hi,
18 |

Click the link below to reset your password. It will expire in 24 hours.

19 | Reset Password 20 | ` 21 | }); 22 | }) 23 | .catch(error => { 24 | reject(error); 25 | }); 26 | }); 27 | } 28 | 29 | module.exports = { passwordReset }; 30 | -------------------------------------------------------------------------------- /api/cloud_api/email_templates/test.js: -------------------------------------------------------------------------------- 1 | module.exports = function(user, recipient, name) { 2 | return { 3 | from: user, 4 | to: recipient, 5 | subject: 'This is a test email', 6 | generateTextFromHTML: true, 7 | html: ` 8 | Hi ${name},
9 |

This is a friendly test.

10 | ` 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /api/cloud_api/email_templates/unsubscribeEmail.js: -------------------------------------------------------------------------------- 1 | function unsubscribeEmail(user, recipient, name) { 2 | return new Promise((resolve, reject) => { 3 | const url = 4 | process.env.VERIFICATION_BASE_URL || 'http://localhost:3000'; 5 | const verifyLink = 6 | `${url}/emailPreferences?user=${recipient}`; 7 | return resolve({ 8 | from: user, 9 | to: recipient, 10 | subject: 'Unsubscribe from SCE Emails', 11 | generateTextFromHTML: true, 12 | html: ` 13 | Hi ${name || ''},
14 |

We appreciate you reducing email spam. 15 | To opt out of all our emails, click the link below 16 | and we’ll take care of the rest.

17 | Update Email Preferences 18 |

Thanks,

19 |

The Software and Computer Engineering Society

20 | ` 21 | }); 22 | }); 23 | } 24 | 25 | module.exports = { unsubscribeEmail }; 26 | -------------------------------------------------------------------------------- /api/cloud_api/email_templates/verification.js: -------------------------------------------------------------------------------- 1 | const generateHashedId = require('../util/auth').generateHashedId; 2 | 3 | function verification(user, recipient, name) { 4 | return new Promise((resolve, reject) => { 5 | generateHashedId(recipient) 6 | .then(hashedId => { 7 | const url = 8 | process.env.VERIFICATION_BASE_URL || 'http://localhost:3000'; 9 | const verifyLink = 10 | `${url}/verify?id=${hashedId}&user=${recipient}`; 11 | return resolve({ 12 | from: user, 13 | to: recipient, 14 | subject: 'Please verify your email address', 15 | generateTextFromHTML: true, 16 | html: ` 17 | Hi ${name || ''},
18 |

Thanks for signing up! 19 | Please verify your email by clicking below.

20 | Verify Email 21 | ` 22 | }); 23 | }) 24 | .catch(error => { 25 | reject(error); 26 | }); 27 | }); 28 | } 29 | 30 | module.exports = { verification }; 31 | -------------------------------------------------------------------------------- /api/cloud_api/server.js: -------------------------------------------------------------------------------- 1 | const { model } = require('mongoose'); 2 | const { SceHttpServer } = require('../util/SceHttpServer'); 3 | 4 | function main() { 5 | const API_ENDPOINTS = __dirname + '/routes/'; 6 | const cloudServer = new SceHttpServer(API_ENDPOINTS, 8082, '/cloudapi/'); 7 | cloudServer.init().then(() => { 8 | cloudServer.openConnection(); 9 | }); 10 | } 11 | 12 | main(); 13 | -------------------------------------------------------------------------------- /api/cloud_api/util/AuthManager.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const readline = require('readline'); 3 | const { consoleColors } = require('../../util/constants'); 4 | 5 | const { 6 | redColor, 7 | greenColor, 8 | blueColor, 9 | yellowColor, 10 | defaultColor 11 | } = consoleColors; 12 | 13 | const rl = readline.createInterface({ 14 | input: process.stdin, 15 | output: process.stdout, 16 | terminal: false 17 | }); 18 | 19 | /** 20 | * Handles the gathering and writing 21 | * of Google API credentials 22 | */ 23 | class AuthManager { 24 | /** 25 | * Prompts user for input and resolves the input or false 26 | * @param {String} prompt question to get the info we need 27 | */ 28 | inputPrompt(prompt) { 29 | return new Promise((resolve, reject) => { 30 | rl.question(prompt, (ans) => { 31 | if(ans !== '') { 32 | resolve(ans); 33 | } else { 34 | resolve(false); 35 | } 36 | }); 37 | }); 38 | } 39 | 40 | /** 41 | * Prompts user for client ID & secret and writes them to config 42 | * file containing auth credentials for various google APIs 43 | * @param {String} configPath the directory path of the config.json file 44 | * @param {function} callback a function to run in sync after successful write 45 | */ 46 | async setAuthCredentials(configPath, callback) { 47 | console.debug(blueColor + 48 | 'Welcome to SCE\'s Google Cloud API Token Generator!\n' 49 | ); 50 | console.debug( 51 | yellowColor + 52 | 'Don\'t know what to do or how this works?\n' + 53 | /* eslint-disable-next-line */ 54 | 'Please visit: https://github.com/SCE-Development/Core-v4/wiki/Getting-Started#setting-up-mailer\n' + 55 | defaultColor 56 | ); 57 | 58 | const configFileData = fs.readFileSync(configPath); 59 | 60 | if (configFileData) { 61 | let config = JSON.parse(configFileData); 62 | 63 | console.debug( 64 | `Current client ID: ${yellowColor}` + 65 | `${config.googleApiKeys.CLIENT_ID}` + 66 | `${defaultColor}` 67 | ); 68 | const clientID = await this.inputPrompt( 69 | 'Please enter the Client ID (press enter to skip): ' 70 | ); 71 | if (clientID) { 72 | config.googleApiKeys.CLIENT_ID = clientID; 73 | } else { 74 | console.debug( 75 | `defaulting to ${yellowColor}${config.googleApiKeys.CLIENT_ID}` 76 | + defaultColor 77 | ); 78 | } 79 | 80 | console.debug( 81 | `Current client secret: ${yellowColor}` + 82 | `${config.googleApiKeys.CLIENT_SECRET}` + 83 | `${defaultColor}` 84 | ); 85 | const clientSecret = await this.inputPrompt( 86 | 'Please enter the Client Secret (press enter to skip): ' 87 | ); 88 | if (clientSecret) { 89 | config.googleApiKeys.CLIENT_SECRET = clientSecret; 90 | } else { 91 | console.debug( 92 | `defaulting to ${yellowColor}${config.googleApiKeys.CLIENT_SECRET}` 93 | + defaultColor 94 | ); 95 | } 96 | 97 | fs.writeFile(configPath, JSON.stringify(config), (error) => { 98 | if (error) { 99 | return console.debug( 100 | `A problem occurred trying to write to ${configPath}`, error 101 | ); 102 | } 103 | 104 | console.debug(greenColor + 105 | 'Successfully wrote config data to:', configPath + defaultColor 106 | ); 107 | callback(); 108 | }); 109 | } else { 110 | console.debug(redColor + 111 | 'config.json file does not exist at path:', configPath + defaultColor 112 | ); 113 | } 114 | } 115 | } 116 | 117 | module.exports = { AuthManager }; 118 | -------------------------------------------------------------------------------- /api/cloud_api/util/auth.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | const GENERAL_API_URL = process.env.GENERAL_API_URL 4 | || 'http://localhost:8080/api'; 5 | async function validateVerificationEmail(){ 6 | let status = ''; 7 | await axios 8 | .post(`${GENERAL_API_URL}/Auth/sendVerificationEmail`) 9 | .then(res =>{ 10 | status = res.data; 11 | }) 12 | .catch(err => { 13 | status = err.data; 14 | }); 15 | return status; 16 | } 17 | 18 | async function generateHashedId(email){ 19 | let hashedId = ''; 20 | await axios 21 | .post(`${GENERAL_API_URL}/Auth/generateHashedId`, {email}) 22 | .then(res =>{ 23 | hashedId = res.data.hashedId; 24 | }) 25 | .catch(err => { 26 | hashedId = null; 27 | }); 28 | return hashedId; 29 | } 30 | 31 | module.exports = { 32 | generateHashedId, 33 | validateVerificationEmail 34 | }; 35 | -------------------------------------------------------------------------------- /api/config/config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "discordApiKeys": { 3 | "ENABLED": true, 4 | "CLIENT_ID": "NOT_SET", 5 | "CLIENT_SECRET": "NOT_SET", 6 | "REFRESH_TOKEN": "NOT_SET" 7 | }, 8 | "googleApiKeys": { 9 | "ENABLED": true, 10 | "CLIENT_ID": "NOT_SET", 11 | "CLIENT_SECRET": "NOT_SET", 12 | "REDIRECT_URIS": [ 13 | "https://developers.google.com/oauthplayground" 14 | ], 15 | "REFRESH_TOKEN": "NOT_SET", 16 | "USER": "sce.sjsu@gmail.com", 17 | "CAPTCHA_SECRET_KEY": "NOT_SET" 18 | }, 19 | "passwordStrength": "medium", 20 | "PRINTING": { 21 | "ENABLED": false 22 | }, 23 | "Cleezy": { 24 | "ENABLED": false 25 | }, 26 | "Speakers": { 27 | "ENABLED": false 28 | }, 29 | "officeAccessCard": { 30 | "API_KEY": "NOTHING_REALLY" 31 | }, 32 | "secretKey": "super duper secret key" 33 | } -------------------------------------------------------------------------------- /api/devServer.js: -------------------------------------------------------------------------------- 1 | // This if statement checks if the module was require()'d or if it was run 2 | // by node server.js. If we are not requiring it and are running it from the 3 | // command line, we create a server instance and start listening for requests. 4 | if (typeof module !== 'undefined' && !module.parent) { 5 | // Starting servers 6 | require('./main_endpoints/server'); 7 | require('./cloud_api/server'); 8 | } 9 | -------------------------------------------------------------------------------- /api/main_endpoints/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json ./ 6 | 7 | RUN npm install --production 8 | 9 | COPY ./main_endpoints /app/main_endpoints 10 | 11 | COPY ./util /app/util 12 | 13 | COPY ./config/config.json /app/config/config.json 14 | 15 | EXPOSE 8080 16 | 17 | ARG DISCORD_REDIRECT_URI 18 | 19 | ARG NODE_ENV 20 | 21 | ARG DATABASE_HOST 22 | 23 | CMD [ "node", "./main_endpoints/server.js" ] 24 | -------------------------------------------------------------------------------- /api/main_endpoints/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json ./ 6 | 7 | RUN npm install --production 8 | RUN npm install -g nodemon 9 | 10 | COPY ./main_endpoints /app/main_endpoints 11 | 12 | COPY ./util /app/util 13 | 14 | COPY ./config/config.json /app/config/config.json 15 | 16 | EXPOSE 8080 17 | 18 | ARG DISCORD_REDIRECT_URI 19 | 20 | ARG NODE_ENV 21 | 22 | ARG DATABASE_HOST 23 | 24 | CMD [ "npm", "run", "main_endpoints"] 25 | -------------------------------------------------------------------------------- /api/main_endpoints/models/Advertisement.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | const AdvertisementSchema = new Schema( 5 | { 6 | message: { 7 | type: String, 8 | required: true, 9 | maxlength: [255, 'message must be at most 255 characters long'] 10 | }, 11 | expireDate: { 12 | type: Date, 13 | } 14 | }, 15 | { collection: 'Advertisements' } 16 | ); 17 | 18 | module.exports = mongoose.model('Advertisement', AdvertisementSchema); 19 | -------------------------------------------------------------------------------- /api/main_endpoints/models/OfficeAccessCard.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | 5 | const OfficeAccessCardSchema = new Schema( 6 | { 7 | cardBytes: { 8 | type: String, 9 | required: true, 10 | }, 11 | createdAt: { 12 | type: Date, 13 | default: Date.now 14 | }, 15 | verifiedCount:{ 16 | type:Number, 17 | default:0 18 | }, 19 | lastVerified:{ 20 | type:Date, 21 | default: Date.now 22 | } 23 | }, 24 | { collection: 'OfficeAccessCards' } 25 | ); 26 | 27 | module.exports = mongoose.model('OfficeAccessCard', OfficeAccessCardSchema); 28 | -------------------------------------------------------------------------------- /api/main_endpoints/models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | const bcrypt = require('bcryptjs'); 4 | const membershipState = require('../../util/constants').MEMBERSHIP_STATE; 5 | 6 | const UserSchema = new Schema( 7 | { 8 | firstName: { 9 | type: String, 10 | required: true 11 | }, 12 | lastName: { 13 | type: String, 14 | required: true 15 | }, 16 | joinDate: { 17 | type: Date, 18 | default: Date.now 19 | }, 20 | password: { 21 | type: String, 22 | required: true 23 | }, 24 | email: { 25 | type: String, 26 | unique: true, 27 | required: true 28 | }, 29 | emailVerified: { 30 | type: Boolean, 31 | default: false 32 | }, 33 | emailOptIn: { 34 | type: Boolean, 35 | default: true 36 | }, 37 | discordUsername: { 38 | type: String, 39 | default: '' 40 | }, 41 | discordDiscrim: { 42 | type: String, 43 | default: '' 44 | }, 45 | discordID: { 46 | type: String, 47 | default: '' 48 | }, 49 | major: { 50 | type: String 51 | }, 52 | 53 | doorCode: { 54 | type: String 55 | }, 56 | 57 | accessLevel: { 58 | type: Number, 59 | default: membershipState.PENDING 60 | }, 61 | lastLogin: { 62 | type: Date, 63 | default: Date.now 64 | }, 65 | membershipValidUntil: { 66 | type: Date, 67 | default: Date.now 68 | }, 69 | pagesPrinted: { 70 | type: Number, 71 | default: 0 72 | }, 73 | apiKey: { 74 | type: String, 75 | default: '' 76 | } 77 | }, 78 | { collection: 'User' } 79 | ); 80 | 81 | UserSchema.pre('save', function(next) { 82 | const member = this; 83 | let emailRegExp = new RegExp(['^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0', 84 | '-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61', 85 | '}[a-zA-Z0-9])?)*$'].join('')); 86 | if (!this.email.match(emailRegExp)) { 87 | return next('Bad email tried to be save (email format is: example@domain)'); 88 | } 89 | if (this.isModified('password') || this.isNew) { 90 | bcrypt.genSalt(10, function(error, salt) { 91 | if (error) { 92 | return next(error); 93 | } 94 | bcrypt.hash(member.password, salt, function(error, hash) { 95 | if (error) { 96 | return next(error); 97 | } 98 | 99 | member.password = hash; 100 | return next(); 101 | }); 102 | }); 103 | } else { 104 | return next(); 105 | } 106 | }); 107 | 108 | UserSchema.methods.comparePassword = function(passwd, callback) { 109 | bcrypt.compare(passwd, this.password, function(error, isMatch) { 110 | if (error) { 111 | return callback(error); 112 | } 113 | 114 | callback(null, isMatch); 115 | }); 116 | }; 117 | 118 | module.exports = 119 | mongoose.models && mongoose.models.User 120 | ? mongoose.models.User 121 | : mongoose.model('User', UserSchema); 122 | -------------------------------------------------------------------------------- /api/main_endpoints/routes/Advertisement.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const { OK, BAD_REQUEST, FORBIDDEN, UNAUTHORIZED, NOT_FOUND } = require('../../util/constants').STATUS_CODES; 4 | const { 5 | decodeToken, 6 | checkIfTokenSent, 7 | } = require('../util/token-functions.js'); 8 | const Advertisement = require('../models/Advertisement'); 9 | 10 | router.get('/', async (req, res) => { 11 | const count = await Advertisement.countDocuments(); 12 | const random = Math.floor(Math.random() * count); 13 | 14 | Advertisement.findOne().skip(random) 15 | .then(items => { 16 | res.status(OK).send(items); 17 | }) 18 | .catch(error => { 19 | res.sendStatus(BAD_REQUEST); 20 | }); 21 | }); 22 | 23 | router.get('/getAllAdvertisements', async (req, res) => { 24 | if (!checkIfTokenSent(req)) { 25 | return res.sendStatus(FORBIDDEN); 26 | } else if (!await decodeToken(req)) { 27 | return res.sendStatus(UNAUTHORIZED); 28 | } 29 | Advertisement.find() 30 | .then(items => res.status(OK).send(items)) 31 | .catch(error => { 32 | res.sendStatus(BAD_REQUEST); 33 | }); 34 | }); 35 | 36 | router.post('/createAdvertisement', async (req, res) => { 37 | if (!checkIfTokenSent(req)) { 38 | return res.sendStatus(FORBIDDEN); 39 | } else if (!await decodeToken(req)) { 40 | return res.sendStatus(UNAUTHORIZED); 41 | } 42 | const newAd = new Advertisement({ 43 | message: req.body.message, 44 | expireDate: req.body.expireDate 45 | }); 46 | 47 | Advertisement.create(newAd) 48 | .then((post) => { 49 | return res.json(post); 50 | }) 51 | .catch( 52 | (error) => res.sendStatus(BAD_REQUEST) 53 | ); 54 | }); 55 | 56 | router.post('/deleteAdvertisement', async (req, res) => { 57 | if (!checkIfTokenSent(req)) { 58 | return res.sendStatus(FORBIDDEN); 59 | } else if (!await decodeToken(req)) { 60 | return res.sendStatus(UNAUTHORIZED); 61 | } 62 | Advertisement.deleteOne({ _id: req.body._id }) 63 | .then(result => { 64 | if (result.n < 1) { 65 | res.sendStatus(NOT_FOUND); 66 | } else { 67 | res.sendStatus(OK); 68 | } 69 | }) 70 | .catch(() => { 71 | res.sendStatus(BAD_REQUEST); 72 | }); 73 | }); 74 | 75 | module.exports = router; 76 | -------------------------------------------------------------------------------- /api/main_endpoints/routes/Cleezy.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const axios = require('axios'); 3 | const router = express.Router(); 4 | const { 5 | decodeToken, 6 | checkIfTokenSent, 7 | } = require('../util/token-functions.js'); 8 | const { 9 | OK, 10 | UNAUTHORIZED, 11 | FORBIDDEN, 12 | SERVER_ERROR, 13 | } = require('../../util/constants').STATUS_CODES; 14 | const logger = require('../../util/logger'); 15 | const { Cleezy } = require('../../config/config.json'); 16 | const { ENABLED } = Cleezy; 17 | 18 | let CLEEZY_URL = process.env.CLEEZY_URL 19 | || 'http://localhost:8000'; 20 | let URL_SHORTENER_BASE_URL = 21 | process.env.NODE_ENV === 'production' ? 'https://sce.sjsu.edu/s/' : 'http://localhost:8000/find/'; 22 | 23 | router.get('/list', async (req, res) => { 24 | if(!ENABLED) { 25 | return res.json({ 26 | disabled: true 27 | }); 28 | } 29 | const { page = 0, search, sortColumn = 'created_at', sortOrder = 'DESC'} = req.query; 30 | if (!checkIfTokenSent(req)) { 31 | return res.sendStatus(FORBIDDEN); 32 | } else if (!await decodeToken(req)) { 33 | return res.sendStatus(UNAUTHORIZED); 34 | } 35 | try { 36 | const response = await axios.get(CLEEZY_URL + '/list', { 37 | params: { 38 | page, 39 | ...(search !== undefined && { search }), 40 | // eslint-disable-next-line camelcase 41 | sort_by: sortColumn, 42 | order: sortOrder 43 | }, 44 | }); 45 | const { data = [], total, rows_per_page: rowsPerPage } = response.data; 46 | const returnData = data.map(element => { 47 | const u = new URL(element.alias, URL_SHORTENER_BASE_URL); 48 | return { ...element, link: u.href }; 49 | }); 50 | res.json({ data: returnData, total, rowsPerPage }); 51 | } catch (err) { 52 | logger.error('/listAll had an error', err); 53 | if (err.response && err.response.data) { 54 | res.status(err.response.status).json({ error: err.response.data }); 55 | } else { 56 | res.status(SERVER_ERROR).json({ error: 'Failed to list URLs' }); 57 | } 58 | } 59 | }); 60 | 61 | router.post('/createUrl', async (req, res) => { 62 | if (!checkIfTokenSent(req)) { 63 | return res.sendStatus(FORBIDDEN); 64 | } else if (!await decodeToken(req)) { 65 | return res.sendStatus(UNAUTHORIZED); 66 | } 67 | const { url, alias } = req.body; 68 | let jsonbody = { url, alias: alias || null }; 69 | try { 70 | const response = await axios.post(CLEEZY_URL + '/create_url', jsonbody); 71 | const data = response.data; 72 | const u = new URL( data.alias, URL_SHORTENER_BASE_URL); 73 | res.json({ ...data, link: u }); 74 | } catch (err) { 75 | logger.error('/createUrl had an error', err); 76 | res.status(err.response.status).json({ error: err.response.status }); 77 | } 78 | }); 79 | 80 | router.post('/deleteUrl', async (req, res) => { 81 | if (!checkIfTokenSent(req)) { 82 | return res.sendStatus(FORBIDDEN); 83 | } else if (!await decodeToken(req)) { 84 | return res.sendStatus(UNAUTHORIZED); 85 | } 86 | const { alias } = req.body; 87 | axios 88 | .post(CLEEZY_URL + '/delete/' + alias) 89 | .then(() => { 90 | res.sendStatus(OK); 91 | }) 92 | .catch(err => { 93 | logger.error('/deleteUrl had an error', err); 94 | res.status(err.response.status).json({ error: err.response.status }); 95 | }); 96 | }); 97 | 98 | module.exports = router; 99 | -------------------------------------------------------------------------------- /api/main_endpoints/routes/LedSign.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const { 4 | OK, 5 | SERVER_ERROR, 6 | UNAUTHORIZED 7 | } = require('../../util/constants').STATUS_CODES; 8 | const { 9 | decodeToken, 10 | checkIfTokenSent 11 | } = require('../util/token-functions.js'); 12 | const logger = require('../../util/logger'); 13 | const { updateSign, healthCheck, turnOffSign } = require('../util/LedSign.js'); 14 | 15 | const runningInDevelopment = process.env.NODE_ENV !== 'production' 16 | && process.env.NODE_ENV !== 'test'; 17 | 18 | 19 | router.get('/healthCheck', async (req, res) => { 20 | /* 21 | * How these work with Quasar: 22 | * https://github.com/SCE-Development/Quasar/wiki/How-do-Health-Checks-Work%3F 23 | */ 24 | if (runningInDevelopment) { 25 | return res.sendStatus(OK); 26 | } 27 | const dataFromSign = await healthCheck(); 28 | if(!dataFromSign) { 29 | return res.sendStatus(SERVER_ERROR); 30 | } 31 | return res.status(OK).json(dataFromSign); 32 | }); 33 | 34 | router.post('/updateSignText', async (req, res) => { 35 | if (!checkIfTokenSent(req)) { 36 | logger.warn('/updateSignText was requested without a token'); 37 | return res.sendStatus(UNAUTHORIZED); 38 | } 39 | if (!await decodeToken(req)) { 40 | logger.warn('/updateSignText was requested with an invalid token'); 41 | return res.sendStatus(UNAUTHORIZED); 42 | } 43 | if (runningInDevelopment) { 44 | return res.sendStatus(OK); 45 | } 46 | // need to make this its own api endpoint 47 | let result = false; 48 | if (req.body.ledIsOff) { 49 | result = await turnOffSign(); 50 | logger.info('turning sign off!'); 51 | } else { 52 | logger.info('updating sign with:', req.body); 53 | result = await updateSign(req.body); 54 | } 55 | let status = OK; 56 | if(!result) { 57 | status = SERVER_ERROR; 58 | } 59 | return res.sendStatus(status); 60 | }); 61 | 62 | 63 | module.exports = router; 64 | -------------------------------------------------------------------------------- /api/main_endpoints/routes/OfficeAccessCard.js: -------------------------------------------------------------------------------- 1 | const { 2 | UNAUTHORIZED, 3 | BAD_REQUEST, 4 | SERVER_ERROR, 5 | NOT_FOUND, 6 | OK, 7 | } = require('../../util/constants').STATUS_CODES; 8 | const express = require('express'); 9 | const router = express.Router(); 10 | const bodyParser = require('body-parser'); 11 | const OfficeAccessCard = require('../models/OfficeAccessCard.js'); 12 | const logger = require('../../util/logger'); 13 | const { officeAccessCard = {} } = require('../../config/config.json'); 14 | const { API_KEY = 'NOTHING_REALLY' } = officeAccessCard; 15 | 16 | router.use(bodyParser.json()); 17 | 18 | function checkIfCardExists(cardBytes) { 19 | return new Promise((resolve) => { 20 | try { 21 | OfficeAccessCard.findOneAndUpdate( 22 | { cardBytes:cardBytes}, 23 | { 24 | $inc: { verifiedCount: 1 }, 25 | $set: { lastVerified: Date.now() } 26 | }, { 27 | useFindAndModify: false, new:true, upsert:false 28 | } 29 | , (error, result) => { 30 | if (error) { 31 | logger.error('checkIfCardExists got an error querying mongodb: ', error); 32 | return resolve(false); 33 | } 34 | if(!result){ 35 | logger.info(`Card:${cardBytes} not found in the database`); 36 | return resolve(false); 37 | } 38 | return resolve(!!result); 39 | }); 40 | } catch (error) { 41 | logger.error('checkIfCardExists caught an error: ', error); 42 | return resolve(false); 43 | } 44 | }); 45 | } 46 | 47 | router.get('/verify', async (req, res) =>{ 48 | const { cardBytes, add = false } = req.query; 49 | const apiKey = req.headers['x-api-key']; 50 | const required = [ 51 | { value: apiKey, title: 'X-API-Key HTTP header', }, 52 | { value: cardBytes, title: 'cardBytes query parameter', }, 53 | ]; 54 | 55 | const missingValue = required.find(({ value }) => !value); 56 | 57 | if (missingValue) { 58 | return res.status(BAD_REQUEST).send(` ${missingValue.title} missing from request`); 59 | } 60 | 61 | if (apiKey !== API_KEY) { 62 | return res.sendStatus(UNAUTHORIZED); 63 | } 64 | 65 | const cardExists = await checkIfCardExists(cardBytes); 66 | if (cardExists) { 67 | return res.sendStatus(OK); 68 | } 69 | // if a card doesnt exist and we arent trying 70 | // to add a new one, that means we were trying 71 | // to verify a card, and that card isnt found. 72 | // therefore return a non OK status 73 | if (!add) { 74 | return res.sendStatus(NOT_FOUND); 75 | } 76 | 77 | try { 78 | if (add) { 79 | logger.info('adding a new card'); 80 | await new OfficeAccessCard({ 81 | cardBytes 82 | }).save(); 83 | return res.sendStatus(OK); 84 | } 85 | } catch (error) { 86 | logger.error('Error creating OfficeAccessCard: ', error); 87 | return res.sendStatus(SERVER_ERROR); 88 | } 89 | }); 90 | 91 | module.exports = router; 92 | -------------------------------------------------------------------------------- /api/main_endpoints/routes/Printer.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const express = require('express'); 3 | const multer = require('multer'); 4 | const FormData = require('form-data'); 5 | const logger = require('../../util/logger'); 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | 9 | const { 10 | decodeToken, 11 | checkIfTokenSent, 12 | } = require('../util/token-functions.js'); 13 | const { 14 | OK, 15 | UNAUTHORIZED, 16 | NOT_FOUND, 17 | SERVER_ERROR, 18 | } = require('../../util/constants').STATUS_CODES; 19 | const { 20 | PRINTING = {} 21 | } = require('../../config/config.json'); 22 | const { MetricsHandler } = require('../../util/metrics'); 23 | 24 | // see https://github.com/SCE-Development/Quasar/tree/dev/docker-compose.dev.yml#L11 25 | let PRINTER_URL = process.env.PRINTER_URL 26 | || 'http://localhost:14000'; 27 | 28 | const router = express.Router(); 29 | 30 | // stores file inside temp folder 31 | const storage = multer.diskStorage({ 32 | destination: function(req, file, cb) { 33 | cb(null, path.join(__dirname, 'printing')); 34 | }, 35 | filename: function(req, file, cb) { 36 | const uniqueSuffix = Date.now(); 37 | cb(null, uniqueSuffix + '_' + file.originalname); 38 | } 39 | }); 40 | 41 | const upload = multer({ storage }); 42 | 43 | router.get('/healthCheck', async (req, res) => { 44 | /* 45 | * How these work with Quasar: 46 | * https://github.com/SCE-Development/Quasar/wiki/How-do-Health-Checks-Work%3F 47 | */ 48 | if (!PRINTING.ENABLED) { 49 | logger.warn('Printing is disabled, returning 200 to mock the printing server'); 50 | return res.sendStatus(OK); 51 | } 52 | await axios 53 | .get(PRINTER_URL + '/healthcheck/printer') 54 | .then(() => { 55 | return res.sendStatus(OK); 56 | }) 57 | .catch((err) => { 58 | logger.error('Printer SSH tunnel is down: ', err); 59 | MetricsHandler.sshTunnelErrors.inc({ type: 'Printer' }); 60 | return res.sendStatus(NOT_FOUND); 61 | }); 62 | }); 63 | 64 | router.post('/sendPrintRequest', upload.single('file'), async (req, res) => { 65 | if (!checkIfTokenSent(req)) { 66 | logger.warn('/sendPrintRequest was requested without a token'); 67 | return res.sendStatus(UNAUTHORIZED); 68 | } 69 | if (!await decodeToken(req)) { 70 | logger.warn('/sendPrintRequest was requested with an invalid token'); 71 | return res.sendStatus(UNAUTHORIZED); 72 | } 73 | if (!PRINTING.ENABLED) { 74 | logger.warn('Printing is disabled, returning 200 to mock the printing server'); 75 | return res.sendStatus(OK); 76 | } 77 | const { copies, sides } = req.body; 78 | const file = req.file; 79 | const data = new FormData(); 80 | data.append('file', fs.createReadStream(file.path), { filename: file.originalname }); 81 | data.append('copies', copies); 82 | data.append('sides', sides); 83 | axios.post(PRINTER_URL + '/print', 84 | data, 85 | { 86 | headers: { 87 | ...data.getHeaders(), 88 | } 89 | }) 90 | .then(() => { 91 | // delete file from temp folder after printing 92 | fs.unlink(file.path, (err) => { 93 | if (err) { 94 | logger.error(`Unable to delete file at path ${file.path}:`, err); 95 | } 96 | }); 97 | res.sendStatus(OK); 98 | }).catch((err) => { 99 | logger.error('/sendPrintRequest had an error: ', err); 100 | res.sendStatus(SERVER_ERROR); 101 | }); 102 | }); 103 | 104 | module.exports = router; 105 | -------------------------------------------------------------------------------- /api/main_endpoints/routes/printing/keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SCE-Development/Clark/46801ff13fd8842b17e29fdd99094097800292da/api/main_endpoints/routes/printing/keep -------------------------------------------------------------------------------- /api/main_endpoints/server.js: -------------------------------------------------------------------------------- 1 | const { SceHttpServer } = require('../util/SceHttpServer'); 2 | 3 | function main() { 4 | const API_ENDPOINTS = [ 5 | __dirname + '/routes/' 6 | ]; 7 | const mainEndpointServer = new SceHttpServer(API_ENDPOINTS, 8080); 8 | mainEndpointServer.init().then(() => { 9 | mainEndpointServer.openConnection(); 10 | }); 11 | } 12 | 13 | main(); 14 | -------------------------------------------------------------------------------- /api/main_endpoints/util/LedSign.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const logger = require('../../util/logger'); 3 | const { MetricsHandler } = require('../../util/metrics'); 4 | 5 | 6 | // see https://github.com/SCE-Development/rpi-led-controller/tree/master/server.py#L126 7 | let LED_SIGN_URL = process.env.LED_SIGN_URL 8 | || 'http://localhost'; 9 | 10 | /** 11 | * These functions are meant only for use in production, where the 12 | * LED sign can be reached from Core-v4 through an SSH tunnel. Want 13 | * to learn more about how this works? Check out the below link: 14 | * https://github.com/SCE-Development/Quasar/wiki/How-Does-the-LED-Sign-Work%3F 15 | */ 16 | 17 | /** 18 | * Send an object to update the LED sign with. 19 | * @param {Object} data The new sign data, for example 20 | * { 21 | * "scrollSpeed": 15, 22 | * "backgroundColor": "#0000FF", 23 | * "textColor": "#00FF00", 24 | * "borderColor": "#FF0000", 25 | * "text": "Welcome to SCE!", 26 | * } 27 | * @returns {Promise} Whether the LED Sign's API can be reached or not. 28 | */ 29 | async function updateSign(data) { 30 | return new Promise((resolve) => { 31 | axios 32 | .post(LED_SIGN_URL + '/api/update-sign', data) 33 | .then(() => { 34 | resolve(true); 35 | }).catch((err) => { 36 | logger.error('updateSign had an error: ', err); 37 | resolve(false); 38 | }); 39 | }); 40 | } 41 | 42 | /** 43 | * Turn the led sign off using its REST API. 44 | * @returns {Promise} Whether the turn off request worked or not. 45 | */ 46 | async function turnOffSign() { 47 | return new Promise((resolve) => { 48 | axios 49 | .get(LED_SIGN_URL + '/api/turn-off') 50 | .then(() => { 51 | resolve(true); 52 | }).catch((err) => { 53 | logger.error('turnOffSign had an error: ', err); 54 | resolve(false); 55 | }); 56 | }); 57 | } 58 | 59 | /** 60 | * Send an HTTP GET request to the LED sign to see if it is up. 61 | * @returns {Promise} If the sign is up, a JSON response 62 | * is returned. If there is no message on the sign, the response looks like 63 | * { 64 | * "success": true 65 | * } 66 | * If there is a message on the sign, the JSON returned looks like: 67 | * { 68 | * "scrollSpeed": 15, 69 | * "backgroundColor": "#0000FF", 70 | * "textColor": "#00FF00", 71 | * "borderColor": "#FF0000", 72 | * "text": "Welcome to SCE!", 73 | * "success": true 74 | * } 75 | * If the sign is unreachable, false is returned. 76 | */ 77 | async function healthCheck() { 78 | return new Promise((resolve) => { 79 | axios 80 | .get(LED_SIGN_URL + '/api/health-check') 81 | .then(({ data }) => { 82 | resolve(data); 83 | }).catch((err) => { 84 | logger.error('healthCheck had an error: ', err); 85 | MetricsHandler.sshTunnelErrors.inc({ type: 'LED' }); 86 | resolve(false); 87 | }); 88 | }); 89 | } 90 | 91 | module.exports = { updateSign, turnOffSign, healthCheck }; 92 | -------------------------------------------------------------------------------- /api/main_endpoints/util/Speaker.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const logger = require('../../util/logger'); 3 | 4 | let SPEAKER_URL = process.env.SPEAKER_URL 5 | || 'http://localhost:8000'; 6 | 7 | async function sendSpeakerRequest(path, body = {}) { 8 | return new Promise((resolve) => { 9 | axios 10 | .post(SPEAKER_URL + path, body) 11 | .then(() => { 12 | resolve(true); 13 | }) 14 | .catch((err) => { 15 | logger.error(`${path} had an error:`, err); 16 | resolve(false); 17 | }); 18 | }); 19 | } 20 | 21 | const getQueued = () => axios.get(SPEAKER_URL + '/queued'); 22 | 23 | module.exports = { sendSpeakerRequest, getQueued }; 24 | -------------------------------------------------------------------------------- /api/main_endpoints/util/captcha.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | const logger = require('../../util/logger'); 4 | const config = require('../../config/config.json'); 5 | const { MetricsHandler } = require('../../util/metrics'); 6 | 7 | async function verifyCaptcha(responseToken) { 8 | if (!responseToken) { 9 | return { success: false }; 10 | } 11 | 12 | const secretKey = config.googleApiKeys.CAPTCHA_SECRET_KEY; 13 | 14 | try { 15 | const url = new URL('https://www.google.com/recaptcha/api/siteverify'); 16 | url.searchParams.append('secret', secretKey); 17 | url.searchParams.append('response', responseToken); 18 | const response = await axios.post(url.href); 19 | return { success: response.data.success }; 20 | } catch (error) { 21 | logger.error('Error verifying captcha:', error); 22 | MetricsHandler.captchaErrors.inc(); 23 | return { success: false }; 24 | } 25 | } 26 | 27 | module.exports = { 28 | verifyCaptcha 29 | }; 30 | -------------------------------------------------------------------------------- /api/main_endpoints/util/emailHelpers.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | const MAILER_API_URL = process.env.MAILER_API_URL 4 | || 'http://localhost:8082/cloudapi'; 5 | 6 | async function sendUnsubscribeEmail(users) { 7 | let status; 8 | await axios 9 | .post(`${MAILER_API_URL}/Mailer/sendUnsubscribeEmail`, { users }) 10 | .then(res => { 11 | status = res.data; 12 | }) 13 | .catch(err => { 14 | status = err.data; 15 | }); 16 | return status; 17 | } 18 | 19 | async function sendVerificationEmail(name, email) { 20 | return new Promise((resolve) => { 21 | axios 22 | .post(`${MAILER_API_URL}/Mailer/sendVerificationEmail`, { 23 | recipientName: name, 24 | recipientEmail: email 25 | }) 26 | .then(() => resolve(true)) 27 | .catch(() => resolve(false)); 28 | }); 29 | } 30 | 31 | async function sendPasswordReset(resetToken, email) { 32 | return new Promise((resolve) => { 33 | axios 34 | .post(`${MAILER_API_URL}/Mailer/sendPasswordReset`, { 35 | resetToken: resetToken, 36 | recipientEmail: email 37 | }) 38 | .then(() => resolve(true)) 39 | .catch(() => resolve(false)); 40 | }); 41 | } 42 | 43 | module.exports = { sendUnsubscribeEmail, sendVerificationEmail, sendPasswordReset }; 44 | -------------------------------------------------------------------------------- /api/main_endpoints/util/passport.js: -------------------------------------------------------------------------------- 1 | const JwtStrategy = require('passport-jwt').Strategy; 2 | const ExtractJwt = require('passport-jwt').ExtractJwt; 3 | const User = require('../models/User'); 4 | const { secretKey } = require('../../config/config.json'); 5 | 6 | module.exports = function(passport) { 7 | const options = {}; 8 | options.jwtFromRequest = ExtractJwt.fromAuthHeaderWithScheme('jwt'); 9 | options.secretOrKey = secretKey; 10 | 11 | passport.use( 12 | new JwtStrategy(options, function(jwtPayload, done) { 13 | User.findOne({ id: jwtPayload.id }, function(error, user) { 14 | if (error) { 15 | return done(error, false); 16 | } 17 | 18 | if (user) { 19 | done(null, user); 20 | } else { 21 | done(null, false); 22 | } 23 | }); 24 | }) 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /api/main_endpoints/util/redis-client.js: -------------------------------------------------------------------------------- 1 | const { createClient } = require('redis'); 2 | const logger = require('../../util/logger'); 3 | 4 | class SceRedisClient { 5 | constructor() { 6 | const redisHostUrl = process.env.REDIS_HOST_URL; 7 | 8 | this.useRedis = !!redisHostUrl; 9 | this.client = new Map(); 10 | 11 | if (redisHostUrl) { 12 | this.client = createClient({ url: redisHostUrl }); 13 | } 14 | } 15 | 16 | async connect() { 17 | // skip connecting to redis if it's not configured 18 | // when we initialized the class 19 | if (!this.useRedis) { 20 | return; 21 | } 22 | 23 | try { 24 | await this.client.connect(); 25 | } catch (err) { 26 | logger.error('Error connecting to Redis:', err); 27 | } 28 | } 29 | 30 | async set(key, value, options = {}) { 31 | return this.client.set(key, value, options); 32 | } 33 | 34 | async get(key) { 35 | return this.client.get(key) || null; 36 | } 37 | 38 | async delete(key) { 39 | if (this.useRedis) { 40 | return this.client.del(key); 41 | } else { 42 | return this.client.delete(key) ? true : false; 43 | } 44 | } 45 | } 46 | 47 | const client = new SceRedisClient(); 48 | client.connect(); 49 | 50 | module.exports = client; 51 | -------------------------------------------------------------------------------- /api/main_endpoints/util/token-functions.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const { secretKey, DISCORD_PRINTING_KEY } = require('../../config/config.json'); 3 | const passport = require('passport'); 4 | const membershipState = require('../../util/constants').MEMBERSHIP_STATE; 5 | 6 | require('./passport')(passport); 7 | 8 | 9 | /** 10 | * Check if the request body contains a token 11 | * @param {object} request the HTTP request from the client 12 | * @returns {boolean} if the token exists in the request body 13 | */ 14 | function checkIfTokenSent(request) { 15 | try { 16 | return !!request.headers.authorization; 17 | } catch(_) { 18 | return false; 19 | } 20 | } 21 | 22 | /** 23 | * @param {object} request the HTTP request from the client 24 | */ 25 | function decodeToken(request){ 26 | try { 27 | let decodedResponse = {}; 28 | if (!request.headers.authorization || !request.headers.authorization.length) { 29 | return decodedResponse; 30 | } 31 | const token = request.headers.authorization.split('Bearer ')[1]; 32 | const userToken = token.replace(/^JWT\s/, ''); 33 | jwt.verify(userToken, secretKey, function(error, decoded) { 34 | if (!error && decoded) { 35 | decodedResponse = decoded; 36 | } 37 | }); 38 | return decodedResponse; 39 | } catch (_) { 40 | return null; 41 | } 42 | } 43 | 44 | /** 45 | * @param {object} request the HTTP request from the client 46 | */ 47 | function decodeTokenFromBodyOrQuery(request){ 48 | const token = request.body.token || request.query.token; 49 | const userToken = token.replace(/^JWT\s/, ''); 50 | let decodedResponse = {}; 51 | jwt.verify(userToken, secretKey, function(error, decoded) { 52 | if (!error && decoded) { 53 | decodedResponse = decoded; 54 | } 55 | }); 56 | return decodedResponse; 57 | } 58 | 59 | /** 60 | * Checks if the request token is valid and returns either a valid response 61 | * or undefined 62 | * @param {object} request the HTTP request from the client 63 | * @param {number} accessLevel the minimum access level to consider the token valid 64 | * @param {boolean} returnDecoded optional parameter to return the decoded 65 | * response to the user 66 | * @returns {boolean} whether the user token is valid or not 67 | */ 68 | function checkIfTokenValid(request, accessLevel = membershipState.NON_MEMBER) { 69 | let decoded = decodeToken(request); 70 | let response = decoded && decoded.accessLevel >= accessLevel; 71 | return response; 72 | } 73 | 74 | module.exports = { 75 | checkIfTokenSent, 76 | checkIfTokenValid, 77 | decodeToken, 78 | decodeTokenFromBodyOrQuery 79 | }; 80 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sce-backend", 3 | "version": "1.0.0", 4 | "description": "contains all the APIs used by the SCE website", 5 | "main": "", 6 | "scripts": { 7 | "dev": "nodemon devServer.js", 8 | "create-user": "node ./util/CreateUserScript.js", 9 | "generate-token": "node ./util/GenerateTokenScript.js", 10 | "main_endpoints": "nodemon main_endpoints/server.js", 11 | "cloud_api": "nodemon cloud_api/server.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/SCE-Development/Core-v4.git" 16 | }, 17 | "keywords": [ 18 | "node", 19 | "gmail", 20 | "mongodb", 21 | "express" 22 | ], 23 | "author": "SCE Development Team Officers", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/SCE-Development/Core-v4/issues" 27 | }, 28 | "homepage": "https://github.com/SCE-Development/Core-v4#readme", 29 | "dependencies": { 30 | "axios": "^0.21.2", 31 | "bcryptjs": "^2.4.3", 32 | "bluebird": "^3.7.2", 33 | "cors": "^2.8.5", 34 | "express": "^4.17.1", 35 | "form-data": "^4.0.0", 36 | "googleapis": "^52.1.0", 37 | "mongoose": "^5.10.0", 38 | "multer": "^1.4.5-lts.1", 39 | "nodemailer": "^6.4.11", 40 | "nodemon": "^2.0.4", 41 | "passport": "^0.4.1", 42 | "passport-jwt": "^4.0.0", 43 | "prom-client": "^15.1.3", 44 | "redis": "^4.7.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /api/util/CreateUserScript.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const User = require('../main_endpoints/models/User.js'); 3 | const inquirer = require('inquirer'); 4 | const mongoose = require('mongoose'); 5 | mongoose.Promise = require('bluebird'); 6 | let membershipState = require('../../src/Enums.js'); 7 | membershipState = membershipState.membershipState; 8 | const { consoleColors } = require('../util/constants'); 9 | 10 | const { 11 | redColor, 12 | greenColor, 13 | blueColor, 14 | defaultColor 15 | } = consoleColors; 16 | 17 | console.debug(blueColor, 'Welcome to the SCE User Creation CLI!', defaultColor); 18 | inquirer 19 | .prompt([ 20 | { 21 | type: 'input', 22 | name: 'email', 23 | message: 'What\'s your email:' 24 | }, 25 | { 26 | type: 'input', 27 | name: 'firstName', 28 | message: 'Please enter a first name for your account (default: "Dummy"):' 29 | }, 30 | { 31 | type: 'input', 32 | name: 'lastName', 33 | message: 'Please enter a last name for your account (defualt: "Account"):' 34 | }, 35 | { 36 | type: 'input', 37 | name: 'password', 38 | message: 'Please enter a password (default: "sce"):' 39 | }, 40 | { 41 | type: 'list', 42 | name: 'accountLevel', 43 | message: 'Choose an account level from the list below: ', 44 | choices: [ 45 | { 46 | name: 'Admin', 47 | value: membershipState.ADMIN 48 | }, 49 | { 50 | name: 'Officer', 51 | value: membershipState.OFFICER 52 | }, 53 | { 54 | name: 'Member', 55 | value: membershipState.MEMBER 56 | }, 57 | { 58 | name: 'Non-Member', 59 | value: membershipState.NON_MEMBER 60 | }, 61 | { 62 | name: 'Pending', 63 | value: membershipState.PENDING 64 | }, 65 | { 66 | name: 'Banned', 67 | value: membershipState.BANNED 68 | }, 69 | { 70 | name: 'Alumni', 71 | value: membershipState.ALUMNI 72 | } 73 | ], 74 | filter: function(val) { 75 | console.debug(val); 76 | return val; 77 | } 78 | } 79 | ]) 80 | .then(async (answers) => { 81 | const newUser = new User({ 82 | password: answers.password || 'sce', 83 | firstName: answers.firstName || 'Dummy', 84 | lastName: answers.lastName || 'Account', 85 | email: answers.email, 86 | emailVerified: true, 87 | accessLevel: answers.accountLevel 88 | }); 89 | this.mongoose = mongoose; 90 | this.mongoose 91 | .connect('mongodb://127.0.0.1/sce_core', { 92 | promiseLibrary: require('bluebird'), 93 | useNewUrlParser: true, 94 | serverSelectionTimeoutMS: 3000, 95 | useUnifiedTopology: true, 96 | useCreateIndex: true 97 | }) 98 | .catch(_ => { 99 | console.debug(redColor, 'MongoDB Connection Unsuccessful. Are you sure' 100 | + ' MongoDB is running?', defaultColor); 101 | process.exit(); 102 | }); 103 | await newUser.save() 104 | .then(_ => { 105 | console.debug(greenColor, 'Account with email ' + answers.email 106 | + ' and password ' 107 | + ((answers.password) ? answers.password : 'sce') 108 | + ' succesfully created.', defaultColor); 109 | this.mongoose.connection.close(); 110 | }) 111 | .catch(_=> { 112 | console.debug(redColor, 'Account creation unsuccessful. Please try' 113 | + ' running the user creation script' 114 | + ' again.', defaultColor); 115 | this.mongoose.connection.close(); 116 | process.exit(); 117 | }); 118 | }) 119 | .catch(error => { 120 | if(error.isTTyError) { 121 | console.debug(redColor, error, 'prompt couldn\'t be rendered in tty' 122 | + 'environment.', defaultColor); 123 | } else { 124 | console.debug(redColor, error, 'error processing response.', 125 | defaultColor); 126 | } 127 | }); 128 | -------------------------------------------------------------------------------- /api/util/GenerateTokenScript.js: -------------------------------------------------------------------------------- 1 | const { AuthManager } = require('../cloud_api/util/AuthManager'); 2 | const { SceGoogleApiHandler } = 3 | require('../cloud_api/util/SceGoogleApiHandler'); 4 | 5 | const authManager = new AuthManager(); 6 | const configPath = __dirname + '/../config/config.json'; 7 | authManager.setAuthCredentials(configPath, () => { 8 | const scopes = [ 9 | 'https://mail.google.com/', 10 | ]; 11 | const tokenPath = __dirname + '/../config/token.json'; 12 | const apiHandler = new SceGoogleApiHandler( 13 | scopes, 14 | tokenPath, 15 | ); 16 | 17 | if (apiHandler) { 18 | apiHandler.getNewToken(true); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /api/util/PathParser.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | /** 5 | * The special object to represent the pair of endpoint name and file path. 6 | */ 7 | class EndpointPair { 8 | /** 9 | * Create an EndpointPair 10 | * @param {String} endpointName The name of an API endpoint, e.g. Event, or 11 | * LedSign. 12 | * @param {String} filePath The path to a file of the given endpoint name, 13 | * e.g. api/routes/Event.js or client/led_sign/led_sign_client.js 14 | */ 15 | constructor(endpointName, filePath) { 16 | this.endpointName = endpointName; 17 | this.filePath = filePath; 18 | } 19 | } 20 | 21 | class PathParser { 22 | /** 23 | * Asynchronously read all the files in a directory. It does not do a 24 | * recursive search of subdirectories and returns an array of endpoint names 25 | * and file paths. 26 | * @param {string} directoryPath The path to a directory with API files in it. 27 | * @returns {Promise>} Resolves with the array of 28 | * EndpointPairs for each file in a directory. 29 | */ 30 | static async getFilesInDirectory(directoryPath) { 31 | return new Promise((resolve, reject) => { 32 | let pairs = []; 33 | fs.readdir(directoryPath, async (err, files) => { 34 | if (!err) { 35 | files.map((file) => { 36 | if (!fs.lstatSync(directoryPath + file).isDirectory()) { 37 | const pathObject = path.parse(path.join(directoryPath, file)); 38 | const endpointName = pathObject.name; 39 | const endpointPair = new EndpointPair( 40 | endpointName, path.join( 41 | pathObject.dir, pathObject.base 42 | )); 43 | pairs.push(endpointPair); 44 | } 45 | }); 46 | resolve(pairs); 47 | } 48 | }); 49 | }); 50 | } 51 | 52 | /** 53 | * Takes in a path and generates EndpointPairs if the path is either a 54 | * directory or single file. 55 | * @param {String} filePath The path to a directory or file. 56 | * @returns {Promise>} The pairs of API endpoint 57 | * names and file paths from the specified path(s). 58 | */ 59 | static async generatePairsFromFilePath(filePath) { 60 | let pairs = []; 61 | if (fs.lstatSync(filePath).isDirectory()) { 62 | pairs = await this.getFilesInDirectory(filePath); 63 | } else { 64 | const pathObject = path.parse(filePath); 65 | const endpointName = pathObject.name; 66 | const endpointPair = new EndpointPair( 67 | endpointName, path.join( 68 | pathObject.dir, pathObject.base 69 | )); 70 | pairs.push(endpointPair); 71 | } 72 | return pairs; 73 | } 74 | 75 | /** 76 | * Take a path or array of paths from SceHttpServer and resolve 77 | * the API endpoint names and filepaths for the class to use. 78 | * @param {(String|Array)} pathFromServer Path(s) supplied to 79 | * SceHttpServer. 80 | * @returns {Promise>} All of the pairs of API endpoint 81 | * names and file paths from the specified path(s). 82 | */ 83 | static async parsePath(pathFromServer) { 84 | let result = []; 85 | if (Array.isArray(pathFromServer)) { 86 | await Promise.all( 87 | pathFromServer.map(async (path) => { 88 | const generatedPairs = await this.generatePairsFromFilePath(path); 89 | result = result.concat(generatedPairs); 90 | }) 91 | ); 92 | } else { 93 | result = await this.generatePairsFromFilePath(pathFromServer); 94 | } 95 | return result; 96 | } 97 | } 98 | 99 | module.exports = { PathParser }; 100 | -------------------------------------------------------------------------------- /api/util/SceHttpServer.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const cors = require('cors'); 4 | const http = require('http'); 5 | const mongoose = require('mongoose'); 6 | mongoose.Promise = require('bluebird'); 7 | const url = require('url'); 8 | 9 | const { PathParser } = require('./PathParser'); 10 | const logger = require('./logger'); 11 | const { MetricsHandler, register } = require('./metrics'); 12 | 13 | /** 14 | * Class responsible for resolving paths of API endpoints and combining them 15 | * them into an express server. 16 | */ 17 | class SceHttpServer { 18 | /** 19 | * Store port information, create express server object and configure 20 | * BodyParser options. 21 | * @param {(String|Array)} pathToEndpoints The path to a single 22 | * server file, directory or array of directories/files; 23 | * @param {Number} port The port for the server to listen on. 24 | * @param {String} prefix The prefix of the api endpoints to send requests 25 | * to, e.g. /api/Event/addEvent, with /api/ being the prefix. 26 | */ 27 | constructor(pathToEndpoints, port, prefix = '/api/') { 28 | const testEnv = process.env.NODE_ENV === 'test'; 29 | this.database = testEnv ? 'sce_core_test' : 'sce_core'; 30 | this.port = port; 31 | this.pathToEndpoints = pathToEndpoints; 32 | this.prefix = prefix; 33 | this.app = express(); 34 | this.app.locals.title = 'Core v4'; 35 | this.app.locals.email = 'test@test.com'; 36 | 37 | this.app.use(cors()); 38 | this.app.use( 39 | bodyParser.json({ 40 | // support JSON-encoded request bodies 41 | limit: '50mb', 42 | strict: true, 43 | }) 44 | ); 45 | this.app.use( 46 | bodyParser.urlencoded({ 47 | // support URL-encoded request bodies 48 | limit: '50mb', 49 | extended: true, 50 | }) 51 | ); 52 | } 53 | 54 | async init() { 55 | this.registerMetricsMiddleware(); 56 | await this.initializeEndpoints(); 57 | } 58 | 59 | registerMetricsMiddleware() { 60 | this.app.use((req, res, next) => { 61 | res.on('finish', () => { 62 | // req.originalUrl can look like /api/path?key=value 63 | const parsedUrl = url.parse(req.originalUrl, true); 64 | MetricsHandler.endpointHits.inc({ 65 | method: req.method, 66 | // using the above example, pathname looks like '/api/path' 67 | route: parsedUrl.pathname, 68 | statusCode: res.statusCode, 69 | }); 70 | }); 71 | next(); 72 | }); 73 | 74 | // metrics 75 | this.app.get('/metrics', async (_, res) => { 76 | res.setHeader('Content-Type', register.contentType); 77 | res.end(await register.metrics()); 78 | }); 79 | } 80 | 81 | /** 82 | * This function is responsible for taking the pathToEndpoints instance 83 | * variable and resolving API endpoints from it. 84 | */ 85 | async initializeEndpoints() { 86 | const requireList = await PathParser.parsePath(this.pathToEndpoints); 87 | requireList.map((route) => { 88 | try { 89 | this.app.use(this.prefix + route.endpointName, require(route.filePath)); 90 | } catch (e) { 91 | logger.error( 92 | `error importing ${route.filePath} to handle: ${route.endpointName}:`, 93 | e 94 | ); 95 | } 96 | }); 97 | } 98 | 99 | /** 100 | * Create the http server, connect to MongoDB and start listening on 101 | * the supplied port. 102 | */ 103 | openConnection() { 104 | const { port } = this; 105 | this.server = http.createServer(this.app); 106 | this.connectToMongoDb(); 107 | this.server.listen(port, function() { 108 | console.debug(`Now listening on port ${port}`); 109 | }); 110 | } 111 | 112 | /** 113 | * Initialize a connection to MongoDB. 114 | */ 115 | connectToMongoDb() { 116 | let dbHost = process.env.DATABASE_HOST || '127.0.0.1'; 117 | this.mongoose = mongoose; 118 | this.mongoose 119 | .connect(`mongodb://${dbHost}:27017/${this.database}`, { 120 | promiseLibrary: require('bluebird'), 121 | useNewUrlParser: true, 122 | useUnifiedTopology: true, 123 | useCreateIndex: true, 124 | }) 125 | .then(() => { }) 126 | .catch((error) => { 127 | throw error; 128 | }); 129 | } 130 | 131 | /** 132 | * Return the current instance of the HTTP server. This function is useful 133 | * for making chai HTTP requests in our API testing files. 134 | * @returns {http.Server} The current instance of the HTTP server. 135 | */ 136 | getServerInstance() { 137 | return this.server; 138 | } 139 | 140 | /** 141 | * Close the connection to MongoDB and stop the server. 142 | * @param {Function|null} done A function supplied by mocha as a callback to 143 | * signify that we have completed stopping the server. 144 | */ 145 | closeConnection(done = null) { 146 | this.server.close(); 147 | this.mongoose.connection.close(done); 148 | } 149 | } 150 | 151 | module.exports = { SceHttpServer }; 152 | -------------------------------------------------------------------------------- /api/util/constants.js: -------------------------------------------------------------------------------- 1 | const STATUS_CODES = { 2 | OK: 200, 3 | BAD_REQUEST: 400, 4 | UNAUTHORIZED: 401, 5 | FORBIDDEN: 403, 6 | NOT_FOUND: 404, 7 | CONFLICT: 409, 8 | SERVER_ERROR: 500, 9 | }; 10 | 11 | const MEMBERSHIP_STATE = { 12 | BANNED: -2, 13 | PENDING: -1, 14 | NON_MEMBER: 0, 15 | ALUMNI: 0.5, 16 | MEMBER: 1, 17 | OFFICER: 2, 18 | ADMIN: 3, 19 | }; 20 | 21 | const MESSAGES_API = { 22 | MAX_AMOUNT_OF_CONNECTIONS: 3 23 | }; 24 | 25 | const consoleColors = { 26 | redColor: '\x1b[31m', 27 | greenColor: '\x1b[32m', 28 | blueColor: '\x1b[34m', 29 | yellowColor: '\x1b[33m', 30 | defaultColor: '\x1b[0m', 31 | }; 32 | 33 | // this is set to 12 hours by: 34 | // 60 seconds per minute * 60 minutes per hour * 12 hours 35 | const PASSWORD_RESET_EXPIRATION = 60 * 60 * 12; 36 | 37 | module.exports = { 38 | STATUS_CODES, 39 | MEMBERSHIP_STATE, 40 | MESSAGES_API, 41 | consoleColors, 42 | PASSWORD_RESET_EXPIRATION, 43 | }; 44 | -------------------------------------------------------------------------------- /api/util/logger.js: -------------------------------------------------------------------------------- 1 | // dont get how this works? check out 2 | // https://www.youtube.com/watch?v=wPIT00eMEzg 3 | 4 | const consoleColors = { 5 | red: '\x1b[31m', 6 | green: '\x1b[32m', 7 | blue: '\x1b[34m', 8 | yellow: '\x1b[33m', 9 | default: '\x1b[0m', 10 | }; 11 | 12 | const util = require('util'); 13 | 14 | const LOG_LEVELS = { 15 | DEBUG: 'DEBUG', 16 | INFO: 'INFO', 17 | WARNING: 'WARNING', 18 | ERROR: 'ERROR', 19 | }; 20 | 21 | function getLineOfCode() { 22 | const e = new Error(); 23 | // point out that 2 is for parent, 3 for grandparent etc... 24 | // yields "at Object. (/path/to/file.js:7:1)" 25 | let backslash = '/'; 26 | if (process.platform === 'win32') { 27 | backslash = '\\'; 28 | } 29 | const parentFunc = e.stack.split('\n')[4]; 30 | const fileAndLineOfCode = parentFunc 31 | .split(backslash) // yields "file.js:7:1)" 32 | .reverse()[0] // yields "file.js:7:1" 33 | .split(')')[0]; // yields "file.js:7:1" 34 | return fileAndLineOfCode; 35 | } 36 | 37 | // Do not call this function directly! Use the below factory 38 | // functions instead 39 | function printToConsole(level, ...message) { 40 | let formattedMessage = new Date().toISOString(); 41 | const args = util.format(...message); 42 | formattedMessage += ` ${getLineOfCode()}`; 43 | formattedMessage += ` ${level}`; 44 | formattedMessage += ` ${args}`; 45 | // Print the log as red or green if the level is error or warning 46 | if (level === LOG_LEVELS.ERROR) { 47 | // eslint-disable-next-line max-len 48 | formattedMessage = `${consoleColors.red}${formattedMessage}${consoleColors.default}`; 49 | } else if (level === LOG_LEVELS.WARNING) { 50 | // eslint-disable-next-line max-len 51 | formattedMessage = `${consoleColors.yellow}${formattedMessage}${consoleColors.default}`; 52 | } 53 | // eslint-disable-next-line no-console 54 | console.log(formattedMessage); 55 | } 56 | 57 | function debug(...message) { 58 | printToConsole(LOG_LEVELS.DEBUG, ...message); 59 | } 60 | 61 | function info(...message) { 62 | printToConsole(LOG_LEVELS.INFO, ...message); 63 | } 64 | 65 | function warn(...message) { 66 | printToConsole(LOG_LEVELS.WARNING, ...message); 67 | } 68 | 69 | function error(...message) { 70 | printToConsole(LOG_LEVELS.ERROR, ...message); 71 | } 72 | 73 | module.exports = { debug, info, warn, error }; 74 | -------------------------------------------------------------------------------- /api/util/metrics.js: -------------------------------------------------------------------------------- 1 | const client = require('prom-client'); 2 | 3 | const register = new client.Registry(); 4 | class MetricsHandler { 5 | endpointHits = new client.Counter({ 6 | name: 'endpoint_hits', 7 | help: 'Counter for tracking endpoint hits with status codes', 8 | labelNames: ['method', 'route', 'statusCode'], 9 | }); 10 | 11 | emailSent = new client.Counter({ 12 | name: 'email_sent', 13 | help: 'Counter for tracking emails sent', 14 | labelNames: ['type'], 15 | }); 16 | 17 | captchaVerificationErrors = new client.Counter({ 18 | name: 'captcha_verification_errors', 19 | help: 'Counter for tracking captcha verification errors', 20 | }); 21 | 22 | sshTunnelErrors = new client.Counter({ 23 | name: 'ssh_tunnel_errors', 24 | help: 'Counter for tracking ssh tunnel errors', 25 | labelNames: ['type'], 26 | }); 27 | 28 | totalMessagesSent = new client.Counter({ 29 | name: 'total_messages_sent', 30 | help: 'Total number of messages sent' 31 | }); 32 | 33 | currentConnectionsOpen = new client.Gauge({ 34 | name: 'current_connections_open', 35 | help: 'Total number of connections open', 36 | labelNames: ['id'] 37 | }); 38 | 39 | totalChatMessagesPerChatRoom = new client.Counter({ 40 | name: 'total_chat_messages_per_chatroom', 41 | help: 'Total number of messages sent per chatroom', 42 | labelNames: ['id'] 43 | }); 44 | 45 | constructor() { 46 | register.setDefaultLabels({ 47 | app: 'sce-core', 48 | }); 49 | client.collectDefaultMetrics({ register }); 50 | 51 | Object.keys(this).forEach(metric => { 52 | register.registerMetric(this[metric]); 53 | }); 54 | } 55 | } 56 | 57 | module.exports = { 58 | MetricsHandler: new MetricsHandler(), 59 | register, 60 | }; 61 | -------------------------------------------------------------------------------- /api/util/token-verification.js: -------------------------------------------------------------------------------- 1 | const { DISCORD_COREV4_KEY } = require('../config/config.json'); 2 | 3 | /** 4 | * Checks API key value and return true or false depending on if it matches 5 | * @param {String} apiKey 6 | * @returns {boolean} whether the api key was valid or not 7 | */ 8 | function checkDiscordKey(apiKey) { 9 | return apiKey === DISCORD_COREV4_KEY; 10 | } 11 | module.exports = { checkDiscordKey }; 12 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | services: 2 | redis: 3 | container_name: sce-redis-dev 4 | image: redis:latest 5 | ports: 6 | - '6379:6379' 7 | restart: always 8 | volumes: 9 | - sce_redis_data_dev:/data/redis 10 | mongodb: 11 | container_name: sce-mongodb-dev 12 | restart: always 13 | image: 'mongo' 14 | ports: 15 | - '27017:27017' 16 | volumes: 17 | - sce_mongodb_data_dev:/data/db 18 | command: 'mongod --quiet --logpath /dev/null' 19 | environment: 20 | - CHOKIDAR_USEPOLLING=${CHOKIDAR_USEPOLLING} 21 | - WATCHPACK_POLLING=${WATCHPACK_POLLING} 22 | sce-cloud-api: 23 | container_name: sce-cloud-api-dev 24 | image: cloudapi:latest 25 | build: 26 | context: ./api 27 | dockerfile: ./cloud_api/Dockerfile.dev 28 | environment: 29 | - GENERAL_API_URL=http://sce-main-endpoints-dev:8080/api 30 | - VERIFICATION_BASE_URL=http://localhost 31 | - DATABASE_HOST=sce-mongodb-dev 32 | - NODE_ENV=development 33 | - CHOKIDAR_USEPOLLING=${CHOKIDAR_USEPOLLING} 34 | - WATCHPACK_POLLING=${WATCHPACK_POLLING} 35 | ports: 36 | - '8082:8082' 37 | restart: 'on-failure' 38 | stdin_open: true 39 | links: 40 | - mongodb 41 | depends_on: 42 | - mongodb 43 | volumes: 44 | - ./api/config:/app/config 45 | - ./api/package.json:/app/package.json 46 | - ./api/util:/app/util/ 47 | - ./api/cloud_api:/app/cloud_api 48 | main-endpoints: 49 | container_name: sce-main-endpoints-dev 50 | build: 51 | context: ./api 52 | dockerfile: ./main_endpoints/Dockerfile.dev 53 | environment: 54 | - MAIN_ENDPOINT_URL=sce-main-endpoints-dev:8080 55 | - CLEEZY_URL=http://host.docker.internal:8000 56 | - PRINTER_URL=http://host.docker.internal:14000 57 | - LED_SIGN_URL=http://host.docker.internal:11000 58 | - DISCORD_REDIRECT_URI=http://localhost/api/user/callback 59 | - MAILER_API_URL=http://sce-cloud-api-dev:8082/cloudapi 60 | - DATABASE_HOST=sce-mongodb-dev 61 | - REDIS_HOST_URL=redis://sce-redis-dev:6379 62 | - NODE_ENV=development 63 | - CHOKIDAR_USEPOLLING=${CHOKIDAR_USEPOLLING} 64 | - WATCHPACK_POLLING=${WATCHPACK_POLLING} 65 | ports: 66 | - '8080:8080' 67 | restart: 'on-failure' 68 | stdin_open: true 69 | links: 70 | - mongodb 71 | - redis 72 | depends_on: 73 | - mongodb 74 | - redis 75 | volumes: 76 | - ./api/config:/app/config 77 | - ./api/package.json:/app/package.json 78 | - ./api/util:/app/util/ 79 | - ./api/main_endpoints:/app/main_endpoints 80 | 81 | frontend: 82 | container_name: sce-frontend-dev 83 | build: 84 | context: . 85 | dockerfile: ./docker/Dockerfile.dev 86 | environment: 87 | - NODE_ENV=dev 88 | - REACT_APP_BASE_API_URL=http://localhost 89 | - NODE_ENV=development 90 | - CHOKIDAR_USEPOLLING=${CHOKIDAR_USEPOLLING} 91 | - WATCHPACK_POLLING=${WATCHPACK_POLLING} 92 | 93 | ports: 94 | - '3000:3000' 95 | restart: 'on-failure' 96 | stdin_open: true 97 | links: 98 | - mongodb 99 | depends_on: 100 | - mongodb 101 | volumes: 102 | - ./package.json:/frontend/package.json 103 | - ./src:/frontend/src/ 104 | - ./public:/frontend/public/ 105 | 106 | nginx: 107 | container_name: sce-nginx-dev 108 | image: 'nginx' 109 | volumes: 110 | - ./nginx.dev.conf:/etc/nginx/nginx.conf:ro 111 | command: [nginx-debug, '-g', 'daemon off;'] 112 | ports: 113 | - '80:80' 114 | restart: 'on-failure' 115 | depends_on: 116 | - frontend 117 | environment: 118 | - CHOKIDAR_USEPOLLING=${CHOKIDAR_USEPOLLING} 119 | - WATCHPACK_POLLING=${WATCHPACK_POLLING} 120 | 121 | volumes: 122 | sce_mongodb_data_dev: 123 | sce_redis_data_dev: 124 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | redis: 5 | container_name: sce-redis 6 | image: redis:latest 7 | restart: always 8 | volumes: 9 | - sce_redis_data:/data/redis 10 | mongodb: 11 | container_name: sce-mongodb 12 | restart: always 13 | image: 'mongo' 14 | volumes: 15 | - '/data/db:/data/db' 16 | command: 'mongod' 17 | keep-ssh-tunnel-open: 18 | container_name: keep-ssh-tunnel-open 19 | build: 20 | context: ./tunnel 21 | dockerfile: ./Dockerfile 22 | extra_hosts: 23 | - "host.docker.internal:host-gateway" 24 | sce-cloud-api: 25 | container_name: sce-cloud-api 26 | build: 27 | context: ./api 28 | dockerfile: ./cloud_api/Dockerfile 29 | environment: 30 | - GENERAL_API_URL=http://sce-main-endpoints:8080/api 31 | - VERIFICATION_BASE_URL=https://sce.sjsu.edu 32 | - DATABASE_HOST=sce-mongodb 33 | - NODE_ENV=production 34 | restart: 'on-failure' 35 | stdin_open: true 36 | links: 37 | - mongodb 38 | depends_on: 39 | - mongodb 40 | main-endpoints: 41 | container_name: sce-main-endpoints 42 | build: 43 | context: ./api 44 | dockerfile: ./main_endpoints/Dockerfile 45 | environment: 46 | - CLEEZY_URL=http://cleezy-app.sce:8000 47 | - PRINTER_URL=http://host.docker.internal:14000 48 | - LED_SIGN_URL=http://host.docker.internal:11000 49 | - SPEAKER_URL=http://host.docker.internal:18000 50 | - DISCORD_REDIRECT_URI=https://sce.sjsu.edu/api/user/callback 51 | - MAILER_API_URL=http://sce-cloud-api:8082/cloudapi 52 | - DATABASE_HOST=sce-mongodb 53 | - REDIS_HOST_URL=redis://sce-redis:6379 54 | - NODE_ENV=production 55 | volumes: 56 | - ./api/main_endpoints/routes/printing:/app/main_endpoints/routes/printing:rw 57 | restart: 'on-failure' 58 | stdin_open: true 59 | links: 60 | - mongodb 61 | depends_on: 62 | - mongodb 63 | extra_hosts: 64 | - "host.docker.internal:host-gateway" 65 | frontend: 66 | container_name: sce-frontend 67 | build: 68 | context: . 69 | dockerfile: ./docker/Dockerfile 70 | restart: 'on-failure' 71 | stdin_open: true 72 | links: 73 | - mongodb 74 | depends_on: 75 | - mongodb 76 | environment: 77 | - NODE_ENV=production 78 | prometheus: 79 | image: prom/prometheus:latest 80 | restart: always 81 | volumes: 82 | - /etc/localtime:/etc/localtime:ro 83 | - /etc/timezone:/etc/timezone:ro 84 | - ./prometheus:/etc/prometheus 85 | ports: 86 | - '127.0.0.1:9090:9090' 87 | command: 88 | - '--config.file=/etc/prometheus/prometheus.yml' 89 | cadvisor: 90 | image: gcr.io/cadvisor/cadvisor:latest 91 | restart: always 92 | volumes: 93 | - /etc/localtime:/etc/localtime:ro 94 | - /etc/timezone:/etc/timezone:ro 95 | - /:/rootfs:ro 96 | - /var/run:/var/run:rw 97 | - /sys:/sys:ro 98 | - /var/lib/docker:/var/lib/docker:ro 99 | - /var/run/docker.sock:/var/run/docker.sock:rw 100 | devices: 101 | - /dev/kmsg:/dev/kmsg 102 | clark-node-exporter: 103 | image: quay.io/prometheus/node-exporter:latest 104 | command: 105 | - '--path.rootfs=/host' 106 | pid: host 107 | restart: unless-stopped 108 | volumes: 109 | - '/:/host:ro,rslave' 110 | nginx: 111 | image: 'nginx' 112 | volumes: 113 | - ./nginx.conf:/etc/nginx/nginx.conf:ro 114 | - ./sce_sjsu_edu_cert.cer:/etc/nginx/sce_sjsu_edu_cert.cer 115 | - ./sce.key:/etc/nginx/sce.key 116 | command: [nginx-debug, '-g', 'daemon off;'] 117 | ports: 118 | - '80:80' 119 | - '443:443' 120 | restart: 'on-failure' 121 | depends_on: 122 | - frontend 123 | 124 | networks: 125 | default: 126 | external: 127 | name: sce 128 | 129 | volumes: 130 | sce_redis_data: 131 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build steps for production frontend 2 | FROM node:20.10.0-alpine as builder 3 | 4 | WORKDIR /frontend 5 | 6 | ARG NODE_ENV 7 | 8 | COPY ./package.json /frontend/package.json 9 | COPY ./package-lock.json /frontend/package-lock.json 10 | 11 | # We don't pass the environment variables as an argument 12 | # and instead define them inline here. We do this as multi-stage 13 | # builds and environment variables from docker-compose.yml weren't working. 14 | ENV REACT_APP_BASE_API_URL https://sce.sjsu.edu 15 | 16 | RUN npm install 17 | 18 | COPY public ./public 19 | 20 | COPY src ./src 21 | 22 | COPY tailwind.config.js ./tailwind.config.js 23 | 24 | RUN npx tailwindcss build -i src/input.css -o public/output.css --minify 25 | 26 | RUN npm run build --production 27 | 28 | # stage 2 29 | FROM nginx:alpine 30 | 31 | COPY ./docker/nginx.conf /etc/nginx/conf.d/default.conf 32 | COPY --from=builder /frontend/build /usr/share/nginx/html 33 | 34 | EXPOSE 3000 35 | 36 | CMD ["nginx", "-g", "daemon off;"] 37 | -------------------------------------------------------------------------------- /docker/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:20.10.0-alpine as builder 2 | 3 | WORKDIR /frontend 4 | 5 | COPY ./package.json /frontend/package.json 6 | COPY ./package-lock.json /frontend/package-lock.json 7 | 8 | ARG NODE_ENV 9 | 10 | ARG REACT_APP_BASE_API_URL 11 | 12 | # see https://stackoverflow.com/a/61215597 13 | ENV DANGEROUSLY_DISABLE_HOST_CHECK true 14 | 15 | RUN npm install 16 | 17 | COPY public ./public 18 | 19 | COPY src ./src 20 | 21 | COPY tailwind.config.js ./tailwind.config.js 22 | 23 | RUN npx tailwindcss build -i src/input.css -o public/output.css --minify 24 | 25 | CMD ["npm", "start"] 26 | -------------------------------------------------------------------------------- /docker/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 3000; 3 | 4 | location / { 5 | root /usr/share/nginx/html; 6 | index index.html; 7 | try_files $uri $uri/ /index.html =404; 8 | } 9 | include /etc/nginx/extra-conf.d/*.conf; 10 | } 11 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | http { 2 | # Run nginx with "npm run start-production" 3 | # Stop nginx with "npm run stop-production" 4 | # Reload nginx without stopping server "npm reload-production" 5 | 6 | #logging 7 | access_log /var/log/nginx/sce.access.log; 8 | 9 | 10 | # when we run docker-compose up for the first time, a default 11 | # network is created that all the containers are attached to. 12 | # this default network name is the directory name + "_default". 13 | # As a result, the network name will be "clark_default". 14 | # To establish a connection to the containers, we can do 15 | # ., and that's what happens below. 16 | upstream webserver { 17 | server sce-frontend:3000; 18 | } 19 | upstream mainendpoints { 20 | server sce-main-endpoints:8080; 21 | } 22 | 23 | server { 24 | #re-routing http to https server 25 | listen 80 default_server; 26 | listen [::]:80 default_server; 27 | server_name _; 28 | return 301 https://$host$request_uri; 29 | } 30 | 31 | # actual nginx server 32 | server { 33 | 34 | #443(https) 35 | listen 443 ssl; 36 | 37 | # ssl certificate 38 | ssl_certificate sce_sjsu_edu_cert.cer; 39 | ssl_certificate_key sce.key; 40 | # TLS protocol (remember to update to the newest protocols for best security) 41 | ssl_protocols TLSv1.2 TLSv1.3; 42 | 43 | location ~ /s/(.*)$ { 44 | proxy_set_header X-Original-URL "$scheme://$host$request_uri"; 45 | proxy_set_header X-Base-URL "$scheme://$host"; 46 | proxy_pass http://cleezy-nginx.sce; 47 | } 48 | 49 | location ~ /qr/(.*)$ { 50 | proxy_set_header X-Original-URL "$scheme://$host$request_uri"; 51 | proxy_set_header X-Base-URL "$scheme://$host"; 52 | proxy_pass http://cleezy-nginx.sce; 53 | } 54 | 55 | location ~ ^/transit/ { 56 | resolver 127.0.0.11 valid=15s; 57 | 58 | proxy_set_header Host $host; 59 | set $upstream http://sceta-nginx.sce; 60 | proxy_pass $upstream; 61 | 62 | rewrite ^/transit(.*)$ $1 break; 63 | } 64 | 65 | location ~ ^/transit$ { 66 | return 302 $scheme://$http_host/transit/; 67 | } 68 | 69 | #Load balancer 70 | location /api { 71 | proxy_pass http://mainendpoints; 72 | } 73 | location / { 74 | proxy_pass http://webserver; 75 | } 76 | } 77 | } 78 | 79 | events { } 80 | -------------------------------------------------------------------------------- /nginx.dev.conf: -------------------------------------------------------------------------------- 1 | http { 2 | # Run nginx with "npm run start-production" 3 | # Stop nginx with "npm run stop-production" 4 | # Reload nginx without stopping server "npm reload-production" 5 | 6 | #logging 7 | access_log /var/log/nginx/sce.access.log; 8 | 9 | # when we run docker-compose up for the first time, a default 10 | # network is created that all the containers are attached to. 11 | # this default network name is the directory name + "_default". 12 | # As a result, the network name will be "clark_default". 13 | # To establish a connection to the containers, we can do 14 | # ., and that's what happens below. 15 | upstream webserver { 16 | server sce-frontend-dev.clark_default:3000; 17 | } 18 | upstream main_endpoints { 19 | server sce-main-endpoints-dev.clark_default:8080; 20 | } 21 | upstream sce-cloud-api { 22 | server sce-cloud-api-dev.clark_default:8082; 23 | } 24 | 25 | # actual nginx server 26 | server { 27 | listen 80; 28 | 29 | #Load balancer 30 | location /api { 31 | proxy_pass http://main_endpoints; 32 | } 33 | location /cloudapi { 34 | proxy_pass http://sce-cloud-api; 35 | } 36 | # see https://stackoverflow.com/a/59816181 37 | location /sockjs-node { 38 | proxy_pass http://webserver; 39 | proxy_redirect default; 40 | proxy_set_header Upgrade $http_upgrade; 41 | proxy_set_header Connection "upgrade"; 42 | proxy_set_header Host $host; 43 | proxy_set_header X-Real-IP $remote_addr; 44 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 45 | proxy_set_header X-Forwarded-Host $server_name; 46 | 47 | # the following two timeout rules fix CRA WDS disconnects after 60s 48 | proxy_read_timeout 86400s; 49 | proxy_send_timeout 86400s; 50 | } 51 | location / { 52 | proxy_pass http://webserver; 53 | } 54 | } 55 | } 56 | 57 | events { } 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sce_core", 3 | "version": "4.0.0", 4 | "description": "The SCE Website", 5 | "scripts": { 6 | "api-test": "mocha --exit test/api/*.js", 7 | "frontend-test": "mocha --require @babel/register --require ignore-styles test/frontend/*.js --exit", 8 | "start": "react-scripts start", 9 | "server": "npm run dev --prefix api", 10 | "lint": "eslint .", 11 | "server-install": "npm install --prefix api", 12 | "build": "react-scripts build", 13 | "create-user": "npm run create-user --prefix api", 14 | "generate-token": "npm run generate-token --prefix api" 15 | }, 16 | "keywords": [ 17 | "sce_core", 18 | "SCE", 19 | "sce", 20 | "core", 21 | "v4", 22 | "4", 23 | "Software", 24 | "Computer", 25 | "Engineering" 26 | ], 27 | "author": "SCE Development Team Officers", 28 | "license": "MIT", 29 | "dependencies": { 30 | "@cfaester/enzyme-adapter-react-18": "^0.8.0", 31 | "axios": "^0.21.0", 32 | "bcryptjs": "^2.4.3", 33 | "bluebird": "^3.7.2", 34 | "cors": "^2.8.5", 35 | "enzyme": "^3.11.0", 36 | "googleapis": "^126.0.1", 37 | "jsonwebtoken": "^9.0.2", 38 | "mongoose": "^5.11.18", 39 | "multer": "^1.4.5-lts.1", 40 | "nodemailer": "^6.9.5", 41 | "passport": "^0.6.0", 42 | "passport-jwt": "^4.0.1", 43 | "pdf-lib": "^1.16.0", 44 | "prom-client": "^15.1.3", 45 | "react": "^18.2.0", 46 | "react-dom": "^18.2.0", 47 | "react-error-overlay": "^6.0.9", 48 | "react-google-recaptcha": "^2.1.0", 49 | "react-iframe": "^1.8.0", 50 | "react-router-dom": "^5.2.0", 51 | "react-scripts": "5.0.1", 52 | "react-transition-group": "^4.4.2", 53 | "universal-cookie": "^4.0.4" 54 | }, 55 | "devDependencies": { 56 | "@babel/core": "^7.16.7", 57 | "@babel/eslint-parser": "^7.16.7", 58 | "@babel/plugin-proposal-class-properties": "^7.16.7", 59 | "@babel/preset-env": "^7.16.11", 60 | "@babel/preset-react": "^7.16.7", 61 | "@babel/register": "^7.17.7", 62 | "chai": "^4.3.0", 63 | "chai-http": "^4.3.0", 64 | "daisyui": "^4.4.24", 65 | "eslint": "^7.32.0", 66 | "ignore-styles": "^5.0.1", 67 | "inquirer": "^8.2.4", 68 | "jsdom": "^19.0.0", 69 | "jsdom-global": "^3.0.2", 70 | "mocha": "^8.1.1", 71 | "proxyquire": "^2.1.3", 72 | "redis": "^4.7.0", 73 | "sinon": "^13.0.1", 74 | "tailwindcss": "^3.4.0" 75 | }, 76 | "browserslist": [ 77 | ">0.2%", 78 | "not dead", 79 | "not ie <= 11", 80 | "not op_mini all" 81 | ], 82 | "proxy": "http://localhost:8080/" 83 | } 84 | -------------------------------------------------------------------------------- /prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 5s 3 | 4 | scrape_configs: 5 | - job_name: 'cleezy' 6 | static_configs: 7 | - targets: ['cleezy-app:8000'] 8 | - job_name: 'sceta-server' 9 | static_configs: 10 | - targets: ['server:8001'] 11 | - job_name: 'cadvisor' 12 | static_configs: 13 | - targets: ['cadvisor:8080'] 14 | - job_name: 'clark-node-exporter' 15 | static_configs: 16 | - targets: ['clark-node-exporter:9100'] 17 | - job_name: 'clark' 18 | static_configs: 19 | - targets: ['main-endpoints:8080'] 20 | # the below is for discord bot 21 | - job_name: 'sarah' 22 | static_configs: 23 | - targets: ['sarah:9000'] 24 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SCE-Development/Clark/46801ff13fd8842b17e29fdd99094097800292da/public/favicon.ico -------------------------------------------------------------------------------- /public/images/SCE-glow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SCE-Development/Clark/46801ff13fd8842b17e29fdd99094097800292da/public/images/SCE-glow.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Software & Computer Engineering Society 10 | 11 | 12 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import platform 3 | import os 4 | 5 | 6 | def alias_already_exists(file_name): 7 | with open(file_name, 'r') as f: 8 | for line in f.readlines(): 9 | if "export ESLINT_NO_DEV_ERRORS=true" in line: 10 | return True 11 | return False 12 | 13 | 14 | def write_to_file(file_name): 15 | if not alias_already_exists(file_name): 16 | with open(file_name, 'a') as file: 17 | file.write('\n') 18 | file.write("export ESLINT_NO_DEV_ERRORS=true") 19 | file.write('\n') 20 | print( 21 | f"\n{file_name} written! " + 22 | "Open a new terminal after completing setup for changes to be in effect.") 23 | 24 | 25 | def add_alias_unix(): 26 | HOME_PATH = os.environ["HOME"] 27 | BASHRC_PATH = f'{HOME_PATH}/.bashrc' 28 | ZSHRC_PATH = f"{HOME_PATH}/.zshrc" 29 | BASH_PROFILE_PATH = f"{HOME_PATH}/.bash_profile" 30 | user_os = platform.system() 31 | if os.path.isfile(BASHRC_PATH): 32 | write_to_file(BASHRC_PATH) 33 | if user_os == "Darwin" and os.path.isfile(ZSHRC_PATH): 34 | write_to_file(ZSHRC_PATH) 35 | elif user_os == "Linux" and os.path.isfile(BASH_PROFILE_PATH): 36 | write_to_file(BASH_PROFILE_PATH) 37 | 38 | 39 | def add_alias_windows(): 40 | subprocess.check_call("setx ESLINT_NO_DEV_ERRORS true", 41 | stderr=subprocess.STDOUT, shell=True) 42 | 43 | 44 | # This function is neccesary such that users from different operating systems can 45 | # both make use of hot reload 46 | # CHOKIDAR_USEPOLLING and WATCHPACK_POLLING are alternate methods of watching for changes on files 47 | # windows users need this because the docker containers are all running linux and since their 48 | # files are also being managed by windows it gets messy using the os default file watching protocal 49 | # the .env file will be user specific and not interfere with non-windows users 50 | def make_env(isWindows): 51 | with open(".env", "x") as f: 52 | f.write(f"CHOKIDAR_USEPOLLING={isWindows}\nWATCHPACK_POLLING={isWindows}") 53 | 54 | 55 | print('Welcome to SCE-Development Setup!\n') 56 | user_os = platform.system() 57 | print('Detected OS: {}'.format(user_os)) 58 | 59 | if user_os == 'Darwin' or user_os == 'Linux': 60 | if os.path.exists("api/config/config.json") == False: 61 | os.system("cp api/config/config.example.json api/config/config.json") 62 | if os.path.exists("src/config/config.json") == False: 63 | os.system("cp src/config/config.example.json src/config/config.json") 64 | if os.path.exists(".env") == False: 65 | make_env(False) 66 | add_alias_unix() 67 | elif user_os == 'Windows': 68 | if os.path.exists("api\\config\\config.json") == False: 69 | os.system("copy api\\config\\config.example.json api\\config\\config.json") 70 | if os.path.exists("src\\config\\config.json") == False: 71 | os.system("copy src\\config\\config.example.json src\\config\\config.json") 72 | if os.path.exists(".env") == False: 73 | make_env(True) 74 | add_alias_windows() 75 | 76 | print('\nSetup complete! Bye!\n') 77 | -------------------------------------------------------------------------------- /src/APIFunctions/2DPrinting.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { 3 | PrintApiResponse, 4 | ApiResponse 5 | } from './ApiResponses'; 6 | 7 | import { BASE_API_URL } from '../Enums'; 8 | 9 | /** 10 | * Return an array similar to python's range() function 11 | * @param {Number} start 12 | * @param {Number} end 13 | */ 14 | export const range = (start, end) => { 15 | const length = end - start; 16 | return Array.from({ length }, (_, i) => start + i); 17 | }; 18 | 19 | /** 20 | * Checks to see if the printer is available to accept requests! 21 | */ 22 | export async function healthCheck() { 23 | let status = new ApiResponse(); 24 | const url = new URL('/api/Printer/healthCheck', BASE_API_URL); 25 | await axios.get(url.href) 26 | .then(res => { 27 | status.responseData = res.data; 28 | }) 29 | .catch(err => { 30 | status.responseData = err; 31 | status.error = true; 32 | }); 33 | return status; 34 | } 35 | 36 | /** 37 | * Returns an array of numbers from pages 38 | * @param {string} pages String containing array of pages 39 | * @param {Number} maxPages Number of pages in the document 40 | */ 41 | export function parseRange(pages, maxPages) { 42 | let result = new Set(); 43 | let pagesFromCommaSplit = pages.split(','); 44 | pagesFromCommaSplit.forEach(element => { 45 | const pagesFromDashSplit = element.split('-'); 46 | const arr = range( 47 | Number(pagesFromDashSplit[0]), 48 | Number(pagesFromDashSplit[pagesFromDashSplit.length - 1]) + 1 49 | ); 50 | arr.forEach(element => { 51 | result.add(element); 52 | }); 53 | }); 54 | result.delete(0); 55 | result.forEach(element => { 56 | if (element > maxPages) result.delete(element); 57 | }); 58 | if (result.size === 0) { 59 | let arr = new Set(range(1, maxPages + 1)); 60 | return arr; 61 | } 62 | return result; 63 | } 64 | 65 | /** 66 | * Print the page 67 | * @param {Object} data - PDF File and its configurations 68 | * @param {String|undefined} data.file - PDF file 69 | * @param {Number|undefined} data.copies - Number of copies 70 | * @param {String|undefined} data.sides - Sides to print: 71 | * one-sided or two-sided 72 | * @param {string} token token of the current user 73 | * @param {String|undefined} data.pageRanges - Pages to print: 74 | * 1-2, 5, 7-10 75 | * @returns {ApiResponse} - Containing information for if 76 | * the page successfully printed 77 | */ 78 | export async function printPage(data, token) { 79 | let status = new ApiResponse(); 80 | const url = new URL('/api/Printer/sendPrintRequest', BASE_API_URL); 81 | await axios.post(url.href, data, { 82 | headers: { 83 | 'Content-Type': 'multipart/form-data', 84 | 'Authorization': `Bearer ${token}` 85 | } 86 | }) 87 | .then(response => { 88 | status.responseData = response.data.message; 89 | }) 90 | .catch(() => { 91 | status.error = true; 92 | }); 93 | return status; 94 | } 95 | 96 | /** 97 | * Return the number of pages the current user has printed 98 | * @param {string} email email of the current user 99 | * @param {string} token token of the current user 100 | * @param {Set(Number)} totalPages set of all pages to be printed 101 | * @param {Number} copies number of copies to be printed 102 | * @returns {PrintApiResponse} Returns if user can print, number of pages 103 | * user can print, and total pages left 104 | */ 105 | export async function getPagesPrinted(email, token) { 106 | let status = new PrintApiResponse(); 107 | const url = new URL('/api/user/getPagesPrintedCount', BASE_API_URL); 108 | await axios 109 | .post(url.href, { 110 | email 111 | }, { 112 | headers: { 113 | 'Authorization': `Bearer ${token}` 114 | } 115 | } 116 | ) 117 | .then(res => { 118 | status.pagesUsed = res.data; 119 | }) 120 | .catch(() => { 121 | status.error = true; 122 | }); 123 | return status; 124 | } 125 | -------------------------------------------------------------------------------- /src/APIFunctions/Advertisement.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { ApiResponse } from './ApiResponses'; 3 | import { BASE_API_URL } from '../Enums'; 4 | 5 | 6 | export async function getAd() { 7 | let status = new ApiResponse(); 8 | await axios.get(BASE_API_URL + '/api/Advertisement/') 9 | .then(res => { 10 | status.responseData = res.data; 11 | }).catch(err => { 12 | status.responseData = err; 13 | status.error = true; 14 | }); 15 | return status; 16 | } 17 | 18 | export async function getAds(token) { 19 | let status = new ApiResponse(); 20 | await axios.get(BASE_API_URL + '/api/Advertisement/getAllAdvertisements', 21 | { 22 | headers: { 23 | Authorization: `Bearer ${token}` 24 | } 25 | } 26 | ) 27 | .then(res => { 28 | status.responseData = res.data; 29 | }).catch(err => { 30 | status.responseData = err; 31 | status.error = true; 32 | }); 33 | return status; 34 | } 35 | 36 | export async function createAd(newAd, token) { 37 | let status = new ApiResponse(); 38 | await axios.post(BASE_API_URL + '/api/Advertisement/createAdvertisement', 39 | newAd, 40 | { 41 | headers: { 42 | Authorization: `Bearer ${token}` 43 | } 44 | }) 45 | .then(res => { 46 | status.responseData = res.data; 47 | }).catch(err => { 48 | status.responseData = err; 49 | status.error = true; 50 | }); 51 | return status; 52 | } 53 | 54 | export async function deleteAd(newAd, token) { 55 | let status = new ApiResponse(); 56 | await axios.post(BASE_API_URL + '/api/Advertisement/deleteAdvertisement', 57 | newAd, 58 | { 59 | headers: { 60 | Authorization: `Bearer ${token}` 61 | } 62 | }) 63 | .then(res => { 64 | status.responseData = res.data; 65 | }).catch(err => { 66 | status.responseData = err; 67 | status.error = true; 68 | }); 69 | return status; 70 | } 71 | -------------------------------------------------------------------------------- /src/APIFunctions/ApiResponses.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Class to hold the server responses 3 | * @member {bool} error - Lets us know if there was any error regarding 4 | * the API call. This variable should be false if there was no error. 5 | * @member {any} responseData - Contains anything we would like to return 6 | * from the API call (e.g. object array or error data) 7 | * @member {string|null} token - An authentication token 8 | */ 9 | export class ApiResponse { 10 | constructor(error = false, responseData = null) { 11 | this.error = error; 12 | this.responseData = responseData; 13 | } 14 | } 15 | 16 | /** 17 | * Class to hold the server responses 18 | * @extends {ApiResponse} 19 | * @member {string|null} token - An authentication token 20 | */ 21 | export class UserApiResponse extends ApiResponse { 22 | constructor(error = false, responseData = null) { 23 | super(error, responseData); 24 | this.token = null; 25 | } 26 | } 27 | 28 | /** 29 | * Class to hold the server responses 30 | * @extends {ApiResponse} 31 | * @member {bool} canPrint - If the user can print given the number of pages 32 | * they have left 33 | * @member {number} pagesUsed - The number of remaining pages a user has 34 | * to print 35 | */ 36 | export class PrintApiResponse extends ApiResponse { 37 | constructor(error = false, responseData = null) { 38 | super(error, responseData); 39 | this.pagesUsed = 0; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/APIFunctions/Cleezy.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { ApiResponse } from './ApiResponses'; 3 | 4 | import { BASE_API_URL } from '../Enums'; 5 | 6 | 7 | export async function getAllUrls({ 8 | token, page, search, sortColumn, sortOrder 9 | }) { 10 | let status = new ApiResponse(); 11 | const url = new URL('/api/Cleezy/list', BASE_API_URL); 12 | await axios 13 | .get( 14 | url.href, { 15 | params: { 16 | page, 17 | ...(search !== undefined && { search }), 18 | sortColumn, 19 | sortOrder 20 | }, headers: { 21 | 'Authorization': `Bearer ${token}` 22 | } 23 | }, 24 | ) 25 | .then(res => { 26 | status.responseData = res.data; 27 | }) 28 | .catch(err => { 29 | status.responseData = err; 30 | status.error = true; 31 | }); 32 | return status; 33 | } 34 | 35 | export async function createUrl(url, alias = null, token) { 36 | let status = new ApiResponse(); 37 | const urlToAdd = { url, alias }; 38 | try { 39 | const url = new URL('/api/Cleezy/createUrl', BASE_API_URL); 40 | const response = await axios 41 | .post( 42 | url.href, 43 | urlToAdd, 44 | { 45 | headers: { 46 | 'Authorization': `Bearer ${token}` 47 | } 48 | }); 49 | const data = response.data; 50 | status.responseData = data; 51 | } catch (err) { 52 | status.error = true; 53 | status.responseData = err; 54 | } 55 | return status; 56 | } 57 | 58 | export async function deleteUrl(aliasIn, token) { 59 | let status = new ApiResponse(); 60 | const alias = { 'alias': aliasIn }; 61 | const url = new URL('/api/Cleezy/deleteUrl', BASE_API_URL); 62 | await axios 63 | .post( 64 | url.href, 65 | alias, 66 | { 67 | headers: { 68 | 'Authorization': `Bearer ${token}` 69 | } 70 | }) 71 | .catch(err => { 72 | status.responseData = err; 73 | status.error = true; 74 | }); 75 | return status; 76 | } 77 | -------------------------------------------------------------------------------- /src/APIFunctions/LedSign.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { ApiResponse } from './ApiResponses'; 3 | import { BASE_API_URL } from '../Enums'; 4 | 5 | 6 | /** 7 | * Checks to see if the sign is accepting requests. This is done 8 | * before any requests to update the sign can be made. 9 | * @param {string} officerName The name of the officer requesting the sign 10 | * @returns {ApiResponse} ApiResponse Object containing the response data 11 | */ 12 | export async function healthCheck(officerName) { 13 | let status = new ApiResponse(); 14 | const url = new URL('/api/LedSign/healthCheck', BASE_API_URL); 15 | await axios 16 | .get(url.href, { officerName }) 17 | .then(res => { 18 | status.responseData = res.data; 19 | }) 20 | .catch(err => { 21 | status.responseData = err; 22 | status.error = true; 23 | }); 24 | return status; 25 | } 26 | 27 | /** 28 | * Update the text of the sign. 29 | * @param {Object} signData - An object containing all of the sign data (text, 30 | * colors, etc.). 31 | * @returns {ApiResponse} Containing any error information related to the 32 | * request 33 | */ 34 | export async function updateSignText(signData, token) { 35 | let status = new ApiResponse(); 36 | const url = new URL('/api/LedSign/updateSignText', BASE_API_URL); 37 | await axios 38 | .post( 39 | url.href, 40 | signData, 41 | { 42 | headers: { 43 | Authorization: `Bearer ${token}` 44 | } 45 | }, 46 | ) 47 | .then(res => { 48 | status.responseData = res.data; 49 | }) 50 | .catch(err => { 51 | status.responseData = err; 52 | status.error = true; 53 | }); 54 | return status; 55 | } 56 | -------------------------------------------------------------------------------- /src/APIFunctions/Mailer.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { ApiResponse } from './ApiResponses'; 3 | import { BASE_API_URL } from '../Enums'; 4 | 5 | /** 6 | * Invoke the gmail API to send an email to verify a user. 7 | * @param {string} email - The user's email 8 | * @param {string} firstName - The user's first name 9 | * @returns {ApiResponse} Containing any error information related to the 10 | * request 11 | */ 12 | export async function sendVerificationEmail(email, token) { 13 | let status = new ApiResponse(); 14 | const url = new URL('/cloudapi/Auth/sendVerificationEmail', BASE_API_URL); 15 | await axios 16 | .post( 17 | url.href, 18 | { 19 | email 20 | }, 21 | { 22 | headers: { 23 | Authorization: `Bearer ${token}` 24 | } 25 | }, 26 | ) 27 | .then((response) => { 28 | status.responseData = response; 29 | }) 30 | .catch((error) => { 31 | status.error = true; 32 | status.responseData = error; 33 | }); 34 | return status; 35 | } 36 | 37 | /** 38 | * Invoke the gmail API to send an email to password reset a user. 39 | * @param {string} email - The user's email 40 | * @returns {ApiResponse} Containing any error information related to the 41 | * request 42 | */ 43 | export async function sendPasswordReset(email, captchaToken) { 44 | let status = new ApiResponse(); 45 | const url = new URL('/api/Auth/sendPasswordReset', BASE_API_URL); 46 | await axios 47 | .post(url.href, { 48 | email, 49 | captchaToken, 50 | }) 51 | .then((response) => { 52 | status.responseData = response; 53 | }) 54 | .catch((error) => { 55 | status.error = error; 56 | }); 57 | return status; 58 | } 59 | -------------------------------------------------------------------------------- /src/APIFunctions/Messaging.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { ApiResponse } from './ApiResponses'; 3 | import { BASE_API_URL } from '../Enums'; 4 | 5 | export async function sendMessage(id, token, message) { 6 | let status = new ApiResponse(); 7 | const roomId = id || 'general'; 8 | 9 | const url = new URL('/api/messages/send', BASE_API_URL); 10 | 11 | await axios 12 | .post(url.href, 13 | { message, id: roomId }, 14 | { 15 | headers: { 16 | 'authorization' : 'Bearer ' + token 17 | } 18 | }) 19 | .then(res => { 20 | status.responseData = res.data; 21 | }) 22 | .catch(err => { 23 | status.error = true; 24 | status.responseData = err; 25 | }); 26 | return status; 27 | } 28 | 29 | export async function connectToRoom(room, token, onMessage, onError) { 30 | const url = new URL('/api/messages/listen', BASE_API_URL); 31 | url.searchParams.append('id', room); 32 | url.searchParams.append('token', token); 33 | const eventSource = new EventSource(url.href); 34 | 35 | eventSource.onmessage = (event) => { 36 | let parsedMessage = JSON.parse(event.data); 37 | 38 | onMessage(parsedMessage); 39 | }; 40 | 41 | eventSource.onerror = (event) => { 42 | onError(event); 43 | }; 44 | 45 | return eventSource; 46 | } 47 | 48 | -------------------------------------------------------------------------------- /src/APIFunctions/Profile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Formats the first and last name by making sure the 3 | * first letter of both are uppercase 4 | * @param {Object} user - The object contianing all 5 | * of the user data fetched from mangoDB 6 | * @param {String} user.firstName - The first name of the user 7 | * @param {String} user.lastName - The last name of the user 8 | * @returns {String} The string of the users first and last name formated 9 | */ 10 | export function formatFirstAndLastName(user) { 11 | return ( 12 | user.firstName[0].toUpperCase() + 13 | user.firstName.slice(1, user.firstName.length) + 14 | ' ' + 15 | user.lastName[0].toUpperCase() + 16 | user.lastName.slice(1, user.lastName.length) 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/APIFunctions/Speaker.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { ApiResponse } from './ApiResponses'; 3 | import { BASE_API_URL } from '../Enums'; 4 | 5 | 6 | export async function queued(token) { 7 | let status = new ApiResponse(); 8 | const url = new URL('/api/Speaker/queued', BASE_API_URL); 9 | await axios 10 | .get(url.href, { 11 | headers: { 12 | 'Authorization': `Bearer ${token}` 13 | } 14 | } 15 | ) 16 | .then(res => { 17 | status.responseData = res.data.queue; 18 | }) 19 | .catch(err => { 20 | status.responseData = err; 21 | status.error = true; 22 | }); 23 | return status; 24 | } 25 | 26 | export async function addUrl(urlToAdd, token) { 27 | let status = new ApiResponse(); 28 | const url = new URL('/api/Speaker/stream', BASE_API_URL); 29 | await axios 30 | .post(url.href, { 31 | url: urlToAdd 32 | }, { 33 | headers: { 34 | 'Authorization': `Bearer ${token}` 35 | } 36 | } 37 | ) 38 | .then(res => { 39 | status = res.data; 40 | }) 41 | .catch(err => { 42 | status.responseData = err; 43 | status.error = true; 44 | }); 45 | return status; 46 | } 47 | 48 | export async function skip(token) { 49 | let status = new ApiResponse(); 50 | const url = new URL('/api/Speaker/skip', BASE_API_URL); 51 | await axios 52 | .post(url.href, {}, { 53 | headers: { 54 | 'Authorization': `Bearer ${token}` 55 | } 56 | } 57 | ) 58 | .then(res => { 59 | status = res.data; 60 | }) 61 | .catch(err => { 62 | status.responseData = err; 63 | status.error = true; 64 | }); 65 | return status; 66 | } 67 | 68 | export async function pause(token) { 69 | let status = new ApiResponse(); 70 | const url = new URL('/api/Speaker/pause', BASE_API_URL); 71 | await axios 72 | .post(url.href, {}, { 73 | headers: { 74 | 'Authorization': `Bearer ${token}` 75 | } 76 | } 77 | ) 78 | .then(res => { 79 | status = res.data; 80 | }) 81 | .catch(err => { 82 | status.responseData = err; 83 | status.error = true; 84 | }); 85 | return status; 86 | } 87 | 88 | export async function resume(token) { 89 | let status = new ApiResponse(); 90 | const url = new URL('/api/Speaker/resume', BASE_API_URL); 91 | await axios 92 | .post(url.href, {}, { 93 | headers: { 94 | 'Authorization': `Bearer ${token}` 95 | } 96 | } 97 | ) 98 | .then(res => { 99 | status = res.data; 100 | }) 101 | .catch(err => { 102 | status.responseData = err; 103 | status.error = true; 104 | }); 105 | return status; 106 | } 107 | 108 | export async function setVolume(volumeToSet, token) { 109 | let status = new ApiResponse(); 110 | const url = new URL('/api/Speaker/volume', BASE_API_URL); 111 | await axios 112 | .post(url.href, { 113 | volume: volumeToSet 114 | }, { 115 | headers: { 116 | 'Authorization': `Bearer ${token}` 117 | } 118 | } 119 | ) 120 | .then(res => { 121 | status = res.data; 122 | }) 123 | .catch(err => { 124 | status.responseData = err; 125 | status.error = true; 126 | }); 127 | } 128 | 129 | export async function rewind(token) { 130 | let status = new ApiResponse(); 131 | const url = new URL('/api/Speaker/rewind', BASE_API_URL); 132 | await axios 133 | .post(url.href, {}, { 134 | headers: { 135 | 'Authorization': `Bearer ${token}` 136 | } 137 | } 138 | ) 139 | .then(res => { 140 | status = res.data; 141 | }) 142 | .catch(err => { 143 | status.responseData = err; 144 | status.error = true; 145 | }); 146 | return status; 147 | } 148 | 149 | export async function forward(token) { 150 | let status = new ApiResponse(); 151 | const url = new URL('/api/Speaker/forward', BASE_API_URL); 152 | await axios 153 | .post(url.href, {}, { 154 | headers: { 155 | 'Authorization': `Bearer ${token}` 156 | } 157 | } 158 | ) 159 | .then(res => { 160 | status = res.data; 161 | }) 162 | .catch(err => { 163 | status.responseData = err; 164 | status.error = true; 165 | }); 166 | return status; 167 | } 168 | -------------------------------------------------------------------------------- /src/Components/Background/background.css: -------------------------------------------------------------------------------- 1 | .background-parent { 2 | position: fixed; 3 | overflow: hidden; 4 | z-index: -1; 5 | /* ensures the background stays behind everything */ 6 | width: 100vw; 7 | height: 100vh; 8 | border: 0; 9 | top: 0; 10 | left: 0; 11 | background-color: #333; 12 | overflow: none; 13 | } 14 | 15 | .background-child { 16 | margin: auto; 17 | } 18 | 19 | .background-parent svg { 20 | position: absolute; 21 | top: 50%; 22 | left: 50%; 23 | transform: translate(-50%, -50%); 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | #circuit, 29 | #boxes, 30 | #circles { 31 | stroke: #ccc; 32 | } 33 | 34 | #electricity { 35 | stroke: #03d8f3; 36 | } 37 | 38 | .path { 39 | animation: draw 20s infinite; 40 | animation-timing-function: linear; 41 | } 42 | 43 | @keyframes draw { 44 | 0% {} 45 | 46 | 100% { 47 | stroke-dashoffset: 0; 48 | stroke-opacity: 1; 49 | } 50 | } -------------------------------------------------------------------------------- /src/Components/Captcha/GoogleRecaptcha.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import ReCAPTCHA from 'react-google-recaptcha'; 3 | 4 | import config from '../../config/config.json'; 5 | 6 | const GoogleRecaptcha = ({ setCaptchaValue, setCaptchaRef }) => { 7 | const recaptchaRef = useRef(null); 8 | 9 | useEffect(() => { 10 | if (typeof setCaptchaRef === 'function') { 11 | setCaptchaRef(recaptchaRef.current); 12 | } 13 | }, [recaptchaRef]); 14 | 15 | return ( 16 | 28 | ); 29 | }; 30 | 31 | export default GoogleRecaptcha; 32 | -------------------------------------------------------------------------------- /src/Components/DecisionModal/ConfirmationModal.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | 3 | export default function ConfirmationModal(props) { 4 | const { headerText, bodyText, handleConfirmation, open, handleCancel = () => {}, confirmClassAddons = '' } = props; 5 | 6 | const confirmText = props.confirmText || 'Confirm'; 7 | const cancelText = props.cancelText || 'Cancel'; 8 | 9 | useEffect(() => { 10 | if (open) { 11 | document.getElementById('confirmation-modal').showModal(); 12 | } 13 | }, [open]); 14 | return (<> 15 | 16 |
17 |

{headerText}

18 |

19 | {bodyText} 20 |

21 |
22 | 23 |
24 |
25 | 28 | 31 |
32 |
33 |
34 |
35 |
36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/Components/Navbar/NavBarWrapper.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import UserNavbar from './UserNavbar'; 3 | import AdminNavbar from './AdminNavbar'; 4 | 5 | function NavBarWrapper({ 6 | enableAdminNavbar = false, 7 | component: Component, 8 | ...appProps 9 | }) { 10 | function handleLogout() { 11 | appProps.setAuthenticated(false); 12 | window.localStorage.removeItem('jwtToken'); 13 | window.location.reload(); 14 | } 15 | 16 | // basically if we are on an admin page, make the navbar 17 | // appear on the side with instead of on top. Wrapping 18 | // the component with the below div allows the navbar 19 | // to appear on the side in a clean way. See below for more info 20 | // https://flowbite.com/docs/components/sidebar/#default-sidebar 21 | function maybeWrapComponentForAdminNavbar() { 22 | if (enableAdminNavbar) { 23 | return ( 24 |
25 | 26 |
27 | ); 28 | } 29 | return ; 30 | } 31 | 32 | if (enableAdminNavbar) { 33 | return ( 34 | 35 | 36 | 37 | ); 38 | } 39 | 40 | return ( 41 | <> 42 | {enableAdminNavbar ? ( 43 | 44 | ) : ( 45 | 46 | )} 47 | {maybeWrapComponentForAdminNavbar()} 48 | 49 | ); 50 | } 51 | 52 | export default NavBarWrapper; 53 | -------------------------------------------------------------------------------- /src/Components/Routing/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | 4 | export default function PrivateRoute({ 5 | component: Component, 6 | appProps, 7 | ...params 8 | }) { 9 | return ( 10 | { 13 | if (appProps.allowed) { 14 | return ; 15 | } else if (appProps.authenticated) { 16 | return ( 17 | 22 | ); 23 | } else { 24 | return ( 25 | ( 27 | 0) 47 | return membershipStatusArray[accessLevel + 3]; 48 | return membershipStatusArray[accessLevel + 2]; 49 | } 50 | 51 | const BASE_API_URL = process.env.REACT_APP_BASE_API_URL || 'http://localhost:8080/'; 52 | 53 | module.exports = { 54 | memberApplicationState, 55 | membershipPlans, 56 | memberShipPlanToString, 57 | membershipState, 58 | membershipStateToString, 59 | BASE_API_URL, 60 | }; 61 | -------------------------------------------------------------------------------- /src/Pages/2DPrinting/PageSelectDropdown.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | export default function PageSelectDropdown(props) { 4 | const [inputEnabled, setInputEnabled] = useState(props.inputEnable); 5 | 6 | function handleMajorChange(e) { 7 | setInputEnabled(e.target.value === 'Custom'); 8 | } 9 | 10 | const options = ['All', 'Custom']; 11 | 12 | return ( 13 |
14 | 20 |
21 | 35 | {inputEnabled ? ( 36 | props.setPageRanges(e.target.value)} 43 | /> 44 | ) : null} 45 |

Note: Prints are black ink only

46 |
47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/Pages/About/About.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function AboutPage() { 4 | return ( 5 |
6 |

What Happens at SCE

7 |
8 | 9 |
10 |

Introduction

11 |

12 | The Software and Computer Engineering Society is a student-run club at San Jose State that aims to provide 13 | students with a sense of community, industry-relevant experience, and mentorship. 14 |

15 |
16 | 17 |
18 |

Location

19 |

20 | We are located in the second floor of the Engineering Building, room 294. When the school semester is in session, our hours are: 21 |

22 | 23 |
24 |           Monday - Thursday: 10:00 AM - 5:00 PM
25 |           

26 | Friday: 10:00 AM - 2:00 PM 27 |
28 | 29 |

30 | During open hours, the room is open to the public. Outside of hours, SCE members and officers may access the room with a numerical door code. 31 |

32 |
33 | 34 |
35 |

Becoming an SCE Member

36 |

Membership is limited to San Jose State students. Membership duration is based on semesters and require a fee:

37 |
38 |           Single semester: $20
39 |           

40 | Two semesters: $30 41 |
42 |

43 | The membership fee must be paid in the SCE room. 44 |

45 |

46 | Benefits include after-hours room access, free paper printing, free 3D printing, company tours, and a free T-shirt. 47 |

48 |
49 | 50 |
51 |

Development Team

52 |

53 | The development team works on projects including full stack, distributed systems, site reliability, networking, and more. 54 | Members often land top internships and job offers through their experience and receive mentorship from SCE alumni. 55 |

56 |
57 | 58 |
59 |

Summer Internship

60 |

61 | Every summer, SCE runs a remote internship program where students design, implement, and maintain large-scale projects. 62 |

63 |

64 | The internship begins in early June and runs for 10 weeks. We host in-person game nights, and at the end, interns present 65 | their projects to alumni and faculty. 66 |

67 |

Details and important dates are announced on our Discord as June approaches.

68 |
69 | 70 |
71 |

Discord

72 |

73 | Join us using the link below. We encourage new users to ask questions in our discussion channels! 74 |

75 |

76 | 77 | https://sce.sjsu.edu/s/discord 78 | 79 |
80 | 81 | sce collage 84 |
85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /src/Pages/ForgotPassword/ForgotPassword.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { sendPasswordReset } from '../../APIFunctions/Mailer'; 3 | import Background from '../../Components/Background/background'; 4 | import GoogleRecaptcha from '../../Components/Captcha/GoogleRecaptcha'; 5 | 6 | const ForgotPassword = () => { 7 | const [email, setEmail] = useState(''); 8 | const [message, setMessage] = useState(''); 9 | const [captchaValue, setCaptchaValue] = useState(null); 10 | const [captchaRef, setCaptchaRef] = useState(null); 11 | 12 | async function handleSubmit(e) { 13 | e.preventDefault(); 14 | if (process.env.NODE_ENV === 'production' && !captchaValue) { 15 | setMessage('Please complete the reCAPTCHA.'); 16 | return; 17 | } 18 | if (!(email.includes('@') && email.includes('.'))) { 19 | setMessage('Please enter a valid email address.'); 20 | return; 21 | } 22 | 23 | captchaRef.reset(); 24 | const resetStatus = await sendPasswordReset(email, captchaValue); 25 | if (resetStatus.error) { 26 | setMessage(resetStatus.error?.response?.data?.message || 'An error occurred. Please try again later.'); 27 | } else { 28 | setMessage('A password reset email has been sent to you if your email exists in our system.'); 29 | } 30 | } 31 | 32 | return ( 33 |
34 |
35 |
36 | sce logo 37 |
38 |
39 | 45 |
46 | 47 |
48 | {message &&

{message}

} 52 | 55 |
56 |
57 | 58 |
59 | ); 60 | }; 61 | 62 | export default ForgotPassword; 63 | -------------------------------------------------------------------------------- /src/Pages/Home/Home.css: -------------------------------------------------------------------------------- 1 | /* Hide scrollbar for WebKit (Chrome, Safari) */ 2 | *::-webkit-scrollbar { 3 | display: none; 4 | } 5 | 6 | /* Hide scrollbar for IE, Edge and Firefox */ 7 | * { 8 | -ms-overflow-style: none; /* IE and Edge */ 9 | scrollbar-width: none; /* Firefox */ 10 | } 11 | 12 | /* Fade-in effect with scale for p */ 13 | .fade-scale-in { 14 | opacity: 0; 15 | transform: scale(0.5); 16 | transition: opacity 0.5s 1s, transform 0.5s 1s; /* dur 0.5s, delay 1s */ 17 | } 18 | 19 | .fade-scale-in.show { 20 | opacity: 1; 21 | transform: scale(1); 22 | } 23 | 24 | /* Fade-in effect with scale for h1 */ 25 | .slide-in-top { 26 | opacity: 0; 27 | transform: translateY(-75px); 28 | transition: opacity 1.5s, transform 1.5s; 29 | } 30 | 31 | .slide-in-top.show { 32 | opacity: 1; 33 | transform: translateY(0); 34 | } 35 | 36 | /* Fade-in effect with scale for h3 */ 37 | .slide-in-right { 38 | opacity: 0; 39 | transform: translateX(100px); 40 | transition: opacity 1s 0.5s, transform 1s 0.5s; 41 | } 42 | .slide-in-right.show { 43 | opacity: 1; 44 | transform: translateX(0); 45 | } 46 | 47 | /* Fade-in effect with scale for div */ 48 | .slide-in-bottom { 49 | opacity: 0; 50 | transform: translateY(100px); 51 | transition: opacity 0.5s 1.5s, transform 0.5s 1.5s; 52 | } 53 | .slide-in-bottom.show { 54 | opacity: 1; 55 | transform: translateY(0); 56 | } 57 | 58 | /* Fade-in effect for images */ 59 | .fade-in-img { 60 | opacity: 0; 61 | transition: opacity 1.5s 0.5s; /* duration 1.5s, delay 0.5s */ 62 | } 63 | .fade-in-img.show { 64 | opacity: 1; 65 | } -------------------------------------------------------------------------------- /src/Pages/Home/Home.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState} from 'react'; 2 | import Footer from '../../Components/Footer/Footer.js'; 3 | import './Home.css'; 4 | 5 | import { getAd } from '../../APIFunctions/Advertisement.js'; 6 | 7 | const Home = () => { 8 | 9 | const [message, setMessage] = useState(''); 10 | const [showMessage, setShowMessage] = useState(false); 11 | const [showAll, setShowAll] = useState(false); 12 | async function getMessage() { 13 | try { 14 | const messageData = await getAd(); 15 | const adsList = messageData.responseData; 16 | setMessage(adsList['message']); 17 | setShowMessage(true); 18 | } catch { 19 | setMessage(''); 20 | } 21 | } 22 | 23 | useEffect(() => { 24 | getMessage(); 25 | setTimeout(() => setShowAll(true), 100); 26 | }, []); 27 | 28 | function isValidUrl(str) { 29 | try { 30 | new URL(str); 31 | return true; 32 | } catch (_) { 33 | return false; 34 | } 35 | } 36 | 37 | function renderMessageWithLinks(message) { 38 | if (!message) { 39 | return null; 40 | } 41 | return message.split(/(https?:\/\/[^\s]+)/g).map((part, index) => { 42 | if (isValidUrl(part)) { 43 | return ( 44 | 51 | {part} 52 | 53 | ); 54 | } 55 | 56 | return {part}; 57 | }); 58 | } 59 | 60 | return ( 61 |
62 |
63 |

64 | {message} 65 |

66 |
67 |
68 |
69 |
70 |

72 | The Software and Computer Engineering Society 73 |

74 |

76 | SJSU's Largest Engineering Club 77 |

78 |
79 | 88 |
89 | 90 | 91 |
92 | 96 |
97 | 98 |
99 | 100 |
101 |
102 | ); 103 | }; 104 | 105 | export default Home; 106 | -------------------------------------------------------------------------------- /src/Pages/LedSign/ledsign.css: -------------------------------------------------------------------------------- 1 | .led-sign-preview-text { 2 | margin: 0; 3 | vertical-align: middle; 4 | height: 100%; 5 | } 6 | 7 | .led-sign-preview-text>h1 { 8 | margin: 0; 9 | vertical-align: middle; 10 | height: 100%; 11 | } 12 | 13 | .led-sign-preview-border-top { 14 | background-color: #c7c7c7; 15 | min-height: 5px; 16 | border-top-left-radius: 20px; 17 | border-top-right-radius: 20px; 18 | } 19 | 20 | .led-sign-preview-border-bottom { 21 | background-color: #c7c7c7; 22 | min-height: 5px; 23 | border-bottom-left-radius: 20px; 24 | border-bottom-right-radius: 20px; 25 | } 26 | 27 | .led-sign-preview-background { 28 | background-color: #dee0df; 29 | text-align: center; 30 | vertical-align: middle; 31 | /* white-space: nowrap; */ 32 | overflow-x: auto; 33 | min-height: 50px; 34 | } 35 | 36 | .led-sign-marquee-container { 37 | width: 100%; 38 | overflow: hidden; 39 | } 40 | 41 | .led-sign-marquee { 42 | display: inline-block; 43 | text-wrap: nowrap; 44 | white-space-collapse: preserve; 45 | animation: led-sign-marquee-animation linear infinite; 46 | } 47 | 48 | @keyframes led-sign-marquee-animation { 49 | 0% { 50 | transform: translateX(100%); 51 | } 52 | 53 | 100% { 54 | transform: translateX(-100%); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Pages/Login/Login.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { loginUser } from '../../APIFunctions/Auth'; 3 | import Background from '../../Components/Background/background'; 4 | 5 | export default function Login(props) { 6 | const queryParams = new URLSearchParams(window.location.search); 7 | const [email, setEmail] = useState(''); 8 | const [password, setPassword] = useState(''); 9 | const [errorMsg, setErrorMsg] = useState(''); 10 | const fields = [ 11 | { 12 | type: 'email', 13 | placeholder: 'Email', 14 | handleChange: (e) => setEmail(e.target.value), 15 | }, 16 | { 17 | type: 'password', 18 | placeholder: 'Password', 19 | handleChange: (e) => setPassword(e.target.value), 20 | }, 21 | ]; 22 | 23 | async function handleSubmit(e) { 24 | e.preventDefault(); 25 | const loginStatus = await loginUser(email, password); 26 | if (!loginStatus.error) { 27 | props.setAuthenticated(true); 28 | window.localStorage.setItem('jwtToken', loginStatus.token); 29 | if (queryParams.get('redirect')) { 30 | window.location.href = queryParams.get('redirect'); 31 | } else { 32 | window.location.reload(); 33 | } 34 | } else { 35 | if(loginStatus.responseData === undefined || loginStatus.responseData.data.message === undefined){ 36 | setErrorMsg('Backend May Be down, check with dev team!'); 37 | }else{ 38 | setErrorMsg( 39 | loginStatus.responseData && loginStatus.responseData.data.message 40 | ); 41 | } 42 | } 43 | } 44 | 45 | return ( 46 |
47 |
48 |
49 | sce logo 50 |
51 |
52 |
53 | 59 | 65 | Forgot Password? 66 | {errorMsg &&

{errorMsg}

} 67 | 70 | 71 | 74 | 75 |
76 |
77 |
78 | 79 |
80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/Pages/Login/LoginInput.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | export default function InputBox(props) { 4 | const [style, setStyle] = useState({}); 5 | const animatedCSS = { 6 | top: '-5px', 7 | borderBottom: '2px solid GREEN' 8 | }; 9 | 10 | return ( 11 |
{ 14 | setStyle(animatedCSS); 15 | }} 16 | onBlur={() => { 17 | setStyle({}); 18 | }} 19 | className='txtb' 20 | > 21 | 22 | 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/Pages/MembershipApplication/ConfirmationPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function ConfrimationPage() { 4 | return ( 5 |
6 |

Your application has been submitted!

7 |

8 | To activate your membership, verify your email address by clicking the 9 | link in the email we sent, and proceed to SCE (Engr 294) to complete 10 | registration and memberhip payment. You may now return to the homepage! 11 |

12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/Pages/MembershipApplication/GetPlans.js: -------------------------------------------------------------------------------- 1 | export function getSemesterPlan() { 2 | const year = new Date().getFullYear(); 3 | const month = new Date().getMonth(); 4 | let plan = ''; 5 | if (month <= 4) { 6 | plan = 'Spring ' + year; 7 | } else { 8 | plan = 'Fall ' + year; 9 | } 10 | return plan; 11 | } 12 | 13 | export function getYearPlan() { 14 | const year = new Date().getFullYear(); 15 | const month = new Date().getMonth(); 16 | let plan = ''; 17 | if (month <= 4) { 18 | plan = 'Spring and Fall ' + year; 19 | } else { 20 | plan = 'Fall ' + year + ' and Spring ' + (year + 1); 21 | } 22 | return plan; 23 | } 24 | -------------------------------------------------------------------------------- /src/Pages/MembershipApplication/MajorDropdown.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | export default function MajorDropdown(props) { 4 | const [inputEnabled, setInputEnabled] = useState(props.inputEnable); 5 | const [major, setMajor] = useState(); 6 | const [dropdownOpen, setDropdownOpen] = useState(); 7 | 8 | 9 | function handleMajorChange(e) { 10 | props.setMajor(e.target.value); 11 | setMajor(e.target.value); 12 | setInputEnabled(e.target.value === 'Other'); 13 | } 14 | 15 | const options = ['Computer Engineering', 'Software Engineering', 'Computer Science', 'Other']; 16 | 17 | return ( 18 |
19 | 25 |
26 | 41 | {inputEnabled ? ( 42 | props.setMajor(e.target.value)} 49 | /> 50 | ) : null} 51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/Pages/MembershipApplication/MembershipApplication.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { memberApplicationState } from '../../Enums'; 4 | import MembershipForm from './MembershipForm'; 5 | import ConfirmationPage from './ConfirmationPage'; 6 | import { CSSTransition } from 'react-transition-group'; 7 | import Background from '../../Components/Background/background'; 8 | 9 | export default function MembershipApplication() { 10 | const [membershipState, setMembershipState] = 11 | useState(memberApplicationState.FORM_INFO); 12 | const [selectedPlan, setSelectedPlan] = useState(); 13 | const appProps = { 14 | setMembershipState, 15 | selectedPlan, 16 | setSelectedPlan 17 | }; 18 | const membershipArray = [ 19 | { 20 | isShown: membershipState === memberApplicationState.FORM_INFO, 21 | Component: 22 | }, 23 | { 24 | isShown: membershipState === memberApplicationState.CONFIRMATION, 25 | Component: 26 | } 27 | ]; 28 | 29 | return ( 30 |
31 | 32 | {membershipArray.map((registerView, index) => { 33 | return ( 34 |
35 | 40 |
41 | {registerView.isShown && registerView.Component} 42 |
43 |
44 |
45 | ); 46 | })} 47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/Pages/MembershipApplication/VerifyEmail.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { sendVerificationEmail } from '../../APIFunctions/Mailer'; 3 | import { validateVerificationEmail } from '../../APIFunctions/Auth'; 4 | 5 | export default class VerifyEmail extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | emailVerified: false, 10 | componentLoaded: false, 11 | email: '', 12 | hashedId: '' 13 | }; 14 | } 15 | 16 | componentDidMount() { 17 | const urlParams = new URLSearchParams(this.props.location.search); 18 | const email = urlParams.get('user'); 19 | const hashedId = urlParams.get('id'); 20 | 21 | this.setState( 22 | { 23 | email: email, 24 | hashedId: hashedId 25 | }, 26 | () => { 27 | this.validateVerificationEmail(); 28 | } 29 | ); 30 | } 31 | 32 | validateVerificationEmail() { 33 | validateVerificationEmail(this.state.email, this.state.hashedId) 34 | .then(emailValidated => { 35 | if (!emailValidated.error) { 36 | this.setState({ 37 | emailVerified: true, 38 | componentLoaded: true 39 | }); 40 | } else { 41 | this.setState({ 42 | componentLoaded: true 43 | }); 44 | } 45 | }) 46 | .catch(error => { 47 | console.debug(error); 48 | }); 49 | } 50 | 51 | render() { 52 | return ( 53 |
54 | {!this.state.componentLoaded ? ( 55 |
56 |

Verifying

57 |
58 | ) : this.state.emailVerified ? ( 59 |
60 |

Your email has been verified

61 |

62 | You're good to go, fellow SCE member! 63 |

64 |
65 | ) : ( 66 |
67 |

68 | There was a problem verifying your email. 69 |

70 |

71 | Reach out to us in our discord to quickly resolve the issue. 72 |

73 |
74 | )} 75 |
76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Pages/NotFoundPage/NotFoundPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | 4 | class NotFoundPage extends Component { 5 | render() { 6 | return ( 7 |
8 |
9 |

There's nobody here.

10 |

11 | return to safety 12 |

13 |
14 |
15 | ); 16 | } 17 | } 18 | 19 | export default NotFoundPage; 20 | -------------------------------------------------------------------------------- /src/Pages/NotFoundPage/NotFoundPics/empty_room.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SCE-Development/Clark/46801ff13fd8842b17e29fdd99094097800292da/src/Pages/NotFoundPage/NotFoundPics/empty_room.png -------------------------------------------------------------------------------- /src/Pages/Overview/PageNumbers.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function PageNumbers(props) { 4 | const numberOfButtons = Math.ceil(props.count / props.usersPerPage); 5 | // this generates array of page numbers 6 | // for example, given numberOfButtons = 3: 7 | // this generates [0,1,2,3] 8 | const pageNumbers = [...Array(numberOfButtons + 1).keys()].slice(1); 9 | const currentPage = props.pageNumber; 10 | return ( 11 | 26 | ); 27 | } 28 | 29 | export default PageNumbers; 30 | -------------------------------------------------------------------------------- /src/Pages/Overview/SVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function checkMark() { 4 | return ( 5 | 6 | 10 | 11 | ); 12 | } 13 | 14 | export function xMark() { 15 | return ( 16 | 17 | 22 | 23 | ); 24 | } 25 | 26 | export function editSymbol() { 27 | return ( 28 | 29 | 38 | 39 | ); 40 | } 41 | 42 | export function trashcanSymbol() { 43 | return ( 44 | 45 | 49 | 50 | ); 51 | } 52 | 53 | export function mapPinSymbol() { 54 | return ( 55 | 63 | 71 | 72 | ); 73 | } 74 | 75 | export function clockSymbol() { 76 | return ( 77 | 85 | 93 | 94 | ); 95 | } 96 | 97 | export function cancelEditSymbol(setToggle) { 98 | return ( 99 | { 104 | setToggle(); 105 | }} 106 | style={{ 107 | position: 'relative', 108 | marginTop: '5px', 109 | marginLeft: '-5px', 110 | left: '95%' 111 | }} 112 | > 113 | 118 | 119 | ); 120 | } 121 | 122 | export function sunIcon() { 123 | return ( 124 | // paste the and body from the website 125 | 129 | 132 | 134 | 135 | ); 136 | } 137 | 138 | export function moonIcon() { 139 | return ( 140 | 149 | ); 150 | } 151 | -------------------------------------------------------------------------------- /src/Pages/Profile/MemberView/ChangePassword.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { editUser } from '../../../APIFunctions/User'; 3 | 4 | export default function ChangePasswordModal(props) { 5 | const { bannerCallback = (message, color) => { }, confirmClassAddons } = props; 6 | const [password, setPassword] = useState(''); 7 | const [confirmPassword, setConfirmPassword] = useState(''); 8 | const [showPassword, setShowPassword] = useState(false); 9 | 10 | const INPUT_CLASS_NAME = 'indent-2 block w-full rounded-md border-0 py-1.5 shadow-sm ring-1 ring-inset ring-gray-300 placeholder: focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6 text-white'; 11 | 12 | async function changePassword() { 13 | const apiResponse = await editUser( 14 | { 15 | ...props.user, 16 | password 17 | }, 18 | props.user.token 19 | ); 20 | 21 | if (!apiResponse.error) { 22 | bannerCallback('Password Updated', 'success'); 23 | } else { 24 | bannerCallback('Unable to update password. Please try again or reach out to dev team if error persists.', 'error', 7000); 25 | } 26 | setPassword(''); 27 | setConfirmPassword(''); 28 | } 29 | 30 | return (<> 31 | 32 |
33 |

Reset Password

34 | 37 |
38 | { 44 | setPassword(e.target.value); 45 | }} 46 | className={INPUT_CLASS_NAME} 47 | /> 48 |
49 | 52 |
53 | { 59 | setConfirmPassword(e.target.value); 60 | }} 61 | className={INPUT_CLASS_NAME} 62 | /> 63 |
64 | 65 |
66 | 75 |
76 | 77 |
78 | 79 |
80 |
81 | 90 | 99 |
100 |
101 |
102 |
103 | 104 | 105 | ); 106 | } 107 | -------------------------------------------------------------------------------- /src/Pages/Profile/MemberView/DeleteAccountModal.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { deleteUserByID } from '../../../APIFunctions/User'; 3 | 4 | export default function DeleteAccountModal(props) { 5 | const { bannerCallback = () => {} } = props; 6 | 7 | async function deleteAccount() { 8 | const apiResponse = await deleteUserByID( 9 | props.user._id, 10 | props.user.token 11 | ); 12 | 13 | if (!apiResponse.error) { 14 | bannerCallback('Account Deleted', 'success'); 15 | setTimeout(() => { 16 | window.localStorage.removeItem('jwtToken'); 17 | window.location.reload(); 18 | }, 2000); 19 | } else { 20 | bannerCallback( 21 | 'Unable to delete account. Please try again or reach out to the dev team if this error persists.', 22 | 'error', 23 | 7000 24 | ); 25 | } 26 | } 27 | 28 | return ( 29 | <> 30 | 34 |
35 |

Delete Account

36 |

37 | Are you sure you want to delete your account? All of your data will 38 | be removed from our database. 39 |

40 |
41 |
42 |
43 | 51 | 54 |
55 |
56 |
57 |
58 |
59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/Pages/Profile/MemberView/GetApiKeyModal.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react'; 2 | import { getApiKey } from '../../../APIFunctions/User'; 3 | 4 | const GetApiKeyModal = (props) => { 5 | const { bannerCallback = () => {} } = props; 6 | const [apiKey, setApiKey] = useState(''); 7 | 8 | async function generateKey() { 9 | const apiResponse = await getApiKey(props.user.token); 10 | 11 | if (!apiResponse.error) { 12 | setApiKey(apiResponse.responseData.apiKey); 13 | } else { 14 | bannerCallback(apiResponse.errror); 15 | } 16 | } 17 | 18 | useEffect(() => { 19 | if (props.user.apiKey) { 20 | setApiKey(props.user.apiKey); 21 | } 22 | }, [props]); 23 | 24 | return ( 25 | 26 |
27 |

Get API Key

28 | 31 |
32 | 39 |
40 | 41 | 42 |
43 |
44 | 49 |
50 | 51 | 54 |
55 |
56 |
57 |
58 | 59 |
60 | ); 61 | }; 62 | 63 | export default GetApiKeyModal; 64 | -------------------------------------------------------------------------------- /src/Pages/Profile/MemberView/InfoCard.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | 4 | 5 | export default function ProfileCard(props) { 6 | const [toggle, setToggle] = useState(false); 7 | 8 | function itemClicked() { 9 | if (props.field.function) { 10 | props.field.function(); 11 | } 12 | setToggle(!toggle); 13 | } 14 | return ( 15 |
itemClicked()}> 16 |
17 | {props.field.icon} 18 | 19 |

20 | 22 | {props.field.title}{' '} 23 | 24 | 25 | {toggle === true ? 26 | {props.field.value} : ''} 27 |

28 |
29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/Pages/Profile/MemberView/getPicBySeason.js: -------------------------------------------------------------------------------- 1 | import spring from './Image/spring.jpg'; 2 | import fall from './Image/fall.jpeg'; 3 | import winter from './Image/winter.jpeg'; 4 | import summer from './Image/summer.jpeg'; 5 | 6 | export function getPictureByMonth() { 7 | let pic = null; 8 | const month = parseInt(new Date().getMonth()) + 1; 9 | if (month === 3 || month === 4 || month === 5) { 10 | pic = spring; 11 | } else if (month === 6 || month === 7 || month === 8) { 12 | pic = summer; 13 | } else if (month === 9 || month === 10 || month === 11) { 14 | pic = fall; 15 | } else if (month === 12 || month === 1 || month === 2) { 16 | pic = winter; 17 | } 18 | return pic; 19 | } 20 | -------------------------------------------------------------------------------- /src/Pages/Profile/admin/SendUnsubscribeEmail.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { getAllUserSubscribedAndVerified } from '../../../APIFunctions/User'; 4 | 5 | function AdminDashboard(props) { 6 | const [buttonText, setButtonText] = useState('Send Unsubscribe Email to All'); 7 | const [buttonColor, setButtonColor] = useState(''); 8 | 9 | const handleButtonClick = async () => { 10 | let status = await getAllUserSubscribedAndVerified(props.user.token); 11 | if (status.responseData === 'OK') { 12 | setButtonText('Successfully sent unsubscribe emails!'); 13 | setButtonColor('green'); 14 | setTimeout(() => { 15 | setButtonText('Send Unsubscribe Email to All'); 16 | setButtonColor(''); 17 | }, 3000); 18 | } 19 | }; 20 | 21 | return ( 22 |
23 |
24 |
25 |

26 | What does clicking this button do? 27 |

28 |

29 | Sends a request to the backend to allow all eligible users to 30 | unsubscribe from club update emails. 31 |

32 |

33 | The server finds all accounts with emails that are have verified 34 | emails and are opted into club updates. 35 |

36 |

37 | For each email satisfying the above condition, an email is sent 38 | to each user with a link to a page where they can manage their preferences. 39 |

40 |
41 |
42 | 45 |
46 |
47 |
48 | ); 49 | } 50 | 51 | export default AdminDashboard; 52 | -------------------------------------------------------------------------------- /src/Pages/Projects/Components/ProjectCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function ProjectCard(project) { 4 | return ( 5 |
6 |
7 |
8 | Album 12 |
13 |
14 |

{project.name}

15 |

{project.caption}

16 | 17 | {project.information} 18 | 19 | 31 |
32 |
33 |
34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/Pages/Projects/Projects.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ProjectCard from './Components/ProjectCard'; 3 | 4 | const projects = [ 5 | { 6 | 'link': 'https://github.com/SCE-Development/Clark', 7 | 'image': 'https://user-images.githubusercontent.com/59713070/235862105-9606e862-e27e-40d4-8991-de1793c48dd0.png', 8 | 'name': 'Clark', 'subnote': '(formerly Core-v4)', 9 | 'information': 'Full Stack', 10 | 'caption': 'React, Express.js, and MongoDB; Clark is the club\'s website. It supports printing services for members and allows officers to control various devices in the clubroom.' 11 | }, 12 | { 13 | 'link': 'https://github.com/SCE-Development/rpi-led-controller', 14 | 'image': 'https://user-images.githubusercontent.com/59713070/235859723-cdea1a8e-5698-40c2-9755-9ec2e40984cd.jpeg', 15 | 'name': 'SCE Light-Emitting Display', 16 | 'information': 'Interfacing RESTful APIs with Hardware', 17 | 'caption': 'Produced as a part of our summer internship projects, SCE interns designed an officer-controlled illuminated sign, functioning to brighten the clubroom\'s atmosphere.' 18 | }, 19 | { 20 | 'link': 'https://github.com/SCE-Development/Clark', 21 | 'image': 'https://github.com/user-attachments/assets/1637dc25-2073-43e5-a952-c1a3d50d16fe', 22 | 'name': 'SCEta Transit', 23 | 'information': 'Full Stack', 24 | 'caption': 'SCETA Transit is a web application that provides real-time bus, Caltrain, and BART timing predictions for nearby stops.' 25 | }, 26 | { 27 | 'link': 'https://github.com/SCE-Development/Clark', 28 | 'image': 'https://github.com/user-attachments/assets/204dc7d7-e7a1-4add-ae6b-37286ba1c510', 29 | 'name': 'SCE Chatroom', 30 | 'information': 'Full Stack', 31 | 'caption': 'SCE\'s chatroom is a web application that allows members to communicate with each other in real-time.' 32 | }, 33 | { 34 | 'link': 'https://github.com/SCE-Development/cleezy', 35 | 'image': 'https://github.com/user-attachments/assets/de1017ac-ca79-46e5-b3b4-5e62666713fb', 36 | 'name': 'Cleezy', 37 | 'information': 'FastAPI', 38 | 'caption': 'A url shortening service created by SCE' 39 | }, 40 | ]; 41 | 42 | export default function ProjectsPage() { 43 | return ( 44 |
45 |
46 |
47 |

Our Recent Projects

48 |
49 |

The SCE Development Team is open to all students, no prior experience is required!

50 |
51 |
52 | {projects.map((project) => ( 53 | <> 54 | 55 |
56 |
57 | 58 | ))} 59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/Pages/URLShortener/SVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | 4 | export function trashcanSymbol() { 5 | return ( 6 | 7 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/Pages/UserManager/ExpirationDropdown.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | 4 | export default function ExpirationDropdown(props) { 5 | /** 6 | * Checks if current semester is spring or fall. Initializes 7 | * expDate1 and expDate2 to a String that represents the 8 | * expiration date for one and two semesters, respectively. 9 | * @returns {Array} Array of two Strings: expDate1 and expDate2 10 | */ 11 | 12 | function membershipExpDate() { 13 | const date = new Date(); 14 | let expDate1 = ''; 15 | let expDate2 = ''; 16 | // spring checks if current month is between January and May 17 | let spring = date.getMonth() >= 0 && date.getMonth() <= 4; 18 | if (spring) { 19 | expDate1 = `June 1, ${date.getFullYear()}`; 20 | expDate2 = `Jan 1, ${date.getFullYear() + 1}`; 21 | } else { 22 | expDate1 = `Jan 1, ${date.getFullYear() + 1}`; 23 | expDate2 = `June 1, ${date.getFullYear() + 1}`; 24 | } 25 | return [expDate1, expDate2]; 26 | } 27 | const expDates = membershipExpDate(); 28 | const membership = [ 29 | { value: null, name: 'Keep Same' }, 30 | { value: 0, name: 'Expired Membership' }, 31 | { value: 1, name: `This semester (${expDates[0]})` }, 32 | { value: 2, name: `2 semesters (${expDates[1]})` } 33 | ]; 34 | 35 | return ( 36 | <> 37 | 38 |
39 | 51 |
52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/Pages/UserManager/RoleDropdown.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const enums = require('../../Enums.js'); 4 | 5 | 6 | export default function RoleDropdown(props) { 7 | return ( 8 | <> 9 | 12 |
13 | 30 |
31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/config/config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "sitekey": "super secret key", 3 | "TINYMCE_API_KEY": "XXXXXXXXXXXXXXXXXXXXX", 4 | "GOOGLE_API_CLIENT_ID": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.apps.googleusercontent.com" 5 | } 6 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { withRouter, BrowserRouter } from 'react-router-dom'; 4 | import './index.css'; 5 | 6 | import Routing from './Routing'; 7 | import { checkIfUserIsSignedIn } from './APIFunctions/Auth'; 8 | 9 | 10 | function App(props) { 11 | const [authenticated, setAuthenticated] = useState(false); 12 | const [isAuthenticating, setIsAuthenticating] = useState(true); 13 | const [user, setUser] = useState(); 14 | 15 | async function getAuthStatus() { 16 | setIsAuthenticating(true); 17 | const authStatus = await checkIfUserIsSignedIn(); 18 | setAuthenticated(!authStatus.error); 19 | setUser({ token: authStatus.token, ...authStatus.responseData}); 20 | setIsAuthenticating(false); 21 | } 22 | 23 | useEffect(() => { 24 | getAuthStatus(); 25 | // eslint-disable-next-line 26 | }, []) 27 | 28 | return ( 29 | !isAuthenticating && ( 30 | 31 | 32 | 33 | ) 34 | ); 35 | } 36 | 37 | export default withRouter(App); 38 | 39 | ReactDOM.render(, document.getElementById('root')); 40 | -------------------------------------------------------------------------------- /src/input.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./src/**/*.{html,js}'], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [require('daisyui')], 8 | }; 9 | 10 | -------------------------------------------------------------------------------- /test/api/Advertisement.js: -------------------------------------------------------------------------------- 1 | /* global describe it before after */ 2 | process.env.NODE_ENV = 'test'; 3 | const Advertisement = require('../../api/main_endpoints/models/Advertisement'); 4 | const chai = require('chai'); 5 | const chaiHttp = require('chai-http'); 6 | const constants = require('../../api/util/constants'); 7 | const { OK, BAD_REQUEST } = constants.STATUS_CODES; 8 | const SceApiTester = require('../../test/util/tools/SceApiTester'); 9 | 10 | let app = null; 11 | let test = null; 12 | const expect = chai.expect; 13 | 14 | const tools = require('../util/tools/tools.js'); 15 | chai.should(); 16 | chai.use(chaiHttp); 17 | 18 | describe('Advertisement', () => { 19 | before(done => { 20 | app = tools.initializeServer( 21 | __dirname + '/../../api/main_endpoints/routes/Advertisement.js'); 22 | test = new SceApiTester(app); 23 | tools.emptySchema(Advertisement); 24 | done(); 25 | }); 26 | 27 | after(done => { 28 | tools.terminateServer(done); 29 | }); 30 | 31 | const INVALID_ADVERTISEMENT = { 32 | createDate: new Date('01/01/2001') 33 | }; 34 | 35 | const VALID_ADVERTISEMENT = { 36 | pictureUrl: 37 | 'https://www.fosi.org/', 38 | createDate: new Date('01/01/2001'), 39 | expireDate: new Date('10/10/2001') 40 | }; 41 | 42 | }); 43 | -------------------------------------------------------------------------------- /test/api/LedSign.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test'; 2 | 3 | const chai = require('chai'); 4 | const chaiHttp = require('chai-http'); 5 | const sinon = require('sinon'); 6 | const tools = require('../util/tools/tools'); 7 | const SceApiTester = require('../util/tools/SceApiTester'); 8 | const { 9 | OK, 10 | SERVER_ERROR, 11 | UNAUTHORIZED, 12 | } = require('../../api/util/constants').STATUS_CODES; 13 | const { 14 | initializeTokenMock, 15 | setTokenStatus, 16 | resetTokenMock, 17 | restoreTokenMock, 18 | } = require('../util/mocks/TokenValidFunctions'); 19 | const SshTunnelFunctions = require('../../api/main_endpoints/util/LedSign'); 20 | 21 | 22 | let app = null; 23 | let test = null; 24 | let sandbox = sinon.createSandbox(); 25 | const expect = chai.expect; 26 | 27 | chai.should(); 28 | chai.use(chaiHttp); 29 | 30 | const token = ''; 31 | 32 | 33 | describe('LED Sign', () => { 34 | let updateSignStub = null; 35 | let healthCheckStub = null; 36 | 37 | before(done => { 38 | initializeTokenMock(); 39 | updateSignStub = sandbox.stub(SshTunnelFunctions, 'updateSign'); 40 | healthCheckStub = sandbox.stub(SshTunnelFunctions, 'healthCheck'); 41 | updateSignStub.resolves(false); 42 | healthCheckStub.resolves(false); 43 | app = tools.initializeServer( 44 | __dirname + '/../../api/main_endpoints/routes/LedSign.js'); 45 | test = new SceApiTester(app); 46 | done(); 47 | }); 48 | 49 | after(done => { 50 | restoreTokenMock(); 51 | if (updateSignStub) updateSignStub.restore(); 52 | if (healthCheckStub) healthCheckStub.restore(); 53 | sandbox.restore(); 54 | tools.terminateServer(done); 55 | }); 56 | 57 | beforeEach(() => { 58 | setTokenStatus(false); 59 | updateSignStub.resolves(false); 60 | healthCheckStub.resolves(false); 61 | }); 62 | 63 | afterEach(() => { 64 | resetTokenMock(); 65 | }); 66 | 67 | describe('/POST updateSignText', () => { 68 | it('Should return 400 when token is not sent', async () => { 69 | const result = await test.sendPostRequest('/api/LedSign/updateSignText'); 70 | expect(result).to.have.status(UNAUTHORIZED); 71 | }); 72 | 73 | it('Should return 400 when invalid token is sent', async () => { 74 | const result = await test.sendPostRequestWithToken(token, 75 | '/api/LedSign/updateSignText'); 76 | expect(result).to.have.status(UNAUTHORIZED); 77 | }); 78 | 79 | it('Should return 500 when the ssh tunnel is down', async () => { 80 | setTokenStatus(true); 81 | updateSignStub.resolves(false); 82 | const result = await test.sendPostRequestWithToken(token, 83 | '/api/LedSign/updateSignText'); 84 | expect(result).to.have.status(SERVER_ERROR); 85 | }); 86 | 87 | it('Should return 200 when the ssh tunnel is up', async () => { 88 | setTokenStatus(true); 89 | updateSignStub.resolves(true); 90 | const result = await test.sendPostRequestWithToken(token, 91 | '/api/LedSign/updateSignText'); 92 | expect(result).to.have.status(OK); 93 | }); 94 | }); 95 | 96 | describe('/GET healthCheck', () => { 97 | it('Should return 500 when the ssh tunnel is down', async () => { 98 | setTokenStatus(true); 99 | healthCheckStub.resolves(false); 100 | const result = await test.sendGetRequest('/api/LedSign/healthCheck'); 101 | expect(result).to.have.status(SERVER_ERROR); 102 | }); 103 | 104 | it('Should return 200 when the ssh tunnel is up', async () => { 105 | setTokenStatus(true); 106 | healthCheckStub.resolves(true); 107 | const result = await test.sendGetRequest('/api/LedSign/healthCheck'); 108 | expect(result).to.have.status(OK); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /test/api/Mailer.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test'; 2 | 3 | const chai = require('chai'); 4 | const chaiHttp = require('chai-http'); 5 | const sinon = require('sinon'); 6 | const constants = require('../../api/util/constants'); 7 | const { OK, BAD_REQUEST } = constants.STATUS_CODES; 8 | const SceApiTester = require('../util/tools/SceApiTester'); 9 | const { SceGoogleApiHandler } = 10 | require('../../api/cloud_api/util/SceGoogleApiHandler'); 11 | const verificationTemplate = 12 | require('../../api/cloud_api/email_templates/verification'); 13 | 14 | let app = null; 15 | let test = null; 16 | let sandbox = sinon.createSandbox(); 17 | const expect = chai.expect; 18 | const tools = require('../util/tools/tools.js'); 19 | 20 | chai.should(); 21 | chai.use(chaiHttp); 22 | 23 | describe('Mailer', () => { 24 | let sendEmailStub = null; 25 | let verificationStub = null; 26 | before(done => { 27 | sendEmailStub = sandbox.stub(SceGoogleApiHandler.prototype, 'sendEmail'); 28 | verificationStub = sandbox.stub(verificationTemplate, 'verification'); 29 | app = tools.initializeServer( 30 | __dirname + '/../../api/cloud_api/routes/Mailer.js'); 31 | test = new SceApiTester(app); 32 | done(); 33 | }); 34 | 35 | after(done => { 36 | if (sendEmailStub) sendEmailStub.restore(); 37 | if (verificationStub) verificationStub.restore(); 38 | sandbox.restore(); 39 | tools.terminateServer(done); 40 | }); 41 | 42 | const VALID_EMAIL_REQUEST = { 43 | recipientEmail: 'a@a.com', 44 | recipientName: 'test' 45 | }; 46 | 47 | describe('/POST sendVerificationEmail', () => { 48 | it('Should return 200 when an email is successfully sent', async () => { 49 | sendEmailStub.resolves({}); 50 | verificationStub.resolves({}); 51 | const result = await test.sendPostRequest( 52 | '/api/Mailer/sendVerificationEmail', VALID_EMAIL_REQUEST); 53 | expect(result).to.have.status(OK); 54 | }); 55 | 56 | it('Should return 400 when we cannot generate a hashed ID', async () => { 57 | sendEmailStub.resolves({}); 58 | verificationStub.rejects({}); 59 | const result = await test.sendPostRequest( 60 | '/api/Mailer/sendVerificationEmail', VALID_EMAIL_REQUEST); 61 | expect(result).to.have.status(BAD_REQUEST); 62 | }); 63 | 64 | it('Should return 400 when sending an email fails', async () => { 65 | sendEmailStub.rejects({}); 66 | const result = await test.sendPostRequest( 67 | '/api/Mailer/sendVerificationEmail', VALID_EMAIL_REQUEST); 68 | expect(result).to.have.status(BAD_REQUEST); 69 | }); 70 | }); 71 | 72 | describe('/POST sendBlastEmail', () => { 73 | it('Should return 200 when an email is successfully sent', async () => { 74 | sendEmailStub.resolves({}); 75 | verificationStub.resolves({}); 76 | const result = await test.sendPostRequest( 77 | '/api/Mailer/sendBlastEmail', 78 | VALID_EMAIL_REQUEST 79 | ); 80 | expect(result).to.have.status(OK); 81 | }); 82 | 83 | it('Should return 400 when sending an email fails', async () => { 84 | sendEmailStub.rejects({}); 85 | const result = await test.sendPostRequest( 86 | '/api/Mailer/sendBlastEmail', 87 | VALID_EMAIL_REQUEST 88 | ); 89 | expect(result).to.have.status(BAD_REQUEST); 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /test/api/Messages.js: -------------------------------------------------------------------------------- 1 | /* global describe it before after beforeEach afterEach */ 2 | // During the test the env variable is set to test 3 | process.env.NODE_ENV = 'test'; 4 | 5 | const User = require('../../api/main_endpoints/models/User.js'); 6 | 7 | // Require the dev-dependencies 8 | const chai = require('chai'); 9 | const mongoose = require('mongoose'); 10 | let id = new mongoose.Types.ObjectId(); 11 | 12 | const chaiHttp = require('chai-http'); 13 | const { 14 | OK, 15 | BAD_REQUEST, 16 | UNAUTHORIZED, 17 | NOT_FOUND, 18 | FORBIDDEN 19 | } = require('../../api/util/constants').STATUS_CODES; 20 | const sinon = require('sinon'); 21 | const SceApiTester = require('../util/tools/SceApiTester'); 22 | 23 | 24 | 25 | let app = null; 26 | let test = null; 27 | let sandbox = sinon.createSandbox(); 28 | 29 | const expect = chai.expect; 30 | const tools = require('../util/tools/tools.js'); 31 | const { 32 | setTokenStatus, 33 | resetTokenMock, 34 | restoreTokenMock, 35 | initializeTokenMock 36 | } = require('../util/mocks/TokenValidFunctions'); 37 | 38 | const { MEMBERSHIP_STATE } = require('../../api/util/constants'); 39 | 40 | chai.should(); 41 | chai.use(chaiHttp); 42 | 43 | 44 | describe('Messages', () => { 45 | before(done => { 46 | initializeTokenMock(); 47 | app = tools.initializeServer([ 48 | __dirname + '/../../api/main_endpoints/routes/Auth.js', 49 | __dirname + '/../../api/main_endpoints/routes/Messages.js', 50 | ]); 51 | test = new SceApiTester(app); 52 | // Before each test we empty the database 53 | tools.emptySchema(User); 54 | done(); 55 | }); 56 | 57 | after(done => { 58 | restoreTokenMock(); 59 | tools.terminateServer(done); 60 | }); 61 | 62 | beforeEach(() => { 63 | setTokenStatus(false); 64 | }); 65 | 66 | afterEach(() => { 67 | resetTokenMock(); 68 | }); 69 | 70 | const token = ''; 71 | 72 | describe('POST /send', () => { 73 | let user; 74 | let userToken; 75 | 76 | before(async () => { 77 | user = new User({ 78 | _id: id, 79 | firstName: 'first-name', 80 | lastName: 'last-name', 81 | email: 'test1@user.com', 82 | password: 'Passw0rd', 83 | emailVerified: true, 84 | accessLevel: MEMBERSHIP_STATE.MEMBER, 85 | apiKey: '123' 86 | }); 87 | await user.save(); 88 | 89 | const loginResponse = await test.sendPostRequest('/api/Auth/login', { 90 | email: user.email, 91 | password: 'Passw0rd' 92 | }); 93 | userToken = loginResponse.body.token; 94 | }); 95 | 96 | it('Should return status code 200 if valid token, room-id, and message was sent', async () => { 97 | const form = { 98 | message: 'Hello', 99 | id: 'general' 100 | }; 101 | setTokenStatus(true, {_id: id}); 102 | const result = await test.sendPostRequestWithToken(userToken, '/api/messages/send', form); 103 | expect(result).to.have.status(OK); 104 | }); 105 | 106 | it('Should return status code 400 if no token is found', async () => { 107 | const form = { 108 | message: 'Hello', 109 | id: 'general' 110 | }; 111 | setTokenStatus(true); 112 | const result = await test.sendPostRequest('/api/messages/send', form); 113 | expect(result).to.have.status(BAD_REQUEST); 114 | }); 115 | 116 | it('Should return status code 401 if token is invalid', async () => { 117 | const form = { 118 | message: 'Hello', 119 | id: 'general' 120 | }; 121 | const result = await test.sendPostRequestWithToken('invalid', '/api/messages/send', form); 122 | expect(result).to.have.status(UNAUTHORIZED); 123 | }); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /test/api/TokenFunctions.js: -------------------------------------------------------------------------------- 1 | /* global describe it before after afterEach */ 2 | 3 | const sinon = require('sinon'); 4 | const chai = require('chai'); 5 | const expect = chai.expect; 6 | const proxyquire = require('proxyquire'); 7 | 8 | const requestWithToken = { 9 | headers: { 10 | authorization: 'Bearer hi thai', 11 | }, 12 | body: { 13 | accessLevel: 2, 14 | } 15 | }; 16 | const requestWithoutToken = { 17 | body: {} 18 | }; 19 | let tokenFunctions; 20 | let jwtStub; 21 | 22 | 23 | describe('TokenFunctions', () => { 24 | jwtStub = sinon.stub(); 25 | beforeEach(done => { 26 | tokenFunctions = proxyquire('../../api/main_endpoints/util/token-functions', 27 | { 28 | jsonwebtoken: { 29 | verify: jwtStub 30 | } 31 | }); 32 | done(); 33 | }); 34 | describe('checkIfTokenSent', () => { 35 | it('Should return true if a token field exists in the request', done => { 36 | expect(tokenFunctions.checkIfTokenSent(requestWithToken)).to.equal(true); 37 | done(); 38 | }); 39 | it('Should return false if a token field does ' + 40 | 'not exist in the request', done => { 41 | expect(tokenFunctions.checkIfTokenSent(requestWithoutToken)) 42 | .to.equal(false); 43 | done(); 44 | }); 45 | }); 46 | 47 | describe('checkIfTokenValid', () => { 48 | it('Should return the decoded response ', done => { 49 | jwtStub.yields(false, requestWithToken.body); 50 | expect(tokenFunctions.checkIfTokenValid(requestWithToken)) 51 | .to.equal(true); 52 | done(); 53 | }); 54 | it('Should return false if a token field ' + 55 | 'does not exist in the request', done => { 56 | jwtStub.yields(true, false); 57 | expect(tokenFunctions.checkIfTokenValid(requestWithToken)) 58 | .to.equal(false); 59 | done(); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/api/registerUser.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test'; 2 | const { getMemberExpirationDate } = 3 | require('../../api/main_endpoints/util/userHelpers.js'); 4 | 5 | const chai = require('chai'); 6 | const { expect, assert } = chai; 7 | 8 | const { mockMonth, revertClock } = require('../util/mocks/Date'); 9 | 10 | const SEPTEMBER = 8; 11 | const FEBRUARY = 1; 12 | const JUNE = 5; 13 | const JANUARY = 0; 14 | const DECEMBER = 11; 15 | const MAY = 4; 16 | const TODAY = new Date(); 17 | 18 | describe('registerUser', () => { 19 | describe('getMemberExpirationDate', () => { 20 | afterEach(done => { 21 | // get rid of the stub 22 | revertClock(); 23 | done(); 24 | }); 25 | it('should return june for a single semester in spring', () => { 26 | mockMonth(FEBRUARY); 27 | let result = getMemberExpirationDate(1); 28 | const testDate = new Date(TODAY.getFullYear(), JUNE); 29 | expect(result).deep.equal(testDate); 30 | }); 31 | it('should return january of next year for a single semester in fall', 32 | () => { 33 | mockMonth(SEPTEMBER); 34 | let result = getMemberExpirationDate(1); 35 | const testDate = new Date(TODAY.getFullYear() + 1, JANUARY); 36 | expect(result).deep.equal(testDate); 37 | }); 38 | it('should return january of next year for two semeseters in spring', 39 | () => { 40 | mockMonth(FEBRUARY); 41 | let result = getMemberExpirationDate(2); 42 | const testDate = new Date(TODAY.getFullYear() + 1, JANUARY); 43 | expect(result).deep.equal(testDate); 44 | }); 45 | it('should return june of next year for two semesters in fall', () => { 46 | mockMonth(SEPTEMBER); 47 | let result = getMemberExpirationDate(2); 48 | const testDate = new Date(TODAY.getFullYear() + 1, JUNE); 49 | expect(result).deep.equal(testDate); 50 | }); 51 | it('should return january of next year for one semester in december', 52 | () => { 53 | mockMonth(DECEMBER); 54 | let result = getMemberExpirationDate(1); 55 | const testDate = new Date(TODAY.getFullYear() + 1, JANUARY); 56 | expect(result).deep.equal(testDate); 57 | }); 58 | it('should return june first for one semester in may', () => { 59 | mockMonth(MAY); 60 | let result = getMemberExpirationDate(1); 61 | const testDate = new Date(TODAY.getFullYear(), JUNE); 62 | expect(result).deep.equal(testDate); 63 | }); 64 | it('should return today for 0 semester', () => { 65 | mockMonth(MAY); 66 | const testDate = new Date(TODAY.getFullYear(), MAY); 67 | let result = getMemberExpirationDate(0); 68 | expect(result).deep.equal(testDate); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/frontend/AboutPage.test.js: -------------------------------------------------------------------------------- 1 | import 'jsdom-global/register'; 2 | import React from 'react'; 3 | import Enzyme, { mount } from 'enzyme'; 4 | import { expect } from 'chai'; 5 | import Adapter from '@cfaester/enzyme-adapter-react-18'; 6 | 7 | import AboutPage from '../../src/Pages/About/About'; 8 | 9 | Enzyme.configure({ adapter: new Adapter() }); 10 | 11 | describe('', () => { 12 | const wrapper = mount(); 13 | it('Should render the main heading', () => { 14 | expect(wrapper.find('h1').text()).to.equal('What Happens at SCE'); 15 | }); 16 | it('Should render the Introduction text', () => { 17 | const subheading = wrapper.find('h2').at(0); 18 | expect(subheading.text()).to.include('Introduction'); 19 | }); 20 | it('Should render the Location text', () => { 21 | const subheading = wrapper.find('h2').at(1); 22 | expect(subheading.text()).to.include('Location'); 23 | }); 24 | it('Should render club hours information', () => { 25 | const preformatted = wrapper.find('pre').at(0); 26 | expect(preformatted.text()).to.include('Monday - Thursday: 10:00 AM - 5:00 PM'); 27 | expect(preformatted.text()).to.include('Friday: 10:00 AM - 2:00 PM'); 28 | }); 29 | it('Should render membership fee information', () => { 30 | const companies = wrapper.find('pre').at(1); 31 | expect(companies.text()).to.include('Single semester: $20'); 32 | expect(companies.text()).to.include('Two semesters: $30'); 33 | }); 34 | it('Should render the image with the correct alt text', () => { 35 | expect(wrapper.find('img').prop('alt')).to.equal('sce collage'); 36 | }); 37 | it('Should render the link to Discord with the correct URL', () => { 38 | const discordLink = wrapper.find('a').at(0); 39 | expect(discordLink.prop('href')).to.equal('https://sce.sjsu.edu/s/discord'); 40 | expect(discordLink.text()).to.equal('https://sce.sjsu.edu/s/discord'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/frontend/DecisionModal.test.js: -------------------------------------------------------------------------------- 1 | import 'jsdom-global/register'; 2 | import React from 'react'; 3 | import Enzyme, { mount } from 'enzyme'; 4 | import { expect } from 'chai'; 5 | import Adapter from '@cfaester/enzyme-adapter-react-18'; 6 | 7 | import ConfirmationModal from '../../src/Components/DecisionModal/ConfirmationModal'; 8 | 9 | Enzyme.configure({ adapter: new Adapter() }); 10 | 11 | describe('', () => { 12 | const modalProps = { 13 | headerText: 'This the test header', 14 | bodyText: 'test body', 15 | }; 16 | const wrapper = mount(); 17 | it('Should render the headerText prop in a h3 tag', () => { 18 | expect(wrapper.find('h3').text()).to.equal(modalProps.headerText); 19 | }); 20 | it('Should render the bodyText prop in a p tag', () => { 21 | expect(wrapper.find('p').text()).to.equal(modalProps.bodyText); 22 | }); 23 | it('Should render \'Confirm\' when no prop is passed', () => { 24 | const wrapper = mount(); 25 | expect(wrapper.find('button').at(0).text()).to.equal('Confirm'); 26 | }); 27 | it('Should render \'Cancel\' when no prop is passed', () => { 28 | const wrapper = mount(); 29 | expect(wrapper.find('button').at(1).text()).to.equal('Cancel'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/frontend/MessagingModal.test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SCE-Development/Clark/46801ff13fd8842b17e29fdd99094097800292da/test/frontend/MessagingModal.test.js -------------------------------------------------------------------------------- /test/util/mocks/Date.js: -------------------------------------------------------------------------------- 1 | const sinon = require('sinon'); 2 | // import sinon from 'sinon'; 3 | 4 | let clock = null; 5 | 6 | /** 7 | * Mock current time to desired day, month and year 8 | * @param {Number} day - day to mock 9 | * @param {Number} month - month to mock 10 | * @param {Number} year - month to mock 11 | */ 12 | function mockDayMonthAndYear(day, month, year) { 13 | clock = sinon.useFakeTimers({ 14 | now: new Date(year, month, day), 15 | toFake: [ 16 | 'setTimeout', 'clearTimeout', 'setImmediate', 'clearImmediate', 17 | 'setInterval', 'clearInterval', 'Date' 18 | ], 19 | }); 20 | } 21 | 22 | /** 23 | * Mock current time to desired month and year 24 | * @param {Number} month - month to mock 25 | * @param {Number} year - month to mock 26 | */ 27 | function mockMonthAndYear(month, year) { 28 | clock = sinon.useFakeTimers({ 29 | now: new Date(year, month), 30 | toFake: [ 31 | 'setTimeout', 'clearTimeout', 'setImmediate', 'clearImmediate', 32 | 'setInterval', 'clearInterval', 'Date' 33 | ], 34 | }); 35 | } 36 | 37 | /** 38 | * Mock current time to desired month. The year of the mocked date 39 | * is whatever the current year was when this function was called. 40 | * @param {Number} month - month to mock 41 | */ 42 | function mockMonth(month) { 43 | mockMonthAndYear(month, new Date().getFullYear()); 44 | } 45 | 46 | /** 47 | * Reset clock, use after mock 48 | */ 49 | function revertClock() { 50 | if (clock) clock.restore(); 51 | } 52 | 53 | module.exports = { mockDayMonthAndYear, mockMonthAndYear, mockMonth, revertClock }; 54 | -------------------------------------------------------------------------------- /test/util/mocks/DiscordApiFunction.js: -------------------------------------------------------------------------------- 1 | const DiscordValidation = require('../../../api/util/token-verification'); 2 | const sinon = require('sinon'); 3 | 4 | let discordApiKeyMock = null; 5 | 6 | /** 7 | * Initialize the stub to be used in other functions. 8 | */ 9 | function initializeDiscordAPIMock() { 10 | discordApiKeyMock = sinon.stub(DiscordValidation, 'checkDiscordKey'); 11 | } 12 | 13 | /** 14 | * Restore sinon's stub, function returned to its original state 15 | */ 16 | function restoreDiscordAPIMock() { 17 | discordApiKeyMock.restore(); 18 | } 19 | 20 | /** 21 | * Reset sinon-stub's call, reset onCall-function back to the beginning 22 | */ 23 | function resetDiscordAPIMock() { 24 | discordApiKeyMock.reset(); 25 | } 26 | 27 | /** 28 | * 29 | * @param {Boolean} returnValue 30 | * @returns the value of the boolean param. 31 | */ 32 | function setDiscordAPIStatus(returnValue) { 33 | if (returnValue) { 34 | discordApiKeyMock.returns(true); 35 | } else { 36 | discordApiKeyMock.returns(false); 37 | } 38 | } 39 | 40 | module.exports = { 41 | setDiscordAPIStatus, 42 | resetDiscordAPIMock, 43 | restoreDiscordAPIMock, 44 | initializeDiscordAPIMock 45 | }; 46 | -------------------------------------------------------------------------------- /test/util/mocks/SceSqsApiHandler.js: -------------------------------------------------------------------------------- 1 | const sinon = require('sinon'); 2 | const { SceSqsApiHandler } = require( 3 | '../../../api/main_endpoints/util/SceSqsApiHandler.js'); 4 | 5 | 6 | let sqsHandlerConstructorMock = null; 7 | let pushMessageToQueueMock = null; 8 | /** 9 | * Initialize the stub to be used 10 | */ 11 | function initializeSqsMock() { 12 | sqsHandlerConstructorMock = sinon.stub(SceSqsApiHandler.prototype, 13 | 'constructor').returnsThis(); 14 | pushMessageToQueueMock = sinon.stub(SceSqsApiHandler.prototype, 15 | 'pushMessageToQueue'); 16 | } 17 | 18 | /** 19 | * Restore sinon's stub, function returned to its original state 20 | */ 21 | function restoreSqsMock() { 22 | sqsHandlerConstructorMock.restore(); 23 | pushMessageToQueueMock.restore(); 24 | } 25 | 26 | /** 27 | * Reset sinon-stub's call, reset onCall-function back to the beginning 28 | */ 29 | function resetSqsMock() { 30 | sqsHandlerConstructorMock.reset(); 31 | pushMessageToQueueMock.reset(); 32 | } 33 | 34 | /** 35 | * Set the values 36 | * @param {Boolean} returnValue: value to be return back 37 | * by the function 'pushMessageToQueue' 38 | */ 39 | function setSqsResponse(returnValue) { 40 | pushMessageToQueueMock.resolves(returnValue); 41 | } 42 | 43 | module.exports = { 44 | initializeSqsMock, 45 | restoreSqsMock, 46 | resetSqsMock, 47 | setSqsResponse 48 | }; 49 | -------------------------------------------------------------------------------- /test/util/mocks/TokenValidFunctions.js: -------------------------------------------------------------------------------- 1 | const TokenFunctions = require( 2 | '../../../api/main_endpoints/util/token-functions'); 3 | const sinon = require('sinon'); 4 | 5 | let checkifTokenValidMock = null; 6 | let decodeTokenValidMock = null; 7 | 8 | /** 9 | * Initialize the stub to be used in other functions. 10 | */ 11 | function initializeTokenMock() { 12 | checkifTokenValidMock = sinon.stub(TokenFunctions, 'checkIfTokenValid'); 13 | decodeTokenValidMock = sinon.stub(TokenFunctions, 'decodeToken'); 14 | } 15 | 16 | /** 17 | * Restore sinon's stub, function returned to its original state 18 | */ 19 | function restoreTokenMock() { 20 | checkifTokenValidMock.restore(); 21 | decodeTokenValidMock.restore(); 22 | } 23 | 24 | /** 25 | * Reset sinon-stub's call, reset onCall-function back to the beginning 26 | */ 27 | function resetTokenMock() { 28 | checkifTokenValidMock.reset(); 29 | decodeTokenValidMock.reset(); 30 | } 31 | 32 | /** 33 | * 34 | * @param {any} returnValue: value to be return back 35 | * by the function 'checkIfTokenValid' 36 | * @param {Object} data: optional value that will be the result 37 | * of the decoded token value 38 | * @returns return parameter (above) 39 | */ 40 | function setTokenStatus( 41 | returnValue, 42 | data = {}, 43 | ) { 44 | checkifTokenValidMock.returns(returnValue); 45 | if (returnValue) { 46 | decodeTokenValidMock.returns(data); 47 | } else { 48 | decodeTokenValidMock.returns(null); 49 | } 50 | } 51 | 52 | module.exports = { 53 | setTokenStatus, resetTokenMock, restoreTokenMock, initializeTokenMock 54 | }; 55 | -------------------------------------------------------------------------------- /test/util/mocks/react-router-dom.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const rrd = require('react-router-dom'); 3 | // Just render plain div with its children 4 | rrd.BrowserRouter = ({ children }) =>
{children}
; 5 | module.exports = rrd; 6 | -------------------------------------------------------------------------------- /test/util/tools/SceApiTester.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test'; 2 | 3 | const chai = require('chai'); 4 | const chaiHttp = require('chai-http'); 5 | chai.should(); 6 | chai.use(chaiHttp); 7 | 8 | /** 9 | * Handles any POST and GET request made during API testing. 10 | */ 11 | class SceApiTester{ 12 | constructor(app){ 13 | this.app = app; 14 | } 15 | 16 | /** 17 | * Creates a chai POST request to test an API route. 18 | * @param {String} endpoint contains the path of the route being tested. 19 | * @param {Object} params the parameters specified for the test. 20 | * @returns {Promise} the outcome of the API call. 21 | */ 22 | async sendPostRequest(endpoint, params){ 23 | let response = null; 24 | await chai 25 | .request(this.app) 26 | .post(endpoint) 27 | .send(params) 28 | .then(function(res) { 29 | response = res; 30 | }) 31 | .catch(err =>{ 32 | throw err; 33 | }); 34 | return response; 35 | } 36 | 37 | /** 38 | * Creates a chai POST request with an access token to test an API route. 39 | * @param {Object} token the access token for the API call 40 | * @param {String} endpoint contains the path of the route being tested. 41 | * @param {Object} params the parameters specified for the test. 42 | * @returns {Promise} the outcome of the API call. 43 | */ 44 | async sendPostRequestWithToken(token, endpoint, params = {}){ 45 | let response = null; 46 | await chai 47 | .request(this.app) 48 | .post(endpoint) 49 | .set('Authorization', `Bearer ${token}`) 50 | .send({ token, ...params }) 51 | .then(function(res) { 52 | response = res; 53 | }) 54 | .catch(err =>{ 55 | throw err; 56 | }); 57 | return response; 58 | } 59 | 60 | /** 61 | * Creates a chai GET request to test an API route. 62 | * @param {String} endpoint contains the path of the route being tested. 63 | * @returns {Promise} the outcome of the API call. 64 | */ 65 | async sendGetRequest(endpoint){ 66 | let response = null; 67 | await chai 68 | .request(this.app) 69 | .get(endpoint) 70 | .then(function(res) { 71 | response = res; 72 | }) 73 | .catch(err =>{ 74 | throw err; 75 | }); 76 | return response; 77 | } 78 | 79 | async sendGetRequestWithToken(token, endpoint) { 80 | let response = null; 81 | await chai 82 | .request(this.app) 83 | .get(endpoint) 84 | .set('Authorization', `Bearer ${token}`) 85 | .send({token : token}) 86 | .then(function(res) { 87 | response = res; 88 | }) 89 | .catch(err =>{ 90 | throw err; 91 | }); 92 | return response; 93 | } 94 | 95 | sendGetRequestWithApiKey(apiKey, endpoint) { 96 | return new Promise((resolve, reject) => { 97 | chai 98 | .request(this.app) 99 | .get(endpoint) 100 | .set('X-API-Key', apiKey) 101 | .then(resolve) 102 | .catch(reject); 103 | }); 104 | } 105 | } 106 | 107 | module.exports = SceApiTester; 108 | -------------------------------------------------------------------------------- /test/util/tools/tools.js: -------------------------------------------------------------------------------- 1 | const { SceHttpServer } = require('../../../api/util/SceHttpServer'); 2 | 3 | let serverInstance = null; 4 | 5 | function emptySchema(schema) { 6 | schema.deleteMany({}, err => { 7 | if (err) { 8 | // 9 | } 10 | }); 11 | } 12 | 13 | function insertItem(schema, item) { 14 | schema.create(item, (err) => { 15 | if(err) { 16 | // 17 | } 18 | }); 19 | } 20 | 21 | function initializeServer(path, port = 7999) { 22 | serverInstance = new SceHttpServer(path, port); 23 | serverInstance.init(); 24 | serverInstance.openConnection(); 25 | return serverInstance.getServerInstance(); 26 | } 27 | 28 | function terminateServer(done) { 29 | serverInstance.closeConnection(done); 30 | } 31 | 32 | // Exporting functions 33 | module.exports = { 34 | emptySchema, 35 | insertItem, 36 | initializeServer, 37 | terminateServer 38 | }; 39 | -------------------------------------------------------------------------------- /tunnel/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9.6-slim-buster 2 | 3 | WORKDIR /app 4 | 5 | RUN pip install requests 6 | 7 | COPY check_tunnel_status.py . 8 | 9 | ENTRYPOINT ["python", "check_tunnel_status.py",\ 10 | "--hosts", "http://host.docker.internal:11000/api/health-check",\ 11 | "http://host.docker.internal:14000/healthcheck/printer", \ 12 | "http://host.docker.internal:18000/healthcheck"] 13 | -------------------------------------------------------------------------------- /tunnel/README.md: -------------------------------------------------------------------------------- 1 | This Python file ensures the forwarded ports via SSH stay open for Core-v4 in production. 2 | The various hosts are "pinged" with HTTP GET requests on a given interval. 3 | 4 | Example usage: 5 | 6 | ``` 7 | python check_tunnel_status.py \ 8 | --request_interval_seconds 50 \ 9 | --hosts http://localhost:3000 http://localhost:5000 10 | ``` 11 | -------------------------------------------------------------------------------- /tunnel/check_tunnel_status.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from time import sleep, gmtime 3 | import requests 4 | import sys 5 | import argparse 6 | 7 | 8 | logging.Formatter.converter = gmtime 9 | logging.basicConfig( 10 | format="%(asctime)s.%(msecs)03dZ %(levelname)s:%(name)s:%(message)s", 11 | datefmt="%Y-%m-%dT%H:%M:%S", 12 | level=logging.INFO, 13 | ) 14 | 15 | parser = argparse.ArgumentParser() 16 | parser.add_argument( 17 | "--request_interval_seconds", 18 | type=int, 19 | default=5, 20 | help="Interval to query hosts through SSH tunnel" 21 | ) 22 | parser.add_argument( 23 | "--hosts", 24 | type=str, 25 | required=True, 26 | nargs="*", 27 | help="space separated values for hosts to send requests to" 28 | ) 29 | 30 | args = parser.parse_args() 31 | 32 | logging.info(f"Starting tunnel routine with hosts: {args.hosts}") 33 | def ping(host): 34 | try: 35 | req = requests.get(host) 36 | except Exception as e: 37 | logging.error(f"Could not reach {host}: {e}") 38 | 39 | 40 | while True: 41 | [ping(host) for host in args.hosts] 42 | sleep(args.request_interval_seconds) 43 | 44 | --------------------------------------------------------------------------------