├── .github └── workflows │ ├── auto-merge-dependabot.yml │ └── node.js.yml ├── .gitignore ├── .husky ├── pre-commit └── pre-push ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── backend-app ├── .deepsource.toml ├── .dockerignore ├── .env.example ├── .eslintrc.js ├── .gitignore ├── .mocharc.json ├── .npmrc ├── .prettierrc.json ├── Dockerfile ├── _config.yml ├── app.ts ├── config │ ├── app_config.ts │ └── logger_config.ts ├── constants │ ├── actions.ts │ └── meta_data.ts ├── controllers │ ├── auth_controllers │ │ ├── auth_controller.ts │ │ ├── github_controller.ts │ │ └── password_management.ts │ ├── base_controller.ts │ ├── calendar_controllers │ │ ├── calendar_base_controller.ts │ │ ├── calendar_validators.ts │ │ └── participents_controller.ts │ ├── tryingtsoa.ts │ └── users_controllers │ │ ├── admin_controller.ts │ │ └── user_controller.ts ├── docs │ ├── full-logo.jpg │ └── logo.png ├── interfaces │ ├── github_repo.ts │ ├── models │ │ ├── i_calendar.ts │ │ ├── i_event.ts │ │ ├── i_role.ts │ │ └── i_user.ts │ └── vendors.ts ├── middlewares │ ├── api_version_controll.ts │ ├── authorization.ts │ ├── global_error_handler.ts │ ├── morgan.ts │ └── rate_limit.ts ├── models │ ├── calendar │ │ ├── calendar_model.ts │ │ └── event_model.ts │ └── user │ │ ├── role_model.ts │ │ └── user_model.ts ├── nodemon.json ├── package-lock.json ├── package.json ├── routes │ ├── auth_routes.ts │ ├── calendar_routes.ts │ ├── github_routes.ts │ ├── index.ts │ ├── routes.ts │ └── users │ │ ├── admin_route.ts │ │ ├── super_admin_route.ts │ │ └── user_route.ts ├── seeds │ ├── index.ts │ ├── setup_seed.ts │ └── users.json ├── server.ts ├── target │ └── npmlist.json ├── tests │ ├── db_config.spec.ts │ ├── e2e │ │ └── auth │ │ │ └── auth.spec.ts │ ├── env.test.ts │ └── init │ │ └── x.spec.ts ├── tsconfig.json ├── tsoa.json ├── typings │ ├── express-serve-static-core.d.ts │ └── xss-clean.d.ts └── utils │ ├── api_features.ts │ ├── app_error.ts │ ├── authorization │ ├── auth_utils.ts │ ├── generate_tokens.ts │ ├── github.ts │ ├── roles │ │ ├── create_roles.ts │ │ └── role.ts │ └── validate_actions.ts │ ├── create_default_user.ts │ ├── logger.ts │ ├── register_paths.ts │ ├── searchCookie.ts │ └── swagger │ └── index.ts ├── docker-compose.yml └── frontend-app ├── .dockerignore ├── .env.local ├── .eslintrc.json ├── .gitignore ├── Dockerfile ├── README.md ├── mui-icons.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── logo.jpg ├── next.svg └── vercel.svg ├── src ├── app │ ├── calendar │ │ ├── loading.tsx │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── home │ │ └── page.tsx │ ├── layout.tsx │ ├── login │ │ └── page.tsx │ ├── page.tsx │ └── signup │ │ └── page.tsx ├── components │ ├── NavBar.tsx │ └── ui │ │ └── calendar.tsx ├── lib │ └── utils.ts └── middleware.ts ├── tailwind.config.js ├── tsconfig.json └── types.d.ts /.github/workflows/auto-merge-dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | pull-requests: write 6 | 7 | jobs: 8 | dependabot: 9 | runs-on: ubuntu-latest 10 | if: ${{ github.actor == 'dependabot[bot]' }} 11 | steps: 12 | - name: Dependabot metadata 13 | id: metadata 14 | uses: dependabot/fetch-metadata@v1 15 | with: 16 | github-token: "${{ secrets.GITHUB_TOKEN }}" 17 | - name: Enable auto-merge for Dependabot PRs 18 | run: gh pr merge --auto --merge "$PR_URL" 19 | env: 20 | GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 21 | pull-request-number: ${{ steps.metadata.outputs.pull-request-number }} 22 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js API CI 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | approved: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | mongodb-version: ['6.0'] 12 | env: 13 | NODE_ENV: production 14 | API_VERSION: v1 15 | MONGO_URI: ${{ secrets.MONGO_URI }} 16 | MONGO_URI_TEST: ${{ secrets.MONGO_URI_TEST }} 17 | PORT: ${{ secrets.PORT }} 18 | ADMIN_EMAIL: ${{ secrets.ADMIN_EMAIL }} 19 | ADMIN_PASSWORD: ${{ secrets.ADMIN_PASSWORD }} 20 | ACCESS_TOKEN_SECRET: ${{ secrets.ACCESS_TOKEN_SECRET }} 21 | ACCESS_TOKEN_EXPIRY_TIME: 1h 22 | REFRESH_TOKEN_SECRET: ${{ secrets.REFRESH_TOKEN_SECRET }} 23 | REFRESH_TOKEN_EXPIRY_TIME: 7d 24 | REQUIRE_ACTIVATION: true 25 | RATE_LIMIT_PER_HOUR: 1000 26 | steps: 27 | - name: Checkout code 28 | uses: actions/checkout@v2 29 | with: 30 | submodules: 'recursive' 31 | path: 'backend-app/' 32 | 33 | - name: Use Node.js 34 | uses: actions/setup-node@v2 35 | with: 36 | node-version: '14.x' 37 | 38 | - name: Set up environment variables 39 | run: | 40 | touch backend-app/.env.production 41 | echo "NODE_ENV=${NODE_ENV}" > backend-app/.env.production 42 | echo "API_VERSION=${API_VERSION}" >> backend-app/.env.production 43 | echo "MONGO_URI=${MONGO_URI}" >> backend-app/.env.production 44 | echo "MONGO_URI_TEST=${MONGO_URI_TEST}" >> backend-app/.env.production 45 | echo "PORT=${PORT}" >> backend-app/.env.production 46 | echo "ADMIN_EMAIL=${ADMIN_EMAIL}" >> backend-app/.env.production 47 | echo "ADMIN_PASSWORD=${ADMIN_PASSWORD}" >> backend-app/.env.production 48 | echo "ACCESS_TOKEN_SECRET=${ACCESS_TOKEN_SECRET}" >> backend-app/.env.production 49 | echo "ACCESS_TOKEN_EXPIRY_TIME=${ACCESS_TOKEN_EXPIRY_TIME}" >> backend-app/.env.production 50 | echo "REFRESH_TOKEN_SECRET=${REFRESH_TOKEN_SECRET}" >> backend-app/.env.production 51 | echo "REFRESH_TOKEN_EXPIRY_TIME=${REFRESH_TOKEN_EXPIRY_TIME}" >> backend-app/.env.production 52 | echo "REQUIRE_ACTIVATION=${REQUIRE_ACTIVATION}" >> backend-app/.env.production 53 | echo "RATE_LIMIT_PER_HOUR=${RATE_LIMIT_PER_HOUR}" >> backend-app/.env.production 54 | 55 | - name: Start MongoDB 56 | uses: supercharge/mongodb-github-action@1.10.0 57 | with: 58 | mongodb-version: ${{ matrix.mongodb-version }} 59 | 60 | - name: Install dependencies 61 | run: | 62 | cd backend-app/ 63 | npm install 64 | working-directory: backend-app/ 65 | 66 | # - name: Build 67 | # run: | 68 | # cd backend-app/ 69 | # npm run build 70 | # working-directory: backend-app/ 71 | 72 | - name: Run tests 73 | run: | 74 | cd backend-app/ 75 | npm run test 76 | working-directory: backend-app/ 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-ninja11/planemaker/2f778de23c977ddadf968d2905ee2706c121afd1/.gitignore -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | cd backend-app 5 | npx lint-staged 6 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | cd backend-app 5 | npm test 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Collaboration Guidelines 2 | 3 | ### [Submitting an Issue](https://opensource.guide/how-to-contribute/#opening-an-issue) 4 | 5 | Thank you for your interest in contributing to our project! To ensure effective collaboration, please follow these guidelines: 6 | 7 | Before you submit your issue search the [archive](https://github.com/ISIL-ESTE/Student-Workflow-Organizer/issues?utf8=%E2%9C%93&q=is%3Aissue), maybe your question was already answered or you may find some related content. 8 | 9 | 10 | - **Issue Overview**: Check if a similar issue exists. Provide a clear description of the problem or feature request. 11 | - **Reproduce the Error**: If reporting a bug, provide steps to reproduce it. 12 | - **Related Issues**: Mention any related issues or pull requests. 13 | - **Suggest a Fix**: If possible, propose a solution for the issue. 14 | 15 | ### [Submitting a Pull Request](https://opensource.guide/how-to-contribute/#opening-a-pull-request) 16 | 17 | Before you submit your pull request, consider the following guidelines: 18 | 19 | - Search [GitHub](https://github.com/ISIL-ESTE/Student-Workflow-Organizer/pulls?utf8=%E2%9C%93&q=is%3Apr) for an open or closed Pull Request 20 | that relates to your submission. 21 | - If you want to modify the project code structure, please start a discussion about it first 22 | - Make your changes in a new git branch 23 | 24 | ```shell 25 | git checkout -b my-fix-branch main 26 | ``` 27 | 28 | - Create your patch, **including appropriate test cases**, Note: if you aren't able to create tests, consider adding **need tests** label 29 | - Commit your changes using a descriptive commit message. 30 | 31 | - Push your branch to GitHub: 32 | 33 | ```shell 34 | git push origin my-fix-branch 35 | ``` 36 | 37 | - In GitHub, send a pull request to `ISIL-ESTE/Student-Workflow-Organizer:main`. 38 | - if your pr includes multiple tasks and you're not done yet, consider creating a draft pull request with a task list to allow other members to track the issue's progress 39 | - If we suggest changes, then 40 | - Make the required updates. 41 | - Make sure the tests are still passing 42 | - Rebase your branch and force push to your GitHub repository (this will update your Pull Request): 43 | 44 | ```shell 45 | git rebase main -i 46 | git push -f 47 | ``` 48 | That's it! Thank you for your contribution! 49 | 50 | #### Resolving merge conflicts ("This branch has conflicts that must be resolved") 51 | 52 | Sometimes your PR will have merge conflicts with the upstream repository's main branch. There are several ways to solve this but if not done correctly this can end up as a true nightmare. So here is one method that works quite well. 53 | 54 | - First, fetch the latest information from the main 55 | 56 | ```shell 57 | git fetch upstream 58 | ``` 59 | 60 | - Rebase your branch against the upstream/main 61 | 62 | ```shell 63 | git rebase upstream/main 64 | ``` 65 | 66 | - Git will stop rebasing at the first merge conflict and indicate which file is in conflict. Edit the file, resolve the conflict then 67 | 68 | ```shell 69 | git add 70 | git rebase --continue 71 | ``` 72 | - The rebase will continue up to the next conflict. Repeat the previous step until all files are merged and the rebase ends successfully. 73 | - Force push to your GitHub repository (this will update your Pull Request) 74 | 75 | ```shell 76 | git push -f 77 | ``` 78 | 79 | 80 | 81 | #### After your pull request is merged 82 | 83 | After your pull request is merged, you can safely delete your branch and pull the changes 84 | from the main (upstream) repository: 85 | 86 | - Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows: 87 | 88 | ```shell 89 | git push origin --delete my-fix-branch 90 | ``` 91 | 92 | - Check out the main branch: 93 | 94 | ```shell 95 | git checkout main -f 96 | ``` 97 | 98 | - Delete the local branch: 99 | 100 | ```shell 101 | git branch -D my-fix-branch 102 | ``` 103 | 104 | - Update your main with the latest upstream version: 105 | 106 | ```shell 107 | git pull --ff upstream main 108 | ``` 109 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 UCA AC ESTE ISIL 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 | # Student Organizer Website 2 | - ( contribute or at least leave a star ) 3 | 4 |
5 | 6 | [![Contributors][contributors-shield]][contributors-url] 7 | [![Forks][forks-shield]][forks-url] 8 | [![Stargazers][stars-shield]][stars-url] 9 | [![Issues][issues-shield]][issues-url] 10 | ![Open Pull Requests][open-pr-shield] ![Closed Pull Requests][closed-pr-shield] 11 | [![MIT License][license-shield]][license-url] 12 | 13 |
14 |

15 | 16 |

17 | 18 | A website designed to help academics and students organize their work, projects and exam in a timeline form. they can easily keep track of their upcoming assignments, exams, and projects and collaborate with others, along with managing their documents. 19 | 20 | ## Features 21 | 22 | - **Timeline:** View assignments, exams, and projects in chronological order. Drag and drop items to re-order them on the timeline and adjust due dates as needed. 23 | 24 | - **Reminders:** Receive reminders a few days before assignments or projects are due via email, text message, or push notification. 25 | 26 | - **Collaboration:** Invite other members of a group to view the timeline ( calendar ) and make changes to due dates or project details, This way, you can achieve the perfect time management. 27 | 28 | - **Resource Library:** Access study materials, reference books, and other resources organized by subject or topic. 29 | 30 | - **Progress Tracking:** Track progress on each assignment or project by marking them as complete or incomplete. 31 | 32 | - **Analytics:** View performance metrics such as average grades, completion rates, and time spent on assignments. 33 | 34 | - **Applying to Schools:** Keep track of application deadlines and required documents for each school. 35 | 36 | ## Sharing and Help 37 | 38 | - **Sharing Courses:** Share your courses with other students and access courses shared by others. 39 | 40 | - **Courses mind-map:** a unique feature in our website that allows you to link your courses, homeworks, summaries and many more so that you never get lost between your documents. 41 | 42 | ## How to Use 43 | 44 | 1. Create an account on the website. 45 | 46 | 2. Add your courses and assignments to the dashboard. 47 | 48 | 3. Use the timeline to keep track of upcoming assignments and projects. 49 | 50 | 4. Collaborate with others on a calendar timeline. 51 | 52 | 5. Use the resource library to access study materials and resources. 53 | 54 | 6. Track your progress on each assignment or project. 55 | 56 | ### Attention ⚠ 57 | 58 | This website is for educational purposes only. Do not submit other students' work as your own. 59 | 60 | ## License 61 | 62 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 63 | 64 | 65 | 66 | [contributors-shield]: https://img.shields.io/github/contributors/ISIL-ESTE/Student-Workflow-Organizer.svg?style=for-the-badge 67 | [contributors-url]: https://github.com/ISIL-ESTE/Student-Workflow-Organizer/graphs/contributors 68 | [forks-shield]: https://img.shields.io/github/forks/ISIL-ESTE/Student-Workflow-Organizer.svg?style=for-the-badge 69 | [forks-url]: https://github.com/ISIL-ESTE/Student-Workflow-Organizer/network/members 70 | [stars-shield]: https://img.shields.io/github/stars/ISIL-ESTE/Student-Workflow-Organizer.svg?style=for-the-badge 71 | [stars-url]: https://github.com/ISIL-ESTE/Student-Workflow-Organizer/stargazers 72 | [issues-shield]: https://img.shields.io/github/issues/ISIL-ESTE/Student-Workflow-Organizer.svg?style=for-the-badge 73 | [issues-url]: https://github.com/ISIL-ESTE/Student-Workflow-Organizer/issues 74 | [license-shield]: https://img.shields.io/github/license/ISIL-ESTE/Student-Workflow-Organizer.svg?style=for-the-badge 75 | [license-url]: https://github.com/ISIL-ESTE/Student-Workflow-Organizer/blob/master/LICENSE.txt 76 | [closed-pr-shield]: https://img.shields.io/github/issues-pr-closed/ISIL-ESTE/Student-Workflow-Organizer.svg?style=for-the-badge 77 | [closed-pr-url]: https://github.com/ISIL-ESTE/Student-Workflow-Organizer/pulls?q=is%3Apr+is%3Aclosed 78 | [open-pr-shield]: https://img.shields.io/github/issues-pr-raw/ISIL-ESTE/Student-Workflow-Organizer.svg?style=for-the-badge 79 | [open-pr-url]: https://github.com/ISIL-ESTE/Student-Workflow-Organizer/pulls 80 | -------------------------------------------------------------------------------- /backend-app/.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "shell" 5 | 6 | [[analyzers]] 7 | name = "javascript" -------------------------------------------------------------------------------- /backend-app/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .dockerignore 3 | .git 4 | docker-compose.yml 5 | -------------------------------------------------------------------------------- /backend-app/.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV = "development" 2 | API_VERSION = "1.0.0" 3 | MONGO_URI = "mongodb://localhost:27017/S-W-O" 4 | MONGO_URI_TEST = "mongodb://localhost:27017/S-W-O-TEST" 5 | PORT = 5000 6 | ADMIN_EMAIL = "admin@swf.com" 7 | ADMIN_PASSWORD = "password123418746" 8 | ACCESS_TOKEN_SECRET = "YourAccessTokenSecretKey" 9 | ACCESS_TOKEN_EXPIRY_TIME = "1d" 10 | REFRESH_TOKEN_SECRET = "YourRefreshTokenSecretKey" 11 | REFRESH_TOKEN_EXPIRY_TIME = "7d" 12 | REQUIRE_ACTIVATION = false 13 | RATE_LIMIT_PER_HOUR = 500 14 | OAUTH_CLIENT_ID_GITHUB = "Iv1.6f4b4b8b0b1b4b8b" 15 | OAUTH_CLIENT_SECRET_GITHUB = "6f4b4b8b0b1b4b8b6f4b4b8b0b1b4b8b" 16 | OAUTH_REDIRECT_URL_GITHUB = "http://localhost:3000/auth/github/callback" 17 | -------------------------------------------------------------------------------- /backend-app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['eslint:recommended', 'plugin:prettier/recommended'], 3 | plugins: ['prettier'], 4 | rules: { 5 | 'accessor-pairs': 'error', 6 | 'array-callback-return': 'error', 7 | 'block-scoped-var': 'error', 8 | complexity: ['error', 10], 9 | 'consistent-return': 'off', 10 | 'default-case': 'error', 11 | 'dot-location': ['error', 'property'], 12 | 'dot-notation': 'error', 13 | eqeqeq: 'error', 14 | 'guard-for-in': 'error', 15 | 'max-classes-per-file': ['error', 1], 16 | 'no-alert': 'error', 17 | 'no-caller': 'error', 18 | 'no-case-declarations': 'error', 19 | 'no-div-regex': 'error', 20 | 'no-else-return': 'error', 21 | 'no-empty-function': 'error', 22 | 'no-empty-pattern': 'error', 23 | 'no-eq-null': 'error', 24 | 'no-eval': 'error', 25 | 'no-extend-native': 'error', 26 | 'no-extra-bind': 'error', 27 | 'no-extra-label': 'error', 28 | 'no-fallthrough': 'error', 29 | 'no-floating-decimal': 'error', 30 | 'no-global-assign': 'error', 31 | 'no-native-reassign': 'error', 32 | 'no-implied-eval': 'error', 33 | 'no-invalid-this': 'warn', 34 | 'no-iterator': 'error', 35 | 'no-labels': 'error', 36 | 'no-lone-blocks': 'error', 37 | 'no-loop-func': 'error', 38 | 'no-multi-spaces': 'error', 39 | 'no-multi-str': 'error', 40 | 'no-new': 'error', 41 | 'no-new-func': 'error', 42 | 'no-new-wrappers': 'error', 43 | 'no-octal-escape': 'error', 44 | 'no-proto': 'error', 45 | 'no-redeclare': 'error', 46 | 'no-return-assign': 'error', 47 | 'no-return-await': 'error', 48 | 'no-script-url': 'error', 49 | 'no-self-assign': 'error', 50 | 'no-self-compare': 'error', 51 | 'no-sequences': 'error', 52 | 'no-throw-literal': 'error', 53 | 'no-unmodified-loop-condition': 'error', 54 | 'no-unused-expressions': 'warn', 55 | 'no-unused-labels': 'error', 56 | 'no-useless-call': 'error', 57 | 'no-useless-concat': 'error', 58 | 'no-useless-escape': 'error', 59 | 'no-useless-return': 'error', 60 | 'no-void': 'error', 61 | 'no-warning-comments': 'warn', 62 | 'no-with': 'error', 63 | 'prefer-promise-reject-errors': 'error', 64 | radix: 'error', 65 | 'require-await': 'error', 66 | 'vars-on-top': 'error', 67 | 'wrap-iife': ['error', 'inside'], 68 | yoda: 'error', 69 | 'no-console': 'warn', 70 | 'no-var': 'error', 71 | 'no-undef': 'off', 72 | 'no-unused-vars': 'warn', 73 | 'arrow-body-style': ['error', 'as-needed'], 74 | }, 75 | parser: '@typescript-eslint/parser', 76 | parserOptions: { 77 | ecmaVersion: 2021, 78 | sourceType: 'module', 79 | }, 80 | settings: { 81 | node: { 82 | allowModules: ['esm', 'js', 'commonjs'], 83 | }, 84 | }, 85 | env: { 86 | es6: true, 87 | node: true, 88 | }, 89 | }; 90 | -------------------------------------------------------------------------------- /backend-app/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | server-logs 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build 29 | 30 | # Dependency directories 31 | node_modules/ 32 | jspm_packages/ 33 | 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional eslint cache 39 | .eslintcache 40 | 41 | # Optional REPL history 42 | .node_repl_history 43 | 44 | # Output of 'npm pack' 45 | *.tgz 46 | 47 | # Yarn Integrity file 48 | .yarn-integrity 49 | 50 | # dotenv environment variables file 51 | .env 52 | 53 | # parcel-bundler cache (https://parceljs.org/) 54 | .cache 55 | 56 | # Next.js build output 57 | .next 58 | 59 | # Build output 60 | dist/ 61 | 62 | 63 | # Build output (Next.js) 64 | .next/ 65 | 66 | # Build output (Gatsby) 67 | public/ 68 | 69 | # Build output (create-react-native-app) 70 | ios/build/ 71 | 72 | # Build output (expo) 73 | .expo-shared/ 74 | 75 | create-node-api.sh 76 | 77 | 78 | 79 | docs/api_docs/* -------------------------------------------------------------------------------- /backend-app/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": ["ts"], 3 | "spec": ["tests/**/*.spec.ts", "tests/**/*.spec.js"], 4 | "require": "tsconfig-paths/register", 5 | "file": ["tests/env.test.ts"] 6 | } 7 | -------------------------------------------------------------------------------- /backend-app/.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /backend-app/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 80, 7 | "useTabs": false, 8 | "endOfLine": "auto" 9 | } 10 | -------------------------------------------------------------------------------- /backend-app/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Node.js runtime as the base image 2 | FROM node:18-alpine 3 | 4 | RUN npm install --location=global ts-node-dev 5 | 6 | # Set the working directory in the container 7 | WORKDIR /app 8 | 9 | # Copy the package.json and package-lock.json files 10 | COPY package*.json ./ 11 | 12 | # env dev 13 | RUN npm install 14 | 15 | # Install the dependencies (env prod) 16 | # RUN npm ci 17 | 18 | # Copy the rest of the application code 19 | COPY . . 20 | 21 | # env port 8000 22 | ENV PORT=8000 23 | 24 | # Expose the port your application will be listening on 25 | EXPOSE 8000 26 | 27 | # Start the Node.js application in dev mode 28 | CMD ["npm", "run", "dev"] 29 | 30 | # Start the Node.js application in prod mode 31 | # CMD ["npm", "start"] 32 | -------------------------------------------------------------------------------- /backend-app/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | -------------------------------------------------------------------------------- /backend-app/app.ts: -------------------------------------------------------------------------------- 1 | import globalErrHandler from './middlewares/global_error_handler'; 2 | import AppError from './utils/app_error'; 3 | import express, { Request, Response, NextFunction } from 'express'; 4 | import limiter from './middlewares/rate_limit'; 5 | import bearerToken from 'express-bearer-token'; 6 | import compression from 'compression'; 7 | import helmet from 'helmet'; 8 | import mongoSanitize from 'express-mongo-sanitize'; 9 | import xss from 'xss-clean'; 10 | import hpp from 'hpp'; 11 | import cors from 'cors'; 12 | import Morgan from './middlewares/morgan'; 13 | import swaggerDocs from './utils/swagger/index'; 14 | import handleAPIVersion from './middlewares/api_version_controll'; 15 | import { COOKIE_SECRET, CURRENT_ENV } from './config/app_config'; 16 | import cookieParser from 'cookie-parser'; 17 | import routesVersioning from 'express-routes-versioning'; 18 | import indexRouter from './routes/index'; 19 | import { RegisterRoutes } from './routes/routes'; 20 | 21 | const app = express(); 22 | 23 | // use json as default format 24 | app.use(express.json()); 25 | //configure cookie parser 26 | app.use(cookieParser(COOKIE_SECRET)); 27 | 28 | // use morgan for logging 29 | app.use(Morgan); 30 | 31 | // Allow Cross-Origin requests 32 | app.use(cors()); 33 | 34 | // Set security HTTP headers 35 | app.use(helmet()); 36 | 37 | // Limit request from the same API 38 | 39 | // Body parser, reading data from body into req.body 40 | app.use( 41 | express.json({ 42 | limit: '15kb', 43 | }) 44 | ); 45 | 46 | // Data sanitization against Nosql query injection 47 | app.use( 48 | mongoSanitize({ 49 | replaceWith: '_', 50 | }) 51 | ); 52 | 53 | // Data sanitization against XSS(clean user input from malicious HTML code) 54 | app.use(xss()); 55 | 56 | // Prevent parameter pollution 57 | app.use(hpp()); 58 | 59 | // Compress all responses 60 | app.use(compression()); 61 | 62 | if (CURRENT_ENV === 'production') { 63 | //Limiting request form same IP 64 | app.use(limiter); 65 | } 66 | 67 | // if no version is specified, use the default version 68 | app.use(handleAPIVersion); 69 | 70 | // handle bearer token 71 | app.use(bearerToken()); 72 | 73 | app.get('/', (_req: Request, res: Response) => { 74 | res.status(200).json({ 75 | message: 'Welcome to the backend app', 76 | env: CURRENT_ENV, 77 | }); 78 | }); 79 | 80 | // routes 81 | app.use( 82 | `/api`, 83 | routesVersioning()({ 84 | '1.0.0': indexRouter, 85 | }) 86 | ); 87 | 88 | // register routes 89 | RegisterRoutes(app); 90 | 91 | // configure swagger docs 92 | swaggerDocs(app); 93 | 94 | // handle undefined Routes 95 | app.use('*', (req: Request, _res: Response, next: NextFunction) => { 96 | const err = new AppError(404, 'Route Not Found', req.originalUrl); 97 | next(err); 98 | }); 99 | 100 | app.use(globalErrHandler); 101 | 102 | export default app; 103 | -------------------------------------------------------------------------------- /backend-app/config/app_config.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import dotenv from 'dotenv'; 3 | import fs from 'fs'; 4 | 5 | // load env file 6 | const envMode = process.env.NODE_ENV?.toLowerCase(); 7 | const envFile = 8 | envMode === 'production' 9 | ? '.env.production' 10 | : fs.existsSync('.env') 11 | ? '.env' 12 | : '.env.example'; 13 | 14 | if (envFile) { 15 | dotenv.config({ path: join(__dirname, `../${envFile}`) }); 16 | } 17 | 18 | // parse boolean values 19 | const parseBoolean = (value: string): boolean => value === 'true'; 20 | 21 | export const logFilePath = join(__dirname, '../server-logs'); 22 | export const CURRENT_ENV = process.env.NODE_ENV?.toLowerCase(); 23 | export const API_VERSION = process.env.npm_package_version; 24 | export const DATABASE = process.env.MONGO_URI; 25 | export const MONGO_URI_TEST = process.env.MONGO_URI_TEST; 26 | export const PORT = process.env.PORT; 27 | export const ADMIN_EMAIL = process.env.ADMIN_EMAIL; 28 | export const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD; 29 | export const REQUIRE_ACTIVATION = parseBoolean( 30 | process.env.REQUIRE_ACTIVATION as string 31 | ); 32 | export const RATE_LIMIT_PER_HOUR = process.env 33 | .RATE_LIMIT_PER_HOUR as unknown as number; 34 | export const OAUTH_CLIENT_ID_GITHUB = process.env.OAUTH_CLIENT_ID_GITHUB; 35 | export const OAUTH_CLIENT_SECRET_GITHUB = 36 | process.env.OAUTH_CLIENT_SECRET_GITHUB; 37 | export const OAUTH_REDIRECT_URL_GITHUB = process.env.OAUTH_REDIRECT_URL_GITHUB; 38 | export const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET; 39 | export const ACCESS_TOKEN_EXPIRY_TIME = process.env.ACCESS_TOKEN_EXPIRY_TIME; 40 | export const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET; 41 | export const REFRESH_TOKEN_EXPIRY_TIME = process.env.REFRESH_TOKEN_EXPIRY_TIME; 42 | export const ACCESS_TOKEN_COOKIE_EXPIRY_TIME = 43 | process.env.ACCESS_TOKEN_COOKIE_EXPIRY_TIME; 44 | export const REFRESH_TOKEN_COOKIE_EXPIRY_TIME = 45 | process.env.REFRESH_TOKEN_COOKIE_EXPIRY_TIME; 46 | export const COOKIE_SECRET = process.env.COOKIE_SECRET; 47 | -------------------------------------------------------------------------------- /backend-app/config/logger_config.ts: -------------------------------------------------------------------------------- 1 | import { addColors, format } from 'winston'; 2 | import { logFilePath } from './app_config'; 3 | // Define the current environment 4 | import { CURRENT_ENV } from './app_config'; 5 | 6 | // Define log colors 7 | const colors = { 8 | error: 'red', 9 | warn: 'yellow', 10 | info: 'cyan', 11 | http: 'magenta', 12 | debug: 'green', 13 | }; 14 | addColors(colors); 15 | 16 | /** 17 | * @description - This is the format for the log message 18 | * @param {string} info - The log message 19 | */ 20 | const formatLogMessage = format.printf( 21 | (info) => 22 | `[${info.level}] \x1b[1.4m${info.timestamp}\x1b[0m - ${info.message}` 23 | ); 24 | 25 | /** 26 | * if the current environment is development, then log level is debug 27 | * else log level is warn. 28 | * when the log level is debug, debug and all the levels above it will be logged. 29 | * when the log level is warn, warn and all the levels above it will be logged. 30 | */ 31 | const logLevel = CURRENT_ENV === 'development' ? 'debug' : 'warn'; 32 | 33 | /** 34 | * @description - This is the configuration for the logger 35 | * @param {string} level - The level of the log 36 | * @param {string} format - The format of the log 37 | * @param {string} timestamp - The timestamp of the log 38 | * @param {string} formatLogMessage - The format of the log message 39 | */ 40 | const consoleOptions = { 41 | level: logLevel, 42 | format: format.combine( 43 | format.timestamp({ 44 | format: 'HH:mm:ss MM-DD-YYYY', 45 | }), 46 | format((info) => { 47 | info.level = info.level.toUpperCase(); 48 | return info; 49 | })(), 50 | format.colorize({ all: true }), 51 | format.timestamp(), 52 | formatLogMessage 53 | ), 54 | }; 55 | 56 | const fileOptions = { 57 | level: logLevel, 58 | dirname: logFilePath, 59 | filename: '%DATE%.log', 60 | datePattern: 'YYYY-MM-DD', 61 | zippedArchive: true, 62 | handleExceptions: true, 63 | json: true, 64 | maxSize: '20m', 65 | maxFiles: '15d', 66 | }; 67 | 68 | export { fileOptions, consoleOptions }; 69 | -------------------------------------------------------------------------------- /backend-app/constants/actions.ts: -------------------------------------------------------------------------------- 1 | const Actions = { 2 | DELETE_USER: 'DELETE_USER', 3 | BAN_USER: 'BAN_USER', 4 | UPDATE_USER: 'UPDATE_USER', 5 | UPDATE_CALANDER: 'UPDATE_CALANDER', 6 | REMOVE_SUPER_ADMIN: 'REMOVE_SUPER_ADMIN', 7 | MANAGE_ROLES: 'MANAGE_ROLES', 8 | }; 9 | 10 | export default Actions; 11 | -------------------------------------------------------------------------------- /backend-app/constants/meta_data.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Schema} model 3 | * @returns {void} 4 | * @description Add common fields to a model 5 | * 6 | **/ 7 | const apply = (model: any): void => { 8 | model.add({ 9 | deleted: { 10 | type: Boolean, 11 | default: false, 12 | }, 13 | UpdatedBy: { 14 | type: String, 15 | }, 16 | createdBy: { 17 | type: String, 18 | default: 'System', 19 | }, 20 | deletedBy: { 21 | type: String, 22 | }, 23 | deletedAt: { 24 | type: Date, 25 | }, 26 | }); 27 | }; 28 | 29 | export default { 30 | apply, 31 | }; 32 | -------------------------------------------------------------------------------- /backend-app/controllers/auth_controllers/auth_controller.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { IReq, IRes, INext } from '@interfaces/vendors'; 3 | import { promisify } from 'util'; 4 | import AppError from '@utils/app_error'; 5 | import Role from '@utils/authorization/roles/role'; 6 | import { REQUIRE_ACTIVATION } from '@config/app_config'; 7 | import { 8 | getGithubOAuthUser, 9 | getGithubOAuthToken, 10 | getGithubOAuthUserPrimaryEmail, 11 | } from '@utils/authorization/github'; 12 | import AuthUtils from '@utils/authorization/auth_utils'; 13 | import searchCookies from '@utils/searchCookie'; 14 | import User from '@models/user/user_model'; 15 | import { Request, Response, NextFunction } from 'express'; 16 | import { IUser } from '@root/interfaces/models/i_user'; 17 | 18 | const generateActivationKey = async () => { 19 | const randomBytesPromiseified = promisify(require('crypto').randomBytes); 20 | const activationKey = (await randomBytesPromiseified(32)).toString('hex'); 21 | return activationKey; 22 | }; 23 | 24 | export const githubHandler = async ( 25 | req: Request, 26 | res: Response, 27 | next: NextFunction 28 | ) => { 29 | try { 30 | const Roles = await Role.getRoles(); 31 | // check if user role exists 32 | if (!Roles.USER) 33 | throw new AppError( 34 | 500, 35 | 'User role does not exist. Please contact the admin.' 36 | ); 37 | const { code } = req.query as { 38 | code: string; 39 | }; 40 | 41 | if (!code) throw new AppError(400, 'Please provide code'); 42 | const { access_token } = await getGithubOAuthToken(code); 43 | if (!access_token) throw new AppError(400, 'Invalid code'); 44 | const githubUser = await getGithubOAuthUser(access_token); 45 | const primaryEmail = await getGithubOAuthUserPrimaryEmail(access_token); 46 | const exists = await User.findOne({ email: primaryEmail }); 47 | if (exists) { 48 | const accessToken = AuthUtils.generateAccessToken( 49 | exists._id.toString() 50 | ); 51 | const refreshToken = AuthUtils.generateRefreshToken( 52 | exists._id.toString() 53 | ); 54 | AuthUtils.setAccessTokenCookie(res, accessToken); 55 | AuthUtils.setRefreshTokenCookie(res, refreshToken); 56 | return res.sendStatus(204); 57 | } 58 | if (!githubUser) throw new AppError(400, 'Invalid access token'); 59 | const createdUser = await User.create({ 60 | name: githubUser.name, 61 | email: primaryEmail, 62 | password: null, 63 | address: githubUser.location ? githubUser.location : null, 64 | roles: [Roles.USER.name], 65 | authorities: Roles.USER.authorities, 66 | restrictions: Roles.USER.restrictions, 67 | githubOauthAccessToken: access_token, 68 | active: true, 69 | }); 70 | 71 | const accessToken = AuthUtils.generateAccessToken( 72 | createdUser._id.toString() 73 | ); 74 | const refreshToken = AuthUtils.generateRefreshToken( 75 | createdUser._id.toString() 76 | ); 77 | AuthUtils.setAccessTokenCookie(res, accessToken); 78 | AuthUtils.setRefreshTokenCookie(res, refreshToken); 79 | res.status(201).json(createdUser); 80 | } catch (err) { 81 | next(err); 82 | } 83 | }; 84 | 85 | export const login = async ( 86 | req: Request, 87 | res: Response, 88 | next: NextFunction 89 | ) => { 90 | try { 91 | const { email, password } = req.body as { 92 | email: string; 93 | password: string; 94 | }; 95 | 96 | // 1) check if password exist 97 | if (!password) { 98 | throw new AppError(400, 'Please provide a password'); 99 | } 100 | // to type safty on password 101 | if (typeof password !== 'string') { 102 | throw new AppError(400, 'Invalid password format'); 103 | } 104 | 105 | // 2) check if user exist and password is correct 106 | const user = await User.findOne({ 107 | email, 108 | }).select('+password'); 109 | 110 | // check if password exist and it is a string 111 | if (!user?.password || typeof user.password !== 'string') 112 | throw new AppError(400, 'Invalid email or password'); 113 | 114 | // Check if the account is banned 115 | if (user && user?.accessRestricted) 116 | throw new AppError( 117 | 403, 118 | 'Your account has been banned. Please contact the admin for more information.' 119 | ); 120 | 121 | if (!user || !(await user.correctPassword(password, user.password))) { 122 | throw new AppError(401, 'Email or Password is wrong'); 123 | } 124 | 125 | // 3) All correct, send accessToken & refreshToken to client via cookie 126 | const accessToken = AuthUtils.generateAccessToken(user._id.toString()); 127 | const refreshToken = AuthUtils.generateRefreshToken( 128 | user._id.toString() 129 | ); 130 | AuthUtils.setAccessTokenCookie(res, accessToken); 131 | AuthUtils.setRefreshTokenCookie(res, refreshToken); 132 | 133 | // Remove the password from the output 134 | user.password = undefined; 135 | 136 | res.status(200).json({ 137 | accessToken, 138 | user, 139 | }); 140 | } catch (err) { 141 | next(err); 142 | } 143 | }; 144 | 145 | export const signup = async ( 146 | req: Request, 147 | res: Response, 148 | next: NextFunction 149 | ) => { 150 | try { 151 | const activationKey = await generateActivationKey(); 152 | const Roles = await Role.getRoles(); 153 | 154 | // check if user role exists 155 | if (!Roles.USER) 156 | throw new AppError( 157 | 500, 158 | 'User role does not exist. Please contact the admin.' 159 | ); 160 | 161 | // check if password is provided 162 | if (!req.body.password) 163 | throw new AppError(400, 'Please provide a password'); 164 | 165 | const userpayload = { 166 | name: req.body.name, 167 | email: req.body.email, 168 | password: req.body.password, 169 | roles: [Roles.USER.name], 170 | authorities: Roles.USER.authorities, 171 | active: !REQUIRE_ACTIVATION, 172 | restrictions: Roles.USER.restrictions, 173 | ...(REQUIRE_ACTIVATION && { activationKey }), 174 | }; 175 | const user = await User.create(userpayload); 176 | const accessToken = AuthUtils.generateAccessToken(user._id.toString()); 177 | const refreshToken = AuthUtils.generateRefreshToken( 178 | user._id.toString() 179 | ); 180 | AuthUtils.setAccessTokenCookie(res, accessToken); 181 | AuthUtils.setRefreshTokenCookie(res, refreshToken); 182 | // Remove the password and activation key from the output 183 | user.password = undefined; 184 | user.activationKey = undefined; 185 | 186 | res.status(201).json({ 187 | accessToken, 188 | user, 189 | }); 190 | } catch (err) { 191 | next(err); 192 | } 193 | }; 194 | 195 | export const tokenRefresh = async ( 196 | req: Request, 197 | res: Response, 198 | next: NextFunction 199 | ) => { 200 | try { 201 | // get the refresh token from httpOnly cookie 202 | const refreshToken = searchCookies(req, 'refresh_token'); 203 | if (!refreshToken) 204 | throw new AppError(400, 'You have to login to continue.'); 205 | const refreshTokenPayload = 206 | await AuthUtils.verifyRefreshToken(refreshToken); 207 | if (!refreshTokenPayload || !refreshTokenPayload._id) 208 | throw new AppError(400, 'Invalid refresh token'); 209 | const user = await User.findById(refreshTokenPayload._id); 210 | if (!user) throw new AppError(400, 'Invalid refresh token'); 211 | const accessToken = AuthUtils.generateAccessToken(user._id.toString()); 212 | //set or override accessToken cookie. 213 | AuthUtils.setAccessTokenCookie(res, accessToken); 214 | res.sendStatus(204); 215 | } catch (err) { 216 | next(err); 217 | } 218 | }; 219 | export const logout = async ( 220 | req: Request, 221 | res: Response, 222 | next: NextFunction 223 | ) => { 224 | try { 225 | const accessToken = searchCookies(req, 'access_token'); 226 | if (!accessToken) 227 | throw new AppError(400, 'Please provide access token'); 228 | const accessTokenPayload = 229 | await AuthUtils.verifyAccessToken(accessToken); 230 | if (!accessTokenPayload || !accessTokenPayload._id) 231 | throw new AppError(400, 'Invalid access token'); 232 | res.sendStatus(204); 233 | } catch (err) { 234 | next(err); 235 | } 236 | }; 237 | 238 | interface ActivationParams { 239 | id: string; 240 | activationKey: string; 241 | } 242 | 243 | export const activateAccount = async ( 244 | req: Request, 245 | res: Response, 246 | next: NextFunction 247 | ) => { 248 | try { 249 | const { id, activationKey } = req.query as unknown as ActivationParams; 250 | 251 | if (!activationKey) { 252 | throw new AppError(400, 'Please provide activation key'); 253 | } 254 | if (!id) { 255 | throw new AppError(400, 'Please provide user id'); 256 | } 257 | 258 | // check if a valid id 259 | if (!mongoose.Types.ObjectId.isValid(id)) { 260 | throw new AppError(400, 'Please provide a valid user id'); 261 | } 262 | 263 | const user = await User.findOne({ 264 | _id: id, 265 | }).select('+activationKey'); 266 | 267 | if (!user) { 268 | throw new AppError(404, 'User does not exist'); 269 | } 270 | if (user.active) { 271 | throw new AppError(409, 'User is already active'); 272 | } 273 | 274 | // verify activation key 275 | if (activationKey !== user.activationKey) { 276 | throw new AppError(400, 'Invalid activation key'); 277 | } 278 | // activate user 279 | user.active = true; 280 | user.activationKey = undefined; 281 | await user.save(); 282 | // Remove the password from the output 283 | user.password = undefined; 284 | 285 | res.status(200).json({ 286 | user, 287 | }); 288 | } catch (err) { 289 | next(err); 290 | } 291 | }; 292 | 293 | export const protect = async ( 294 | req: Request, 295 | res: Response, 296 | next: NextFunction 297 | ) => { 298 | try { 299 | const accessToken = searchCookies(req, 'access_token'); 300 | 301 | if (!accessToken) throw new AppError(401, 'Please login to continue'); 302 | 303 | const accessTokenPayload = 304 | await AuthUtils.verifyAccessToken(accessToken); 305 | 306 | if (!accessTokenPayload || !accessTokenPayload._id) 307 | throw new AppError(401, 'Invalid access token'); 308 | // 3) check if the user is exist (not deleted) 309 | const user: IUser = await User.findById(accessTokenPayload._id).select( 310 | 'accessRestricted active roles authorities restrictions name email' 311 | ); 312 | if (!user) { 313 | throw new AppError(401, 'This user is no longer exist'); 314 | } 315 | 316 | // Check if the account is banned 317 | if (user?.accessRestricted) 318 | throw new AppError( 319 | 403, 320 | 'Your account has been banned. Please contact the admin for more information.' 321 | ); 322 | 323 | // check if account is active 324 | if (!user.active) 325 | throw new AppError( 326 | 403, 327 | 'Your account is not active. Please activate your account to continue.' 328 | ); 329 | 330 | // Create a new request object with the user property set to the user object 331 | 332 | req.user = user; 333 | next(); 334 | } catch (err) { 335 | // check if the token is expired 336 | if (err.name === 'TokenExpiredError') { 337 | return next(new AppError(401, 'Your token is expired')); 338 | } 339 | if (err.name === 'JsonWebTokenError') { 340 | return next(new AppError(401, err.message)); 341 | } 342 | next(err); 343 | } 344 | }; 345 | 346 | // Authorization check if the user have rights to do this action 347 | export const restrictTo = 348 | (...roles: string[]) => 349 | (req: IReq, res: IRes, next: INext) => { 350 | try { 351 | const roleExist = roles.some((role) => 352 | req.user.roles.includes(role) 353 | ); 354 | if (!roleExist) 355 | throw new AppError( 356 | 403, 357 | 'You are not allowed to do this action' 358 | ); 359 | next(); 360 | } catch (err) { 361 | next(err); 362 | } 363 | }; 364 | -------------------------------------------------------------------------------- /backend-app/controllers/auth_controllers/github_controller.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import Repository from '@interfaces/github_repo'; 3 | import AppError from '@utils/app_error'; 4 | import { INext, IReq, IRes } from '@interfaces/vendors'; 5 | 6 | export const getRecentRepo = async (req: IReq, res: IRes, next: INext) => { 7 | try { 8 | if (!req.user) { 9 | throw new AppError(401, 'You are not logged in'); 10 | } 11 | const { githubOauthAccessToken } = req.user; 12 | const userRepositories = await axios.get( 13 | 'https://api.github.com/user/repos', 14 | { 15 | headers: { 16 | Authorization: `Bearer ${githubOauthAccessToken}`, 17 | }, 18 | } 19 | ); 20 | const mappedUserRepositories = userRepositories.data.map( 21 | (repository: any): Repository => ({ 22 | id: repository.id, 23 | name: repository.name, 24 | full_name: repository.full_name, 25 | description: repository.description, 26 | isFork: repository.fork, 27 | language: repository.language, 28 | license: repository.license?.name 29 | ? repository.license.name 30 | : null, 31 | openedIssuesCount: repository.open_issues_count, 32 | repoCreatedAt: repository.created_at, 33 | url: repository.url, 34 | }) 35 | ); 36 | if (mappedUserRepositories.length <= 0) { 37 | throw new AppError(400, 'No repositories found'); 38 | } 39 | 40 | const sortedRepository = mappedUserRepositories.sort( 41 | (a: Repository, b: Repository) => 42 | new Date(b.repoCreatedAt).getTime() - 43 | new Date(a.repoCreatedAt).getTime() 44 | ); 45 | 46 | const recentRepository = sortedRepository[0]; 47 | res.status(200).json({ 48 | recentRepository, 49 | }); 50 | } catch (err) { 51 | next(err); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /backend-app/controllers/auth_controllers/password_management.ts: -------------------------------------------------------------------------------- 1 | import User from '@models/user/user_model'; 2 | import logger from '@utils/logger'; 3 | import AppError from '@utils/app_error'; 4 | import generateTokens from '@utils/authorization/generate_tokens'; 5 | import validator from 'validator'; 6 | import { NextFunction, Request, Response } from 'express'; 7 | 8 | export const updatePassword = async ( 9 | req: Request, 10 | res: Response, 11 | next: NextFunction 12 | ) => { 13 | try { 14 | const { email, resetKey, password } = req.body; 15 | 16 | if (!validator.isEmail(email)) 17 | throw new AppError(400, 'Invalid email format'); 18 | 19 | const user = await User.findOne({ email }).select('+password'); 20 | 21 | if (!user) 22 | throw new AppError(404, 'User with this email does not exist'); 23 | 24 | if (!resetKey) throw new AppError(400, 'Please provide reset key'); 25 | 26 | if (!user.resetKey) throw new AppError(400, 'Invalid reset key'); 27 | 28 | if (resetKey !== user.resetKey) 29 | throw new AppError(400, 'Invalid reset key'); 30 | 31 | user.password = password; 32 | user.resetKey = undefined; 33 | await user.save(); 34 | 35 | const token = generateTokens(user.id); 36 | user.password = undefined; 37 | 38 | res.status(200).json({ 39 | token, 40 | user, 41 | }); 42 | } catch (err) { 43 | next(err); 44 | } 45 | }; 46 | 47 | export const forgotPassword = async ( 48 | req: Request, 49 | res: Response, 50 | next: NextFunction 51 | ) => { 52 | try { 53 | const { email } = req.body; 54 | 55 | if (!email) throw new AppError(400, 'Please provide email'); 56 | 57 | if (!validator.isEmail(email)) 58 | throw new AppError(400, 'Invalid email format'); 59 | 60 | const user = await User.findOne({ email }); 61 | 62 | if (!user) 63 | throw new AppError(404, 'User with this email does not exist'); 64 | 65 | const resetKey = user.generateResetKey(); 66 | await user.save(); 67 | 68 | logger.info( 69 | `User ${user.name} with email ${user.email} has requested for password reset with reset key ${resetKey}` 70 | ); 71 | 72 | // send email with reset key 73 | // eslint-disable-next-line no-warning-comments 74 | // TODO: send email with reset key 75 | 76 | res.status(200).json({ 77 | message: 'Email with reset key sent successfully', 78 | }); 79 | } catch (err) { 80 | next(err); 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /backend-app/controllers/base_controller.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, RequestHandler } from 'express'; 2 | import { Model } from 'mongoose'; 3 | import AppError from '@utils/app_error'; 4 | import APIFeatures from '@utils/api_features'; 5 | import { IReq, IRes } from '@interfaces/vendors'; 6 | 7 | /** 8 | * Delete a document by ID (soft delete) 9 | * @param {Model} Model - The mongoose model 10 | * @returns {Function} - Express middleware function 11 | */ 12 | export const deleteOne = 13 | (Model: Model): RequestHandler => 14 | async (req: IReq, res: IRes, next: NextFunction): Promise => { 15 | try { 16 | const doc = await Model.findByIdAndUpdate( 17 | req.params.id, 18 | { 19 | deleted: true, 20 | ...(req.user && { deletedBy: req.user?._id }), 21 | deletedAt: Date.now(), 22 | }, 23 | { new: true } 24 | ); 25 | 26 | if (!doc) throw new AppError(404, 'No document found with that id'); 27 | 28 | res.status(204).json({ 29 | data: null, 30 | }); 31 | } catch (error) { 32 | next(error); 33 | } 34 | }; 35 | 36 | /** 37 | * Update a document by ID 38 | * @param {Model} Model - The mongoose model 39 | * @returns {Function} - Express middleware function 40 | */ 41 | export const updateOne = 42 | (Model: Model): RequestHandler => 43 | async (req: IReq, res: IRes, next: NextFunction) => { 44 | try { 45 | // get the user who is updating the document 46 | const userid = req.user?._id; 47 | req.body.updatedBy = userid; 48 | const payload = new Model(req.body); 49 | const doc = await Model.findByIdAndUpdate(req.params.id, payload, { 50 | new: true, 51 | runValidators: true, 52 | }); 53 | 54 | if (!doc) throw new AppError(404, 'No document found with that id'); 55 | 56 | res.status(200).json({ 57 | doc, 58 | }); 59 | } catch (error) { 60 | next(error); 61 | } 62 | }; 63 | 64 | /** 65 | * Create a new document 66 | * @param {Model} Model - The mongoose model 67 | * @returns {Function} - Express middleware function 68 | */ 69 | export const createOne = 70 | (Model: Model): RequestHandler => 71 | async (req: IReq, res: IRes, next: NextFunction) => { 72 | try { 73 | // get the user who is creating the document 74 | if (req.user === undefined) 75 | throw new AppError( 76 | 401, 77 | 'You are not authorized to perform this action' 78 | ); 79 | const userid = req.user._id; 80 | req.body.createdBy = userid; 81 | 82 | const doc = await Model.create(req.body); 83 | 84 | res.status(201).json({ 85 | doc, 86 | }); 87 | } catch (error) { 88 | next(error); 89 | } 90 | }; 91 | /** 92 | * Get a document by ID 93 | * @param {Model} Model - The mongoose model 94 | * @returns {Function} - Express middleware function 95 | */ 96 | export const getOne = 97 | (Model: Model): RequestHandler => 98 | async (req: IReq, res: IRes, next: NextFunction) => { 99 | try { 100 | const doc = await Model.findById(req.params.id); 101 | 102 | if (!doc) throw new AppError(404, 'No document found with that id'); 103 | 104 | res.status(200).json({ 105 | doc, 106 | }); 107 | } catch (error) { 108 | next(error); 109 | } 110 | }; 111 | 112 | /** 113 | * Get all documents 114 | * @param {Model} Model - The mongoose model 115 | * @returns {Function} - Express middleware function 116 | */ 117 | export const getAll = 118 | (Model: Model): RequestHandler => 119 | async (req: IReq, res: IRes, next: NextFunction) => { 120 | try { 121 | const features = new APIFeatures( 122 | Model.find(), 123 | req.query as Record 124 | ) 125 | .sort() 126 | .paginate(); 127 | 128 | const doc = await features.query; 129 | 130 | res.status(200).json({ 131 | results: doc.length, 132 | data: doc, 133 | }); 134 | } catch (error) { 135 | next(error); 136 | } 137 | }; 138 | -------------------------------------------------------------------------------- /backend-app/controllers/calendar_controllers/calendar_base_controller.ts: -------------------------------------------------------------------------------- 1 | import { IReq, IRes, INext } from '@interfaces/vendors'; 2 | import AppError from '@utils/app_error'; 3 | import * as calendar_validators from './calendar_validators'; 4 | 5 | export const updateCalendar = async ( 6 | req: IReq, 7 | _res: IRes, 8 | next: INext 9 | ): Promise => { 10 | try { 11 | const calendar = await calendar_validators.validateCalendar(req); 12 | // check if user is not admin nor the owner of the calendar 13 | if ( 14 | calendar.createdBy !== req.user._id.toString() || 15 | !req.user.roles.includes('ADMIN') || 16 | !req.user.roles.includes('SUPER_ADMIN') 17 | ) { 18 | throw new AppError( 19 | 403, 20 | 'You are not allowed to update this calendar' 21 | ); 22 | } 23 | // TODO: update calendar 24 | } catch (err) { 25 | next(err); 26 | } 27 | }; 28 | 29 | export const deleteCalendar = async ( 30 | req: IReq, 31 | _res: IRes, 32 | next: INext 33 | ): Promise => { 34 | try { 35 | const calendar = await calendar_validators.validateCalendar(req); 36 | // check if user is not admin nor the owner of the calendar 37 | if ( 38 | calendar.createdBy !== req.user._id.toString() || 39 | !req.user.roles.includes('SUPER_ADMIN') 40 | ) { 41 | throw new AppError( 42 | 403, 43 | 'You are not allowed to delete this calendar' 44 | ); 45 | } 46 | // TODO: delete calendar 47 | } catch (err) { 48 | next(err); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /backend-app/controllers/calendar_controllers/calendar_validators.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import AppError from '@utils/app_error'; 3 | import Calendar from '@models/calendar/calendar_model'; 4 | 5 | export async function validateCalendar(req: Request) { 6 | const calendarid = req.params.calendarid; 7 | if (!calendarid) { 8 | throw new AppError(400, 'Calendar id is required'); 9 | } 10 | const calendar = await Calendar.findById(calendarid); 11 | if (!calendar) { 12 | throw new AppError(404, 'Calendar not found with that id'); 13 | } 14 | return calendar; 15 | } 16 | -------------------------------------------------------------------------------- /backend-app/controllers/calendar_controllers/participents_controller.ts: -------------------------------------------------------------------------------- 1 | import { INext, IReq, IRes } from '@interfaces/vendors'; 2 | import AppError from '@utils/app_error'; 3 | import validator from 'validator'; 4 | import * as calendar_validators from './calendar_validators'; 5 | import { ObjectId } from 'mongoose'; 6 | 7 | export const inviteUsersByEmail = async (req: IReq, res: IRes, next: INext) => { 8 | try { 9 | // check if calendar exists 10 | const calendar = await calendar_validators.validateCalendar(req); 11 | // check if user is the calendar owner 12 | if (calendar.createdBy.toString() !== req.user._id.toString()) { 13 | // check if user is a participant and the calendar is shareable 14 | if (!calendar.participants.includes(req.user._id)) 15 | throw new AppError( 16 | 403, 17 | 'You do not have permission to invite users to this calendar' 18 | ); 19 | if (!calendar.isShareAble) 20 | throw new AppError(403, 'This calendar is not shareable'); 21 | } 22 | // get emails from request body 23 | const emails = req.body.emails; 24 | if (!emails) { 25 | throw new AppError(400, 'Emails are required'); 26 | } 27 | // check if emails are valid 28 | const validEmails: string[] = []; 29 | const invalidEmails: string[] = []; 30 | emails.forEach((email: string) => { 31 | if (validator.isEmail(email)) { 32 | validEmails.push(email); 33 | } else { 34 | invalidEmails.push(email); 35 | } 36 | }); 37 | // TODO: send emails to valid emails 38 | res.status(200).json({ 39 | validEmails, 40 | invalidEmails, 41 | }); 42 | } catch (err) { 43 | next(err); 44 | } 45 | }; 46 | 47 | export const removeCalendarParticipants = async ( 48 | req: IReq, 49 | res: IRes, 50 | next: INext 51 | ) => { 52 | try { 53 | const calendar = await calendar_validators.validateCalendar(req); 54 | // check if user is the calendar owner 55 | if (calendar.createdBy.toString() !== req.user._id.toString()) { 56 | throw new AppError(403, 'You are not the owner of this calendar'); 57 | } 58 | // get list of participants to remove from calendar 59 | const listOfParticipants: ObjectId[] = req.body.participants; 60 | // remove users from list of participants in calendar 61 | calendar.participants = calendar.participants.filter( 62 | (participant: ObjectId) => !listOfParticipants.includes(participant) 63 | ); 64 | // save calendar 65 | await calendar.save(); 66 | res.status(200).json({ 67 | calendar, 68 | }); 69 | } catch (err) { 70 | next(err); 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /backend-app/controllers/tryingtsoa.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Route } from 'tsoa'; 2 | import { Body, Post, Query, Response, Path, Middlewares } from '@tsoa/runtime'; 3 | import { Request, Response as i_res } from 'express'; 4 | 5 | // interface IUser { 6 | // /** 7 | // * @isString Please enter a valid name as a string 8 | // * @minLength 1 Name must have at least 1 character 9 | // * @maxLength 255 Name should not exceed 255 characters 10 | // */ 11 | // name: string; 12 | 13 | // /** 14 | // * @isString Please enter a valid email address 15 | // * @isEmail Please provide a valid email format 16 | // * @minLength 5 Email must have at least 5 characters 17 | // * @maxLength 255 Email should not exceed 255 characters 18 | // */ 19 | // email: string; 20 | 21 | // /** 22 | // * @isString Please enter a valid address as a string 23 | // * @maxLength 255 Address should not exceed 255 characters 24 | // */ 25 | // address?: string; 26 | 27 | // /** 28 | // * @isString Please enter a valid password as a string 29 | // * @minLength 8 Password must have at least 8 characters 30 | // * @maxLength 255 Password should not exceed 255 characters 31 | // */ 32 | // password?: string; 33 | 34 | // /** 35 | // * @isArray Please provide an array of authorities 36 | // * @minItems 1 At least one authority is required 37 | // */ 38 | // authorities: string[]; 39 | 40 | // /** 41 | // * @isArray Please provide an array of restrictions 42 | // * @uniqueItems Restrictions should be unique 43 | // */ 44 | // restrictions: string[]; 45 | 46 | // /** 47 | // * @isArray Please provide an array of roles 48 | // * @uniqueItems Roles should be unique 49 | // */ 50 | // roles: string[]; 51 | 52 | // /** 53 | // * @isBool Please provide a valid boolean value for active 54 | // */ 55 | // active: boolean; 56 | 57 | // /** 58 | // * @isString Please provide a valid activation key as a string 59 | // * @maxLength 255 Activation key should not exceed 255 characters 60 | // */ 61 | // activationKey?: string; 62 | 63 | // /** 64 | // * @isBool Please provide a valid boolean value for accessRestricted 65 | // */ 66 | // accessRestricted: boolean; 67 | 68 | // /** 69 | // * @isString Please provide a valid GitHub OAuth access token as a string 70 | // * @maxLength 255 GitHub OAuth access token should not exceed 255 characters 71 | // */ 72 | // githubOauthAccessToken?: string; 73 | 74 | // /** 75 | // * @isString Please provide a valid reset key as a string 76 | // * @maxLength 255 Reset key should not exceed 255 characters 77 | // */ 78 | // resetKey?: string; 79 | 80 | // /** 81 | // * @isDateTime Please provide a valid date and time for createdAt 82 | // */ 83 | // createdAt: Date; 84 | 85 | // /** 86 | // * @isDateTime Please provide a valid date and time for updatedAt 87 | // */ 88 | // updatedAt: Date; 89 | 90 | // /** 91 | // * @isBool Please provide a valid boolean value for deleted 92 | // */ 93 | // deleted: boolean; 94 | 95 | // /** 96 | // * @isString Please enter a valid deleted by as a string 97 | // * @maxLength 255 Deleted by should not exceed 255 characters 98 | // */ 99 | // deletedBy?: string; 100 | 101 | // /** 102 | // * @isDateTime Please provide a valid date and time for deletedAt 103 | // */ 104 | // deletedAt?: Date; 105 | 106 | // /** 107 | // * @isString Please enter a valid created by as a string 108 | // * @maxLength 255 Created by should not exceed 255 characters 109 | // */ 110 | // createdBy?: string; 111 | 112 | // /** 113 | // * @isString Please enter a valid updated by as a string 114 | // * @maxLength 255 Updated by should not exceed 255 characters 115 | // */ 116 | // updatedBy?: string; 117 | // } 118 | 119 | interface ValidateErrorJSON { 120 | message: 'Validation failed'; 121 | details: { [name: string]: unknown }; 122 | } 123 | 124 | function customMiddleware(req: Request, res: i_res, next: any) { 125 | // Perform any necessary operations or modifications 126 | next(); 127 | } 128 | 129 | @Route('users') 130 | export class UsersController extends Controller { 131 | /** 132 | * Retrieves the details of an existing user. 133 | * Supply the unique user ID from either and receive corresponding user details. 134 | */ 135 | @Response(404, 'Not Found') 136 | @Post('{userId}') 137 | @Middlewares(customMiddleware) 138 | public getUser( 139 | @Path() userId: number, 140 | @Query() name?: string, 141 | @Body() body?: { name: string } 142 | ): {} { 143 | // return new UsersService().get(userId, name); 144 | return { userId, name }; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /backend-app/controllers/users_controllers/admin_controller.ts: -------------------------------------------------------------------------------- 1 | import { IReq, IRes, INext } from '@interfaces/vendors'; 2 | import USER from '@models/user/user_model'; 3 | import Role from '@utils/authorization/roles/role'; 4 | import AppError from '@utils/app_error'; 5 | import validateActions from '@utils/authorization/validate_actions'; 6 | 7 | export const addAdmin = async (req: IReq, res: IRes, next: INext) => { 8 | try { 9 | const Roles = await Role.getRoles(); 10 | const { userId } = req.params; 11 | const user = await USER.findById(userId); 12 | if (!user) throw new AppError(404, 'No user found with this id'); 13 | if (!Roles.ADMIN) 14 | throw new AppError( 15 | 500, 16 | 'Error in base roles, please contact an admin' 17 | ); 18 | if (user.roles?.includes(Roles.ADMIN.name)) 19 | throw new AppError(400, 'User is already an admin'); 20 | user.roles?.push(Roles.ADMIN.name); 21 | const existingAuthorities = user.authorities; 22 | const existingRestrictions = user.restrictions; 23 | user.authorities = Array.from( 24 | new Set([...Roles.ADMIN.authorities, ...existingAuthorities]) 25 | ); 26 | user.restrictions = Array.from( 27 | new Set([...Roles.ADMIN.restrictions, ...existingRestrictions]) 28 | ); 29 | await user.save(); 30 | res.status(200).json({ 31 | message: 'User is now an admin', 32 | }); 33 | } catch (err) { 34 | next(err); 35 | } 36 | }; 37 | 38 | export const removeAdmin = async (req: IReq, res: IRes, next: INext) => { 39 | try { 40 | const Roles = await Role.getRoles(); 41 | const { userId } = req.params; 42 | const user = await USER.findById(userId); 43 | if (!user) throw new AppError(404, 'No user found with this id'); 44 | if (!Roles.ADMIN || !Roles.USER) 45 | throw new AppError( 46 | 500, 47 | 'Error in base roles, please contact an admin' 48 | ); 49 | if (req.user._id?.toString() === userId?.toString()) 50 | throw new AppError(400, 'You cannot remove yourself as an admin'); 51 | if (!user.roles?.includes(Roles.ADMIN.name)) 52 | throw new AppError(400, 'User is not an admin'); 53 | user.roles = user.roles.filter((role) => role !== Roles.ADMIN.name); 54 | user.authorities = Roles.USER.authorities; 55 | user.restrictions = Roles.USER.restrictions; 56 | await user.save(); 57 | res.status(200).json({ 58 | message: 'User is no longer an admin', 59 | }); 60 | } catch (err) { 61 | next(err); 62 | } 63 | }; 64 | 65 | export const addSuperAdmin = async (req: IReq, res: IRes, next: INext) => { 66 | try { 67 | const Roles = await Role.getRoles(); 68 | const { userId } = req.params; 69 | const user = await USER.findById(userId); 70 | if (!user) throw new AppError(404, 'No user found with this id'); 71 | if (req.user._id?.toString() === userId?.toString()) 72 | throw new AppError(400, 'You cannot make yourself a super admin'); 73 | if (user.roles?.includes(Roles.SUPER_ADMIN.name)) 74 | throw new AppError(400, 'User is already a super admin'); 75 | user.roles?.push(Roles.SUPER_ADMIN.name); 76 | const existingRestrictions = user.restrictions; 77 | user.authorities = Roles.SUPER_ADMIN.authorities; 78 | user.restrictions = Array.from( 79 | new Set([ 80 | ...Roles.SUPER_ADMIN.restrictions, 81 | ...existingRestrictions, 82 | ]) 83 | ); 84 | await user.save(); 85 | res.status(200).json({ 86 | message: 'User is now a super admin', 87 | }); 88 | } catch (err) { 89 | next(err); 90 | } 91 | }; 92 | 93 | export const removeSuperAdmin = async (req: IReq, res: IRes, next: INext) => { 94 | const { userId } = req.params; 95 | try { 96 | const Roles = await Role.getRoles(); 97 | const user = await USER.findById(userId); 98 | if (!user) throw new AppError(404, 'No user found with this id'); 99 | if (req.user._id?.toString() === userId?.toString()) 100 | throw new AppError( 101 | 400, 102 | 'You cannot remove yourself as a super admin' 103 | ); 104 | if (!user.roles?.includes(Roles.SUPER_ADMIN.name)) 105 | throw new AppError(400, 'User is not a super admin'); 106 | user.roles = user.roles.filter( 107 | (role) => role !== Roles.SUPER_ADMIN.name 108 | ); 109 | user.authorities = Roles.ADMIN.authorities; 110 | user.restrictions = Roles.ADMIN.restrictions; 111 | await user.save(); 112 | res.status(200).json({ 113 | message: 'User is no longer a super admin', 114 | }); 115 | } catch (err) { 116 | next(err); 117 | } 118 | }; 119 | 120 | export const authorizeOrRestrict = async ( 121 | req: IReq, 122 | res: IRes, 123 | next: INext 124 | ) => { 125 | try { 126 | const { authorities, restrictions } = req.body; 127 | const { userId } = req.params; 128 | if (!validateActions(authorities)) 129 | throw new AppError( 130 | 400, 131 | 'One or many actions are invalid in the authorities array' 132 | ); 133 | if (!validateActions(restrictions)) 134 | throw new AppError( 135 | 400, 136 | 'One or many actions are invalid in the restrictions array' 137 | ); 138 | if (req.user._id?.toString() === userId?.toString()) 139 | throw new AppError( 140 | 400, 141 | 'You cannot change your own authorities or restrictions' 142 | ); 143 | const user = await USER.findById(userId); 144 | if (!user) throw new AppError(404, 'No user found with this id'); 145 | // if the user is a super admin, he can't be restricted 146 | if (user.roles?.includes('SUPER_ADMIN')) 147 | throw new AppError(400, 'User is a super admin'); 148 | const existingAuthorities = user.authorities; 149 | const existingRestrictions = user.restrictions; 150 | user.authorities = Array.from( 151 | new Set([...authorities, ...existingAuthorities]) 152 | ); 153 | user.restrictions = Array.from( 154 | new Set([...restrictions, ...existingRestrictions]) 155 | ); 156 | await user.save(); 157 | res.status(200).json({ 158 | message: 'User authorities and restrictions updated', 159 | }); 160 | } catch (err) { 161 | next(err); 162 | } 163 | }; 164 | 165 | export const banUser = async (req: IReq, res: IRes, next: INext) => { 166 | const { userId } = req.params; 167 | try { 168 | const Roles = await Role.getRoles(); 169 | const user = await USER.findById(userId); 170 | if (!user) throw new AppError(404, 'No user found with this id'); 171 | if (req.user._id?.toString() === userId?.toString()) 172 | throw new AppError(400, 'You cannot ban yourself'); 173 | if (user.accessRestricted) 174 | throw new AppError(400, 'User is already banned'); 175 | if (user.roles?.includes(Roles.SUPER_ADMIN.name)) 176 | throw new AppError(400, 'You cannot ban a super admin'); 177 | if (user.roles?.includes(Roles.ADMIN.name)) 178 | throw new AppError(400, 'You cannot ban an admin'); 179 | user.accessRestricted = true; 180 | await user.save(); 181 | res.status(200).json({ 182 | message: 'User is now banned', 183 | }); 184 | } catch (err) { 185 | next(err); 186 | } 187 | }; 188 | 189 | export const unbanUser = async (req: IReq, res: IRes, next: INext) => { 190 | const { userId } = req.params; 191 | try { 192 | const user = await USER.findById(userId); 193 | if (!user) throw new AppError(404, 'No user found with this id'); 194 | if (req.user._id?.toString() === userId?.toString()) 195 | throw new AppError(400, 'You cannot unban yourself'); 196 | if (!user.accessRestricted) 197 | throw new AppError(400, 'User is not banned'); 198 | user.accessRestricted = false; 199 | await user.save(); 200 | res.status(200).json({ 201 | message: 'User is now unbanned', 202 | }); 203 | } catch (err) { 204 | next(err); 205 | } 206 | }; 207 | 208 | export const createRole = async (req: IReq, res: IRes, next: INext) => { 209 | const { name, authorities, restrictions } = req.body; 210 | try { 211 | if (await Role.getRoleByName(name)) 212 | throw new AppError(400, 'Role already exists'); 213 | const createdRole = await Role.createRole( 214 | name, 215 | authorities, 216 | restrictions 217 | ); 218 | res.status(201).json({ 219 | message: 'Role created', 220 | data: createdRole, 221 | }); 222 | } catch (err) { 223 | next(err); 224 | } 225 | }; 226 | 227 | export const getRoles = async (_req: IReq, res: IRes, next: INext) => { 228 | try { 229 | const roles = await Role.getRoles(); 230 | res.status(200).json({ 231 | message: 'Roles retrieved', 232 | data: roles, 233 | }); 234 | } catch (err) { 235 | next(err); 236 | } 237 | }; 238 | 239 | export const getRole = async (req: IReq, res: IRes, next: INext) => { 240 | const { name } = req.params; 241 | try { 242 | const singleRole = await Role.getRoleByName(name as string); 243 | res.status(200).json({ 244 | message: 'Role retrieved', 245 | data: singleRole, 246 | }); 247 | } catch (err) { 248 | next(err); 249 | } 250 | }; 251 | 252 | export const deleteRole = async (req: IReq, res: IRes, next: INext) => { 253 | const { name } = req.params; 254 | try { 255 | const deletedRole = await Role.deleteRoleByName(name as string); 256 | res.status(200).json({ 257 | message: 'Role deleted', 258 | data: deletedRole, 259 | }); 260 | } catch (err) { 261 | next(err); 262 | } 263 | }; 264 | 265 | export const updateRole = async (req: IReq, res: IRes, next: INext) => { 266 | const { name } = req.params; 267 | const { authorities, restrictions } = req.body; 268 | try { 269 | const updatedRole = await Role.updateRoleByName( 270 | name as string, 271 | authorities, 272 | restrictions 273 | ); 274 | res.status(200).json({ 275 | message: 'Role updated', 276 | data: updatedRole, 277 | }); 278 | } catch (err) { 279 | next(err); 280 | } 281 | }; 282 | 283 | export const assignRoleToUser = async (req: IReq, res: IRes, next: INext) => { 284 | const { userId, name } = req.params; 285 | try { 286 | const user = await USER.findById(userId); 287 | const role = await Role.getRoleByName(name as string); 288 | if (!user) throw new AppError(404, 'No user found with this id'); 289 | if (!role) throw new AppError(404, 'No role found with this name'); 290 | if (user.roles.includes(role.name)) 291 | throw new AppError(400, 'User already has this role'); 292 | user.roles.push(role.name); 293 | user.authorities = Array.from( 294 | new Set([...role.authorities, ...user.authorities]) 295 | ); 296 | user.restrictions = Array.from( 297 | new Set([...role.restrictions, ...user.restrictions]) 298 | ); 299 | await user.save(); 300 | res.status(200).json({ 301 | message: 'Role assigned to user', 302 | }); 303 | } catch (err) { 304 | next(err); 305 | } 306 | }; 307 | 308 | export const removeRoleFromUser = async (req: IReq, res: IRes, next: INext) => { 309 | const { userId, name } = req.params; 310 | try { 311 | const role = await Role.getRoleByName(name as string); 312 | if (!role) throw new AppError(404, 'No role found with this name'); 313 | const user = await USER.findById(userId); 314 | if (!user) throw new AppError(404, 'No user found with this id'); 315 | if (!user.roles.includes(role.name)) 316 | throw new AppError(400, 'User does not have this role'); 317 | user.roles = user.roles.filter((_role) => _role !== role.name); 318 | user.authorities = user.authorities.filter( 319 | (authority) => !role.authorities.includes(authority) 320 | ); 321 | user.restrictions = user.restrictions.filter( 322 | (restriction) => !role.restrictions.includes(restriction) 323 | ); 324 | await user.save(); 325 | res.status(200).json({ 326 | message: 'Role removed from user', 327 | }); 328 | } catch (err) { 329 | next(err); 330 | } 331 | }; 332 | -------------------------------------------------------------------------------- /backend-app/controllers/users_controllers/user_controller.ts: -------------------------------------------------------------------------------- 1 | import User from '@models/user/user_model'; 2 | import * as base from '@controllers/base_controller'; 3 | import AppError from '@utils/app_error'; 4 | import { INext, IReq, IRes } from '@interfaces/vendors'; 5 | 6 | export const getMe = (req: IReq, res: IRes) => { 7 | // return data of the current user 8 | res.status(200).json({ 9 | user: req.user, 10 | }); 11 | }; 12 | 13 | export const deleteMe = async (req: IReq, res: IRes, next: INext) => { 14 | try { 15 | await User.findByIdAndUpdate(req.user._id, { 16 | deleted: true, 17 | deletedAt: Date.now(), 18 | deletedBy: req.user._id, 19 | }); 20 | 21 | res.status(204).json({ 22 | data: null, 23 | }); 24 | } catch (error) { 25 | next(error); 26 | } 27 | }; 28 | 29 | export const updateMe = async (req: IReq, res: IRes, next: INext) => { 30 | try { 31 | // 1) Create error if user POSTs password data 32 | if (req.body.password || req.body.passwordConfirm) { 33 | throw new AppError( 34 | 400, 35 | 'This route is not for password updates. Please use /updateMyPassword' 36 | ); 37 | } 38 | // create error if user tries to update role 39 | if (req.body.roles) { 40 | throw new AppError( 41 | 400, 42 | 'This route is not for role updates. Please use /updateRole' 43 | ); 44 | } 45 | // 2) Filtered out unwanted fields names that are not allowed to be updated 46 | const payload = { 47 | name: req.body.name, 48 | email: req.body.email, 49 | }; 50 | // 3) Update user document 51 | const doc = await User.findByIdAndUpdate(req.user._id, payload, { 52 | new: true, 53 | runValidators: true, 54 | }); 55 | if (!doc) { 56 | return next(new AppError(404, 'No document found with that id')); 57 | } 58 | 59 | res.status(200).json({ 60 | doc, 61 | }); 62 | } catch (error) { 63 | next(error); 64 | } 65 | }; 66 | 67 | export const getAllUsers = base.getAll(User); 68 | export const getUser = base.getOne(User); 69 | 70 | // Don't update password on this 71 | export const updateUser = base.updateOne(User); 72 | export const deleteUser = base.deleteOne(User); 73 | -------------------------------------------------------------------------------- /backend-app/docs/full-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-ninja11/planemaker/2f778de23c977ddadf968d2905ee2706c121afd1/backend-app/docs/full-logo.jpg -------------------------------------------------------------------------------- /backend-app/docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-ninja11/planemaker/2f778de23c977ddadf968d2905ee2706c121afd1/backend-app/docs/logo.png -------------------------------------------------------------------------------- /backend-app/interfaces/github_repo.ts: -------------------------------------------------------------------------------- 1 | export default interface Repository { 2 | id: number; 3 | name: string; 4 | full_name: string; 5 | description: string; 6 | isFork: boolean; 7 | language: string; 8 | license: string | null; 9 | openedIssuesCount: number; 10 | repoCreatedAt: string; 11 | url: string; 12 | } 13 | -------------------------------------------------------------------------------- /backend-app/interfaces/models/i_calendar.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | export interface ICalendar extends Document { 4 | Name: string; 5 | Type: string; 6 | isPublic: boolean; 7 | isShareAble: boolean; 8 | participants: mongoose.Schema.Types.ObjectId[]; 9 | events: mongoose.Schema.Types.ObjectId[]; 10 | description?: string; 11 | accessCode?: string; 12 | tags: string[]; 13 | allowedUsers: mongoose.Schema.Types.ObjectId[]; 14 | deniedUsers: mongoose.Schema.Types.ObjectId[]; 15 | deleted: boolean; 16 | deletedBy?: string; 17 | deletedAt?: Date; 18 | createdBy?: string; 19 | updatedBy?: string; 20 | } 21 | -------------------------------------------------------------------------------- /backend-app/interfaces/models/i_event.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose'; 2 | 3 | export interface IEvent extends Document { 4 | name: string; 5 | description?: string; 6 | location?: string; 7 | startDate: Date; 8 | endDate: Date; 9 | startTime: string; 10 | endTime: string; 11 | color: string; 12 | recurring?: boolean; 13 | recurringType?: 'Daily' | 'Weekly' | 'Monthly' | 'Yearly'; 14 | recurringEndDate?: Date; 15 | reminder?: Date; 16 | deleted: boolean; 17 | deletedBy?: string; 18 | deletedAt?: Date; 19 | createdBy?: string; 20 | updatedBy?: string; 21 | } 22 | -------------------------------------------------------------------------------- /backend-app/interfaces/models/i_role.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose'; 2 | 3 | export interface IRole extends Document { 4 | name: string; 5 | authorities: string[]; 6 | restrictions: string[]; 7 | deleted: boolean; 8 | deletedBy?: string; 9 | deletedAt?: Date; 10 | createdBy?: string; 11 | updatedBy?: string; 12 | } 13 | -------------------------------------------------------------------------------- /backend-app/interfaces/models/i_user.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId } from 'mongoose'; 2 | import { Document } from 'mongoose'; 3 | 4 | export interface IUser extends Document { 5 | _id: ObjectId; 6 | name: string; 7 | email: string; 8 | address?: string; 9 | password?: string; 10 | authorities: string[]; 11 | restrictions: string[]; 12 | roles: string[]; 13 | active: boolean; 14 | activationKey?: string; 15 | accessRestricted: boolean; 16 | githubOauthAccessToken?: string; 17 | resetKey?: string; 18 | createdAt: Date; 19 | updatedAt: Date; 20 | deleted: boolean; 21 | deletedBy?: string; 22 | deletedAt?: Date; 23 | createdBy?: string; 24 | updatedBy?: string; 25 | correctPassword( 26 | typedPassword: string, 27 | originalPassword: string 28 | ): Promise; 29 | isAuthorizedTo(action: string[]): boolean; 30 | isRestrictedFrom(action: string[]): boolean; 31 | generateResetKey(): string; 32 | } 33 | -------------------------------------------------------------------------------- /backend-app/interfaces/vendors.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | 3 | export interface IReq extends Request { 4 | user: { 5 | _id: ObjectId; 6 | name?: string; 7 | email: string; 8 | roles?: string[]; 9 | authorities?: string[]; 10 | restrictions?: string[]; 11 | active: boolean; 12 | githubOauthAccessToken?: string; 13 | }; 14 | } 15 | 16 | import { Response } from 'express'; 17 | 18 | export interface IRes extends Response {} 19 | 20 | import { NextFunction } from 'express'; 21 | import { ObjectId } from 'mongoose'; 22 | 23 | export interface INext extends NextFunction {} 24 | -------------------------------------------------------------------------------- /backend-app/middlewares/api_version_controll.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { API_VERSION } from '@config/app_config'; 3 | 4 | /** 5 | * Middleware to set default API version in the request URL if not provided. 6 | * If the API version is missing, it will be set to 'v3' by default. 7 | * @param {Request} req - Express request object. 8 | * @param {Response} res - Express response object. 9 | * @param {NextFunction} next - Express next function to pass control to the next middleware or route handler. 10 | */ 11 | const handleAPIVersion = (req: Request, _res: Response, next: NextFunction) => { 12 | if (!req.headers['accept-version']) { 13 | req.headers['accept-version'] = API_VERSION; 14 | } 15 | next(); 16 | }; 17 | 18 | export default handleAPIVersion; 19 | -------------------------------------------------------------------------------- /backend-app/middlewares/authorization.ts: -------------------------------------------------------------------------------- 1 | import AppError from '@utils/app_error'; 2 | import { INext, IReq, IRes } from '@interfaces/vendors'; 3 | import User from '@models/user/user_model'; 4 | 5 | const restrictTo = 6 | (...actions: string[]) => 7 | async (req: IReq, res: IRes, next: INext): Promise => { 8 | try { 9 | // get the user by id 10 | const user = await User.findById(req.user._id); 11 | if (!user) 12 | throw new AppError( 13 | 401, 14 | 'The user belonging to this token does no longer exist' 15 | ); 16 | if (user.isAuthorizedTo(actions)) { 17 | if (!user.isRestrictedFrom(actions)) { 18 | next(); 19 | } else { 20 | next( 21 | new AppError( 22 | 403, 23 | 'You are restricted from performing this action, contact the admin for more information' 24 | ) 25 | ); 26 | } 27 | } else { 28 | next( 29 | new AppError( 30 | 403, 31 | 'You do not have permission to perform this action ; required permissions: ' + 32 | actions 33 | ) 34 | ); 35 | } 36 | } catch (error) { 37 | next(error); 38 | } 39 | }; 40 | export default restrictTo; 41 | -------------------------------------------------------------------------------- /backend-app/middlewares/global_error_handler.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import httpStatus from 'http-status-codes'; 3 | import { CURRENT_ENV } from '@config/app_config'; 4 | import AppError from '@utils/app_error'; 5 | 6 | /** 7 | * Error handling middleware 8 | * @param {Error} err - The error object 9 | * @param {Object} req - The Express request object 10 | * @param {Object} res - The Express response object 11 | * @param {Function} next - The next middleware function 12 | * @returns {void} 13 | * Express automatically knows that this entire function is an error handling middleware by specifying 4 parameters 14 | */ 15 | const errorHandler = ( 16 | err: Error, 17 | req: Request, 18 | res: Response, 19 | _next: NextFunction 20 | ): void => { 21 | // Set default values if not provided 22 | (err as any).path = (err as any).path || req.path; 23 | (err as any).statusCode = (err as any).statusCode || 500; 24 | (err as any).message = 25 | (err as any).message || 26 | httpStatus.getStatusText((err as any).statusCode); 27 | 28 | // Handle duplicate key errors 29 | if ((err as any).code === 11000) { 30 | Object.keys((err as any).keyValue).forEach((key) => { 31 | ( 32 | err as any 33 | ).message = `Duplicate value for the field: [${key}]. Please use another value!`; 34 | }); 35 | (err as any).statusCode = 409; 36 | } 37 | 38 | // Handle validation errors 39 | if ((err as any).name === 'ValidationError') { 40 | err = new AppError(400, (err as any).message); 41 | } 42 | 43 | // Determine the message based on error status code and environment 44 | let message; 45 | if ((err as any).statusCode >= 500) { 46 | if (CURRENT_ENV === 'development') { 47 | message = (err as any).message; 48 | } else { 49 | message = 50 | "We're sorry, something went wrong. Please try again later." + 51 | err; 52 | } 53 | } else { 54 | message = (err as any).message; 55 | } 56 | // tsting : 57 | const stackTrace = (err as any).stack?.split('at '); 58 | const stackTraceObject = stackTrace?.reduce( 59 | (acc: any, line: string, index: number) => { 60 | acc[index + 1] = line; 61 | return acc; 62 | }, 63 | {} 64 | ); 65 | 66 | // Construct the response object 67 | const response = { 68 | status: (err as any).statusCode, 69 | title: httpStatus.getStatusText((err as any).statusCode), 70 | details: { 71 | ...((err as any).path && { path: (err as any).path }), 72 | ...(CURRENT_ENV === 'development' && { 73 | error: { 74 | '0': 'Do not forget to remove this in production!', 75 | ...stackTraceObject, 76 | }, 77 | }), 78 | }, 79 | message, 80 | }; 81 | // logger.debug((err as any).stack); 82 | 83 | // Send the response 84 | res.status((err as any).statusCode).json(response); 85 | }; 86 | 87 | export default errorHandler; 88 | -------------------------------------------------------------------------------- /backend-app/middlewares/morgan.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description This file contains the morgan middleware for logging requests 3 | */ 4 | import morgan from 'morgan'; 5 | import logger from '@utils/logger'; 6 | import { CURRENT_ENV } from '@config/app_config'; 7 | 8 | // Create a stream object with a 'write' function that will be used by `morgan` 9 | const stream = { 10 | write: (message: string) => logger.http(message), 11 | }; 12 | 13 | // Setup the logger 14 | const Morgan = morgan( 15 | ':method :url :status :res[content-length] - :response-time ms', 16 | { 17 | stream, 18 | skip: (req: any) => 19 | CURRENT_ENV.toLowerCase() === 'production' || 20 | CURRENT_ENV.toLowerCase() === 'test' || 21 | (req.originalUrl && req.originalUrl !== req.url), 22 | } 23 | ); 24 | 25 | // Export the logger 26 | export default Morgan; 27 | -------------------------------------------------------------------------------- /backend-app/middlewares/rate_limit.ts: -------------------------------------------------------------------------------- 1 | import rateLimit from 'express-rate-limit'; 2 | import { RATE_LIMIT_PER_HOUR } from '@config/app_config'; 3 | import { Request } from 'express'; 4 | 5 | export default rateLimit({ 6 | max: RATE_LIMIT_PER_HOUR, 7 | windowMs: 60 * 60 * 1000, 8 | message: 'Too many requests from this IP, please try again in an hour!', 9 | skip: (req: Request) => req.headers.accept === 'text/event-stream', // Ignore SSE requests 10 | }); 11 | -------------------------------------------------------------------------------- /backend-app/models/calendar/calendar_model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | import metaData from '@constants/meta_data'; 3 | import { ICalendar } from '@interfaces/models/i_calendar'; 4 | 5 | const calendarSchema: Schema = new Schema( 6 | { 7 | Name: { 8 | type: String, 9 | required: [true, 'Please fill your calendar name'], 10 | }, 11 | Type: { 12 | type: String, 13 | required: [true, 'Please fill your calendar type'], 14 | validate: { 15 | validator: function (el: string) { 16 | return el === 'Personal' || el === 'Group'; 17 | }, 18 | }, 19 | }, 20 | isPublic: { 21 | type: Boolean, 22 | default: false, 23 | }, 24 | isShareAble: { 25 | type: Boolean, 26 | default: false, 27 | description: 'If true, the calendar can be shared with other users', 28 | }, 29 | participants: [ 30 | { 31 | type: mongoose.Schema.Types.ObjectId, 32 | ref: 'User', 33 | }, 34 | ], 35 | events: [ 36 | { 37 | type: mongoose.Schema.Types.ObjectId, 38 | ref: 'Event', 39 | }, 40 | ], 41 | description: { 42 | type: String, 43 | }, 44 | accessCode: { 45 | type: String, 46 | }, 47 | tags: [ 48 | { 49 | type: String, 50 | }, 51 | ], 52 | allowedUsers: [ 53 | { 54 | type: mongoose.Schema.Types.ObjectId, 55 | ref: 'User', 56 | }, 57 | ], 58 | deniedUsers: [ 59 | { 60 | type: mongoose.Schema.Types.ObjectId, 61 | ref: 'User', 62 | }, 63 | ], 64 | }, 65 | { 66 | toJSON: { virtuals: true }, 67 | toObject: { virtuals: true }, 68 | } 69 | ); 70 | 71 | metaData.apply(calendarSchema); 72 | 73 | const Calendar = mongoose.model('Calendar', calendarSchema); 74 | 75 | export default Calendar; 76 | -------------------------------------------------------------------------------- /backend-app/models/calendar/event_model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | import metaData from '@constants/meta_data'; 3 | import { IEvent } from '@interfaces/models/i_event'; 4 | 5 | const eventSchema: Schema = new Schema({ 6 | name: { 7 | type: String, 8 | required: [true, 'Please fill your event name'], 9 | }, 10 | description: { 11 | type: String, 12 | }, 13 | location: { 14 | type: String, 15 | }, 16 | startDate: { 17 | type: Date, 18 | required: [true, 'Please fill your event start date'], 19 | }, 20 | endDate: { 21 | type: Date, 22 | required: [true, 'Please fill your event end date'], 23 | }, 24 | startTime: { 25 | type: String, 26 | required: [true, 'Please fill your event start time'], 27 | }, 28 | endTime: { 29 | type: String, 30 | required: [true, 'Please fill your event end time'], 31 | }, 32 | color: { 33 | type: String, 34 | required: [true, 'Please fill your event color'], 35 | }, 36 | recurring: { 37 | type: Boolean, 38 | default: false, 39 | }, 40 | recurringType: { 41 | type: String, 42 | enum: ['Daily', 'Weekly', 'Monthly', 'Yearly'], 43 | }, 44 | recurringEndDate: { 45 | type: Date, 46 | }, 47 | reminder: { 48 | type: Date, 49 | validate: { 50 | validator: function (this: IEvent, el: Date) { 51 | return el < this.startDate; 52 | }, 53 | }, 54 | }, 55 | }); 56 | 57 | metaData.apply(eventSchema); 58 | 59 | const Event = mongoose.model('Event', eventSchema); 60 | 61 | export default Event; 62 | -------------------------------------------------------------------------------- /backend-app/models/user/role_model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | import Actions from '@constants/actions'; 3 | import validator from 'validator'; 4 | import { IRole } from '@interfaces/models/i_role'; 5 | 6 | const roleSchema: Schema = new Schema( 7 | { 8 | name: { 9 | type: String, 10 | required: true, 11 | trim: true, 12 | unique: true, 13 | uppercase: true, 14 | validate: { 15 | validator: (value: string) => value.length > 0, 16 | }, 17 | }, 18 | authorities: { 19 | type: [String], 20 | required: true, 21 | default: [], 22 | validate: { 23 | validator: (value: string[]) => 24 | value.every((v) => 25 | validator.isIn(v, Object.values(Actions)) 26 | ), 27 | }, 28 | }, 29 | restrictions: { 30 | type: [String], 31 | required: true, 32 | default: [], 33 | validate: { 34 | validator: (value: string[]) => 35 | value.every((v) => 36 | validator.isIn(v, Object.values(Actions)) 37 | ), 38 | }, 39 | }, 40 | }, 41 | { 42 | timestamps: true, 43 | } 44 | ); 45 | 46 | const roleModel = mongoose.model('Role', roleSchema); 47 | export default roleModel; 48 | -------------------------------------------------------------------------------- /backend-app/models/user/user_model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Model, Schema } from 'mongoose'; 2 | import validator from 'validator'; 3 | import bcrypt from 'bcrypt'; 4 | import Actions from '@constants/actions'; 5 | import metaData from '@constants/meta_data'; 6 | import { randomBytes, createHash } from 'crypto'; 7 | import { IUser } from '@interfaces/models/i_user'; 8 | 9 | const userSchema: Schema = new mongoose.Schema( 10 | { 11 | name: { 12 | type: String, 13 | required: [true, 'Please fill your name'], 14 | }, 15 | email: { 16 | type: String, 17 | required: [true, 'Please fill your email'], 18 | lowercase: true, 19 | validate: [validator.isEmail, ' Please provide a valid email'], 20 | }, 21 | address: { 22 | type: String, 23 | trim: true, 24 | }, 25 | password: { 26 | type: String, 27 | minLength: 6, 28 | select: false, 29 | }, 30 | authorities: { 31 | type: [String], 32 | default: [], 33 | validate: { 34 | validator: function (el: string[]) { 35 | return el.every((action) => 36 | Object.values(Actions).includes(action) 37 | ); 38 | }, 39 | }, 40 | message: 'Please provide a valid action', 41 | }, 42 | restrictions: { 43 | type: [String], 44 | default: [], 45 | validate: { 46 | validator: function (el: string[]) { 47 | return el.every((action) => 48 | Object.values(Actions).includes(action) 49 | ); 50 | }, 51 | message: 'Please provide a valid action', 52 | }, 53 | }, 54 | roles: { 55 | type: [String], 56 | default: [], 57 | }, 58 | active: { 59 | type: Boolean, 60 | default: true, 61 | }, 62 | activationKey: { 63 | type: String, 64 | select: false, 65 | }, 66 | accessRestricted: { 67 | type: Boolean, 68 | default: false, 69 | }, 70 | githubOauthAccessToken: { 71 | type: String, 72 | select: false, 73 | default: null, 74 | }, 75 | resetKey: { 76 | type: String, 77 | select: false, 78 | }, 79 | }, 80 | { timestamps: true } 81 | ); 82 | 83 | // add meta data to the schema 84 | metaData.apply(userSchema); 85 | 86 | userSchema.pre('save', async function (next) { 87 | if ( 88 | !this.isModified('password') || 89 | this.password === undefined || 90 | (this.password === null && this.githubOauthAccessToken !== null) 91 | ) { 92 | return next(); 93 | } 94 | this.password = await bcrypt.hash(this.password, 12); 95 | next(); 96 | }); 97 | 98 | userSchema.methods.correctPassword = async function ( 99 | typedPassword: string, 100 | originalPassword: string 101 | ) { 102 | const isTrue = await bcrypt.compare(typedPassword, originalPassword); 103 | return isTrue; 104 | }; 105 | 106 | // verify if the user is authorized or restricted from an action 107 | userSchema.methods.isAuthorizedTo = function (actions: string[]) { 108 | return actions.every((action) => this.authorities.includes(action)); 109 | }; 110 | 111 | userSchema.methods.isRestrictedFrom = function (actions: string[]) { 112 | return actions.some((action) => this.restrictions.includes(action)); 113 | }; 114 | 115 | // generateResetKey 116 | userSchema.methods.generateResetKey = function () { 117 | const resetKey = randomBytes(32).toString('hex'); 118 | this.resetKey = createHash('sha256').update(resetKey).digest('hex'); 119 | return resetKey; 120 | }; 121 | 122 | userSchema.index( 123 | { email: 1 }, 124 | { unique: true, partialFilterExpression: { deleted: false } } 125 | ); 126 | 127 | userSchema.pre('find', function () { 128 | this.where({ deleted: false }); 129 | }); 130 | 131 | userSchema.pre('findOne', function () { 132 | this.where({ deleted: false }); 133 | }); 134 | 135 | const User: Model = mongoose.model('User', userSchema); 136 | export default User; 137 | -------------------------------------------------------------------------------- /backend-app/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["."], 3 | "ignore": ["swagger.json", "routes/routes.ts"], 4 | "ext": "js ts json", 5 | "exec": "npm run generate && ts-node server.ts" 6 | } 7 | -------------------------------------------------------------------------------- /backend-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "student-workflow-api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.ts", 6 | "scripts": { 7 | "debug": "ndb server.ts", 8 | "start": "nodemon", 9 | "start:build": "node build/server.js", 10 | "test": "ts-mocha --bail", 11 | "test:build": "cd ./build && mocha --bail", 12 | "clean": "tsc --build --clean", 13 | "build": "tsc", 14 | "generate": "tsoa spec-and-routes && tsoa swagger", 15 | "seed": "ts-node-dev ./seeds/m.js" 16 | }, 17 | "author": "NFS-Team", 18 | "license": "ISC", 19 | "dependencies": { 20 | "axios": "1.5.0", 21 | "bcrypt": "5.1.1", 22 | "chai": "4.3.10", 23 | "compression": "1.7.4", 24 | "cookie-parser": "^1.4.5", 25 | "cors": "^2.8.5", 26 | "dotenv": "16.3.1", 27 | "eslint": "8.48.0", 28 | "eslint-config-prettier": "9.0.0", 29 | "eslint-plugin-prettier": "5.0.0", 30 | "express": "4.18.2", 31 | "express-bearer-token": "2.4.0", 32 | "express-mongo-sanitize": "2.2.0", 33 | "express-rate-limit": "6.10.0", 34 | "express-routes-versioning": "1.0.1", 35 | "express-swagger-generator": "1.1.17", 36 | "helmet": "4.6.0", 37 | "hpp": "0.2.3", 38 | "http-status-codes": "2.2.0", 39 | "husky": "7.0.4", 40 | "jsonwebtoken": "9.0.2", 41 | "lint-staged": "14.0.1", 42 | "mocha": "10.2.0", 43 | "mongoose": "7.5.0", 44 | "morgan": "1.10.0", 45 | "prettier": "3.0.3", 46 | "qs": "6.11.2", 47 | "supertest": "6.3.3", 48 | "swagger-ui-express": "5.0.0", 49 | "validator": "^13.11.0", 50 | "winston": "^3.10.0", 51 | "winston-daily-rotate-file": "^4.7.1", 52 | "xss-clean": "^0.1.4" 53 | }, 54 | "devDependencies": { 55 | "@inquirer/prompts": "3.1.1", 56 | "@types/bcrypt": "5.0.0", 57 | "@types/chai": "4.3.6", 58 | "@types/compression": "1.7.3", 59 | "@types/cookie-parser": "1.4.4", 60 | "@types/cors": "2.8.14", 61 | "@types/express": "4.17.17", 62 | "@types/express-routes-versioning": "1.0.1", 63 | "@types/hpp": "0.2.3", 64 | "@types/jsonwebtoken": "9.0.3", 65 | "@types/mocha": "10.0.2", 66 | "@types/morgan": "1.9.5", 67 | "@types/node": "^16.11.7", 68 | "@types/supertest": "2.0.13", 69 | "@types/swagger-ui-express": "4.1.3", 70 | "@types/validator": "13.11.1", 71 | "@typescript-eslint/parser": "6.7.2", 72 | "chai": "4.3.10", 73 | "eslint": "8.48.0", 74 | "eslint-config-prettier": "9.0.0", 75 | "eslint-plugin-prettier": "5.0.0", 76 | "lint-staged": "14.0.1", 77 | "prettier": "3.0.3", 78 | "ts-mocha": "10.0.0", 79 | "ts-node": "^10.4.0", 80 | "tsconfig-paths": "4.2.0", 81 | "tsoa": "5.1.1", 82 | "typescript": "^5.2.2" 83 | }, 84 | "lint-staged": { 85 | "*.+(ts|js)": "eslint --fix", 86 | "*.+(ts|json|js)": "prettier --config .prettierrc.json --write ." 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /backend-app/routes/auth_routes.ts: -------------------------------------------------------------------------------- 1 | import * as authController from '@controllers/auth_controllers/auth_controller'; 2 | import * as password_management from '@controllers/auth_controllers/password_management'; 3 | import express, { Router } from 'express'; 4 | 5 | const router = express.Router(); 6 | 7 | router.post('/signup', authController.signup); 8 | router.post('/login', authController.login); 9 | router.delete('/logout', authController.logout); 10 | router.get('/refreshToken', authController.tokenRefresh); 11 | router.get('/activate', authController.activateAccount); 12 | router.patch('/forgotPassword', password_management.forgotPassword); 13 | router.patch('/updateMyPassword', password_management.updatePassword); 14 | router.get('/github/callback', authController.githubHandler); 15 | 16 | const authRoutes = (mainrouter: Router) => { 17 | mainrouter.use('/auth', router); 18 | }; 19 | 20 | export default authRoutes; 21 | -------------------------------------------------------------------------------- /backend-app/routes/calendar_routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as base from '@controllers/base_controller'; 3 | import { restrictTo } from '@controllers/auth_controllers/auth_controller'; 4 | import Calendar from '@models/calendar/calendar_model'; 5 | const router = Router(); 6 | 7 | router.post('/', base.createOne(Calendar)); 8 | router.get('/:id', base.getOne(Calendar)); 9 | 10 | router.patch('/:id', base.updateOne(Calendar)); 11 | router.delete('/:id', base.deleteOne(Calendar)); 12 | 13 | router.use(restrictTo('ADMIN', 'SUPER_ADMIN')); 14 | 15 | router.get('/', base.getAll(Calendar)); 16 | 17 | export default router; 18 | -------------------------------------------------------------------------------- /backend-app/routes/github_routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | const router = Router(); 3 | import * as githubController from '@controllers/auth_controllers/github_controller'; 4 | 5 | router.get('/recent-repo', githubController.getRecentRepo); 6 | 7 | /** 8 | * Registers the GitHub routes with the main router and adds them to the Swagger documentation. 9 | * @function 10 | * @name githubRoutes 11 | * @param {Object} mainrouter - Express router object. 12 | */ 13 | const githubRoutes = (mainrouter: Router) => { 14 | mainrouter.use('/github', router); 15 | }; 16 | 17 | export default githubRoutes; 18 | -------------------------------------------------------------------------------- /backend-app/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import userRoutes from './users/user_route'; 3 | import adminRoutes from './users/admin_route'; 4 | import superAdminRoutes from './users/super_admin_route'; 5 | import authRoutes from './auth_routes'; 6 | import githubRoutes from './github_routes'; 7 | import { protect } from '@controllers/auth_controllers/auth_controller'; 8 | 9 | const router = Router(); 10 | 11 | // public routes 12 | authRoutes(router); 13 | 14 | router.use(protect); 15 | 16 | // protected routes 17 | userRoutes(router); 18 | adminRoutes(router); 19 | superAdminRoutes(router); 20 | githubRoutes(router); 21 | 22 | export default router; 23 | -------------------------------------------------------------------------------- /backend-app/routes/routes.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 4 | import { 5 | Controller, 6 | ValidationService, 7 | FieldErrors, 8 | ValidateError, 9 | TsoaRoute, 10 | HttpStatusCodeLiteral, 11 | TsoaResponse, 12 | fetchMiddlewares, 13 | } from '@tsoa/runtime'; 14 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 15 | import { UsersController } from './../controllers/tryingtsoa'; 16 | import type { RequestHandler, Router } from 'express'; 17 | 18 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 19 | 20 | const models: TsoaRoute.Models = { 21 | ValidateErrorJSON: { 22 | dataType: 'refObject', 23 | properties: { 24 | message: { 25 | dataType: 'enum', 26 | enums: ['Validation failed'], 27 | required: true, 28 | }, 29 | details: { 30 | dataType: 'nestedObjectLiteral', 31 | nestedProperties: {}, 32 | additionalProperties: { dataType: 'any' }, 33 | required: true, 34 | }, 35 | }, 36 | additionalProperties: false, 37 | }, 38 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 39 | }; 40 | const validationService = new ValidationService(models); 41 | 42 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 43 | 44 | export function RegisterRoutes(app: Router) { 45 | // ########################################################################################################### 46 | // NOTE: If you do not see routes for all of your controllers in this file, then you might not have informed tsoa of where to look 47 | // Please look into the "controllerPathGlobs" config option described in the readme: https://github.com/lukeautry/tsoa 48 | // ########################################################################################################### 49 | app.post( 50 | '/users/:userId', 51 | ...fetchMiddlewares(UsersController), 52 | ...fetchMiddlewares(UsersController.prototype.getUser), 53 | 54 | function UsersController_getUser( 55 | request: any, 56 | response: any, 57 | next: any 58 | ) { 59 | const args = { 60 | userId: { 61 | in: 'path', 62 | name: 'userId', 63 | required: true, 64 | dataType: 'double', 65 | }, 66 | name: { in: 'query', name: 'name', dataType: 'string' }, 67 | body: { 68 | in: 'body', 69 | name: 'body', 70 | dataType: 'nestedObjectLiteral', 71 | nestedProperties: { 72 | name: { dataType: 'string', required: true }, 73 | }, 74 | }, 75 | }; 76 | 77 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 78 | 79 | let validatedArgs: any[] = []; 80 | try { 81 | validatedArgs = getValidatedArgs(args, request, response); 82 | 83 | const controller = new UsersController(); 84 | 85 | const promise = controller.getUser.apply( 86 | controller, 87 | validatedArgs as any 88 | ); 89 | promiseHandler(controller, promise, response, undefined, next); 90 | } catch (err) { 91 | return next(err); 92 | } 93 | } 94 | ); 95 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 96 | 97 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 98 | 99 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 100 | 101 | function isController(object: any): object is Controller { 102 | return ( 103 | 'getHeaders' in object && 104 | 'getStatus' in object && 105 | 'setStatus' in object 106 | ); 107 | } 108 | 109 | function promiseHandler( 110 | controllerObj: any, 111 | promise: any, 112 | response: any, 113 | successStatus: any, 114 | next: any 115 | ) { 116 | return Promise.resolve(promise) 117 | .then((data: any) => { 118 | let statusCode = successStatus; 119 | let headers; 120 | if (isController(controllerObj)) { 121 | headers = controllerObj.getHeaders(); 122 | statusCode = controllerObj.getStatus() || statusCode; 123 | } 124 | 125 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 126 | 127 | returnHandler(response, statusCode, data, headers); 128 | }) 129 | .catch((error: any) => next(error)); 130 | } 131 | 132 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 133 | 134 | function returnHandler( 135 | response: any, 136 | statusCode?: number, 137 | data?: any, 138 | headers: any = {} 139 | ) { 140 | if (response.headersSent) { 141 | return; 142 | } 143 | Object.keys(headers).forEach((name: string) => { 144 | response.set(name, headers[name]); 145 | }); 146 | if ( 147 | data && 148 | typeof data.pipe === 'function' && 149 | data.readable && 150 | typeof data._read === 'function' 151 | ) { 152 | response.status(statusCode || 200); 153 | data.pipe(response); 154 | } else if (data !== null && data !== undefined) { 155 | response.status(statusCode || 200).json(data); 156 | } else { 157 | response.status(statusCode || 204).end(); 158 | } 159 | } 160 | 161 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 162 | 163 | function responder( 164 | response: any 165 | ): TsoaResponse { 166 | return function (status, data, headers) { 167 | returnHandler(response, status, data, headers); 168 | }; 169 | } 170 | 171 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 172 | 173 | function getValidatedArgs(args: any, request: any, response: any): any[] { 174 | const fieldErrors: FieldErrors = {}; 175 | const values = Object.keys(args).map((key) => { 176 | const name = args[key].name; 177 | switch (args[key].in) { 178 | case 'request': 179 | return request; 180 | case 'query': 181 | return validationService.ValidateParam( 182 | args[key], 183 | request.query[name], 184 | name, 185 | fieldErrors, 186 | undefined, 187 | { noImplicitAdditionalProperties: 'throw-on-extras' } 188 | ); 189 | case 'queries': 190 | return validationService.ValidateParam( 191 | args[key], 192 | request.query, 193 | name, 194 | fieldErrors, 195 | undefined, 196 | { noImplicitAdditionalProperties: 'throw-on-extras' } 197 | ); 198 | case 'path': 199 | return validationService.ValidateParam( 200 | args[key], 201 | request.params[name], 202 | name, 203 | fieldErrors, 204 | undefined, 205 | { noImplicitAdditionalProperties: 'throw-on-extras' } 206 | ); 207 | case 'header': 208 | return validationService.ValidateParam( 209 | args[key], 210 | request.header(name), 211 | name, 212 | fieldErrors, 213 | undefined, 214 | { noImplicitAdditionalProperties: 'throw-on-extras' } 215 | ); 216 | case 'body': 217 | return validationService.ValidateParam( 218 | args[key], 219 | request.body, 220 | name, 221 | fieldErrors, 222 | undefined, 223 | { noImplicitAdditionalProperties: 'throw-on-extras' } 224 | ); 225 | case 'body-prop': 226 | return validationService.ValidateParam( 227 | args[key], 228 | request.body[name], 229 | name, 230 | fieldErrors, 231 | 'body.', 232 | { noImplicitAdditionalProperties: 'throw-on-extras' } 233 | ); 234 | case 'formData': 235 | if (args[key].dataType === 'file') { 236 | return validationService.ValidateParam( 237 | args[key], 238 | request.file, 239 | name, 240 | fieldErrors, 241 | undefined, 242 | { 243 | noImplicitAdditionalProperties: 244 | 'throw-on-extras', 245 | } 246 | ); 247 | } else if ( 248 | args[key].dataType === 'array' && 249 | args[key].array.dataType === 'file' 250 | ) { 251 | return validationService.ValidateParam( 252 | args[key], 253 | request.files, 254 | name, 255 | fieldErrors, 256 | undefined, 257 | { 258 | noImplicitAdditionalProperties: 259 | 'throw-on-extras', 260 | } 261 | ); 262 | } else { 263 | return validationService.ValidateParam( 264 | args[key], 265 | request.body[name], 266 | name, 267 | fieldErrors, 268 | undefined, 269 | { 270 | noImplicitAdditionalProperties: 271 | 'throw-on-extras', 272 | } 273 | ); 274 | } 275 | case 'res': 276 | return responder(response); 277 | } 278 | }); 279 | 280 | if (Object.keys(fieldErrors).length > 0) { 281 | throw new ValidateError(fieldErrors, ''); 282 | } 283 | return values; 284 | } 285 | 286 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 287 | } 288 | 289 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 290 | -------------------------------------------------------------------------------- /backend-app/routes/users/admin_route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | authorizeOrRestrict, 4 | banUser, 5 | unbanUser, 6 | createRole, 7 | updateRole, 8 | getRole, 9 | getRoles, 10 | deleteRole, 11 | assignRoleToUser, 12 | removeRoleFromUser, 13 | } from '@controllers/users_controllers/admin_controller'; 14 | import * as authController from '@controllers/auth_controllers/auth_controller'; 15 | import restrictTo from '@middlewares/authorization'; 16 | import Actions from '@constants/actions'; 17 | import * as userController from '@controllers/users_controllers/user_controller'; 18 | 19 | /** 20 | * Below all routes are protected 21 | */ 22 | 23 | const router = Router(); 24 | 25 | router.use(authController.restrictTo('ADMIN', 'SUPER_ADMIN')); 26 | 27 | router 28 | .route('/user/:id') 29 | .get(userController.getUser) 30 | .patch(userController.updateUser) 31 | .delete(userController.deleteUser); 32 | router.get('/users', userController.getAllUsers); 33 | 34 | /** 35 | * @protected 36 | * @route PUT /api/admin/remove-super-admin/:userId 37 | * @description Add authorizations and restrictions to a user 38 | * @access Super Admin 39 | * @param {string} userId - Id of the user to add authorizations and restrictions 40 | * @param {string[]} authorizations - List of authorizations to add to the user 41 | * @param {string[]} restrictions - List of restrictions to add to the user 42 | */ 43 | router.put( 44 | '/authorize-or-restrict/:userId', 45 | restrictTo(Actions.UPDATE_USER), 46 | authorizeOrRestrict 47 | ); 48 | 49 | /** 50 | * @protected 51 | * @route PUT /api/admin/ban-user/:userId 52 | * @description ban a user 53 | * @access Super Admin 54 | * @param {string} userId - Id of the user to ban 55 | **/ 56 | router.put( 57 | '/ban-user/:userId', 58 | restrictTo(Actions.UPDATE_USER, Actions.BAN_USER), 59 | banUser 60 | ); 61 | 62 | /** 63 | * @protected 64 | * @route PUT /api/admin/unban-user/:userId 65 | * @description unban a user 66 | * @access Super Admin 67 | * @param {string} userId - Id of the user to unban 68 | **/ 69 | router.put( 70 | '/unban-user/:userId', 71 | restrictTo(Actions.UPDATE_USER, Actions.BAN_USER), 72 | unbanUser 73 | ); 74 | 75 | /** 76 | * @protected 77 | * @route PUT /api/admin/role 78 | * @description Get all roles 79 | * @access Super Admin 80 | **/ 81 | router.put('/role', restrictTo(Actions.MANAGE_ROLES), getRoles); 82 | 83 | /** 84 | * @protected 85 | * @route PUT /api/admin/role/:name 86 | * @description Get a single role 87 | * @access Super Admin 88 | * @param {string} name - Name of the role to find 89 | **/ 90 | router.put('/role/:name', restrictTo(Actions.MANAGE_ROLES), getRole); 91 | 92 | /** 93 | * @protected 94 | * @route POST /api/admin/role 95 | * @description Create a role 96 | * @access Super Admin 97 | **/ 98 | router.post('/role', restrictTo(Actions.MANAGE_ROLES), createRole); 99 | 100 | /** 101 | * @protected 102 | * @route PUT /api/admin/role/:name 103 | * @description Update a role 104 | * @access Super Admin 105 | * @param {string} name - Name of the role to update 106 | **/ 107 | router.put('/role/:name', restrictTo(Actions.MANAGE_ROLES), updateRole); 108 | 109 | /** 110 | * @protected 111 | * @route DELETE /api/admin/role/:name 112 | * @description Delete a role 113 | * @access Super Admin 114 | * @param {string} name - Name of the role to delete 115 | **/ 116 | router.delete('/role/:name', restrictTo(Actions.MANAGE_ROLES), deleteRole); 117 | 118 | /** 119 | * @protected 120 | * @route PUT /api/admin/assign-role/:name/:userId 121 | * @description Assign a role to a user 122 | * @access Super Admin 123 | * @param {string} name - Name of the role to assign 124 | * @param {string} userId - Id of the user to assign the role to 125 | * */ 126 | router.put( 127 | '/assign-role/:name/:userId', 128 | restrictTo(Actions.MANAGE_ROLES), 129 | assignRoleToUser 130 | ); 131 | /** 132 | * @protected 133 | * @route PUT /api/admin/remove-role/:name/:userId 134 | * @description Remove a role from a user 135 | * @access Super Admin 136 | * @param {string} name - Name of the role to remove 137 | * @param {string} userId - Id of the user to remove the role from 138 | * */ 139 | router.put( 140 | '/remove-role/:name/:userId', 141 | restrictTo(Actions.MANAGE_ROLES), 142 | removeRoleFromUser 143 | ); 144 | 145 | const adminRoutes = (mainrouter: Router) => { 146 | // swaggergenerator.register('admin', './routes/users/admin_route.js'); 147 | mainrouter.use('/admin', router); 148 | }; 149 | export default adminRoutes; 150 | -------------------------------------------------------------------------------- /backend-app/routes/users/super_admin_route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { Router } from 'express'; 3 | import * as authController from '@controllers/auth_controllers/auth_controller'; 4 | import restrictTo from '@middlewares/authorization'; 5 | import Actions from '@constants/actions'; 6 | import { 7 | addSuperAdmin, 8 | removeSuperAdmin, 9 | addAdmin, 10 | removeAdmin, 11 | } from '@controllers/users_controllers/admin_controller'; 12 | 13 | const router = Router(); 14 | 15 | router.use(authController.restrictTo('SUPER_ADMIN')); 16 | 17 | /** 18 | * @protected 19 | * @route PUT /api/admin/add-super-admin/:userId 20 | * @description Add super admin role to a user 21 | * @access Super Admin 22 | * @param {string} userId - Id of the user to add super admin role to 23 | */ 24 | router.put( 25 | '/add-super-admin/:userId', 26 | restrictTo(Actions.UPDATE_USER), 27 | addSuperAdmin 28 | ); 29 | 30 | /* 31 | * @protected 32 | * @route PUT /api/admin/remove-super-admin/:userId 33 | * @description Remove super admin role from a user 34 | * @access Super Admin 35 | * @param {string} userId - Id of the user to remove super admin role from 36 | **/ 37 | router.put( 38 | '/remove-super-admin/:userId', 39 | restrictTo(Actions.UPDATE_USER, Actions.REMOVE_SUPER_ADMIN), 40 | removeSuperAdmin 41 | ); 42 | 43 | /** 44 | * @protected 45 | * @route PUT /api/admin/add-admin/:userId 46 | * @description Add admin role to a user 47 | * @access Super Admin 48 | * @param {string} userId - Id of the user to add admin role to 49 | */ 50 | router.put('/add-admin/:userId', restrictTo(Actions.UPDATE_USER), addAdmin); 51 | 52 | /** 53 | * @protected 54 | * @route PUT /api/admin/remove-admin/:userId 55 | * @description Remove admin role from a user 56 | * @access Super Admin 57 | * @param {string} userId - Id of the user to remove admin role from 58 | */ 59 | router.put( 60 | '/remove-admin/:userId', 61 | restrictTo(Actions.UPDATE_USER), 62 | removeAdmin 63 | ); 64 | 65 | const superAdminRoutes = (mainrouter: express.Router) => { 66 | // swaggergenerator.register( 67 | // 'super_admin', 68 | // './routes/users/super_admin_route.js' 69 | // ); 70 | mainrouter.use('/super_admin', router); 71 | }; 72 | export default superAdminRoutes; 73 | -------------------------------------------------------------------------------- /backend-app/routes/users/user_route.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import * as userController from '@controllers/users_controllers/user_controller'; 3 | 4 | const router = Router(); 5 | router 6 | .route('/me') 7 | /** 8 | * @swagger 9 | * /api/useron: 10 | * get: 11 | * summary: Get a list of users 12 | * description: Retrieve a list of users from the database. 13 | * requestBody: 14 | * description: Optional description in *Markdown* 15 | * required: false 16 | * responses: 17 | * 200: 18 | * description: A list of users. 19 | * content: 20 | * application/json: 21 | * example: 22 | * - id: 1 23 | * name: John Doe 24 | * - id: 2 25 | * name: Jane Smith 26 | * x-swagger-jsdoc: 27 | * tryItOutEnabled: true // Enable "Try it out" by default 28 | */ 29 | .get(userController.getMe) 30 | .delete(userController.deleteMe) 31 | .patch(userController.updateMe); 32 | 33 | const userRoutes = (mainrouter: Router) => { 34 | mainrouter.use('/users', router); 35 | }; 36 | 37 | export default userRoutes; 38 | -------------------------------------------------------------------------------- /backend-app/seeds/index.ts: -------------------------------------------------------------------------------- 1 | function seed() { 2 | try { 3 | // Connect to the database 4 | // await connectDatabase(); 5 | 6 | // // Run individual seed scripts 7 | // await seedUsers(); 8 | // await seedProducts(); 9 | 10 | // // Disconnect from the database 11 | // await disconnectDatabase(); 12 | 13 | console.log('Seeding completed successfully.'); 14 | } catch (error) { 15 | console.error('Error seeding the database:', error); 16 | } 17 | } 18 | 19 | seed(); 20 | -------------------------------------------------------------------------------- /backend-app/seeds/setup_seed.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import '@utils/register_paths'; 3 | import Role from '@models/user/role_model'; 4 | import User from '@models/user/user_model'; 5 | import logger from '@utils/logger'; 6 | import { DATABASE, ADMIN_EMAIL, ADMIN_PASSWORD } from '@config/app_config'; 7 | import Actions from '@constants/actions'; 8 | 9 | // Connect to MongoDB 10 | 11 | async function seed() { 12 | await mongoose 13 | .connect( 14 | DATABASE as string, 15 | { 16 | useNewUrlParser: true, 17 | useUnifiedTopology: true, 18 | } as mongoose.ConnectOptions 19 | ) 20 | .then(() => logger.info('MongoDB Connected')) 21 | .catch((err: any) => logger.error(err)); 22 | 23 | interface Role { 24 | name: string; 25 | authorities: Array; 26 | restrictions: Array; 27 | } 28 | 29 | const Roles: { [key: string]: Role } = { 30 | SUPER_ADMIN: { 31 | name: 'SUPER_ADMIN', 32 | authorities: Object.values(Actions), 33 | restrictions: [], 34 | }, 35 | ADMIN: { 36 | name: 'ADMIN', 37 | authorities: [ 38 | Actions.DELETE_USER, 39 | Actions.UPDATE_USER, 40 | Actions.BAN_USER, 41 | ], 42 | restrictions: [], 43 | }, 44 | USER: { 45 | name: 'USER', 46 | authorities: [Actions.UPDATE_CALANDER], 47 | restrictions: [], 48 | }, 49 | }; 50 | Object.freeze(Roles); 51 | 52 | // Seed default roles 53 | async function seedRoles() { 54 | const roles = await Role.find(); 55 | if (roles.length === 0) { 56 | for (const role of Object.values(Roles)) { 57 | await Role.create(role); 58 | } 59 | logger.info('Roles seeded'); 60 | } else { 61 | logger.info('Roles already seeded'); 62 | } 63 | } 64 | 65 | // seed default users 66 | const createAdminUser = async () => { 67 | try { 68 | const user = await User.findOne({ email: ADMIN_EMAIL }); 69 | if (!user) { 70 | // make sure super admin role is created if not created throw error 71 | const superAdminRole = await Role.findOne({ 72 | name: 'SUPER_ADMIN', 73 | }); 74 | if (!superAdminRole) { 75 | throw new Error('Super Admin role not found'); 76 | } 77 | await User.create({ 78 | name: 'Supper Admin', 79 | email: ADMIN_EMAIL, 80 | password: ADMIN_PASSWORD, 81 | roles: [superAdminRole._id], 82 | authorities: superAdminRole.authorities, 83 | restrictions: superAdminRole.restrictions, 84 | active: true, 85 | }); 86 | } else { 87 | logger.info('default users already created'); 88 | } 89 | } catch (err) { 90 | logger.error(err.stack); 91 | } 92 | }; 93 | 94 | // create a default normal user 95 | 96 | await seedRoles(); 97 | await createAdminUser(); 98 | } 99 | 100 | seed() 101 | .then(() => { 102 | mongoose.disconnect(); 103 | logger.info('Seeding completed successfully.'); 104 | }) 105 | .catch((err) => { 106 | logger.error('Error seeding the database:', err); 107 | mongoose.disconnect(); 108 | }); 109 | -------------------------------------------------------------------------------- /backend-app/seeds/users.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /backend-app/server.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import './utils/register_paths'; 3 | import logger from '@utils/logger'; 4 | import fs from 'fs'; 5 | import { DATABASE, PORT } from './config/app_config'; 6 | import createRoles from './utils/authorization/roles/create_roles'; 7 | 8 | process.on('uncaughtException', (err) => { 9 | logger.error('UNCAUGHT EXCEPTION!!! shutting down ...'); 10 | logger.error(`${err}, ${err.message}, ${err.stack}`); 11 | process.exit(1); 12 | }); 13 | 14 | import app from './app'; 15 | 16 | mongoose.set('strictQuery', true); 17 | 18 | let expServer: Promise; 19 | 20 | // Connect the database 21 | mongoose 22 | .connect( 23 | DATABASE as string, 24 | { useNewUrlParser: true } as mongoose.ConnectOptions 25 | ) 26 | .then(() => { 27 | logger.info('DB Connected Successfully!'); 28 | expServer = startServer(); 29 | logger.info(`Swagger Will Be Available at /docs /docs-json`); 30 | }) 31 | .catch((err: Error) => { 32 | logger.error( 33 | 'DB Connection Failed! \n\tException : ' + err + '\n' + err.stack 34 | ); 35 | }); 36 | 37 | // When the connection is disconnected 38 | mongoose.connection.on('disconnected', () => { 39 | logger.error('DB Connection Disconnected!'); 40 | }); 41 | 42 | // Start the server 43 | const startServer = async (): Promise => { 44 | if (!fs.existsSync('.env')) 45 | logger.warn('.env file not found, using .env.example file'); 46 | logger.info(`App running on http://localhost:${PORT}`); 47 | await createRoles(); 48 | return app.listen(PORT); 49 | }; 50 | 51 | import createDefaultUser from './utils/create_default_user'; 52 | createDefaultUser(); 53 | 54 | process.on('unhandledRejection', (err: Error) => { 55 | logger.error('UNHANDLED REJECTION!!! shutting down ...'); 56 | logger.error(`${err.name}, ${err.message}, ${err.stack}`); 57 | expServer.then((server) => { 58 | server.close(() => { 59 | process.exit(1); 60 | }); 61 | }); 62 | }); 63 | 64 | // add graceful shutdown. 65 | process.on('SIGTERM', () => { 66 | logger.info('SIGINT RECEIVED. Shutting down gracefully'); 67 | mongoose.connection.close(false).then(() => { 68 | logger.info('💥 Process terminated!'); 69 | process.exit(0); 70 | }); 71 | }); 72 | 73 | process.on('SIGINT', () => { 74 | logger.info('SIGINT RECEIVED. Shutting down gracefully'); 75 | mongoose.connection.close(false).then(() => { 76 | logger.info('💥 Process terminated!'); 77 | process.exit(0); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /backend-app/target/npmlist.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "name": "student-workflow-api", 4 | "dependencies": { 5 | "axios": { "version": "1.5.0" }, 6 | "bcrypt": { "version": "5.1.1" }, 7 | "compression": { "version": "1.7.4" }, 8 | "cookie-parser": { "version": "1.4.6" }, 9 | "cors": { "version": "2.8.5" }, 10 | "dotenv": { "version": "16.3.1" }, 11 | "express-bearer-token": { "version": "2.4.0" }, 12 | "express-mongo-sanitize": { "version": "2.2.0" }, 13 | "express-rate-limit": { "version": "6.10.0" }, 14 | "express-routes-versioning": { "version": "1.0.1" }, 15 | "express": { "version": "4.18.2" }, 16 | "helmet": { "version": "4.6.0" }, 17 | "hpp": { "version": "0.2.3" }, 18 | "http-status-codes": { "version": "2.2.0" }, 19 | "jsonwebtoken": { "version": "9.0.2" }, 20 | "mongoose": { "version": "7.5.0" }, 21 | "morgan": { "version": "1.10.0" }, 22 | "qs": { "version": "6.11.2" }, 23 | "supertest": { "version": "6.3.3" }, 24 | "swagger-autogen": { "version": "2.23.5" }, 25 | "swagger-ui-express": { "version": "5.0.0" }, 26 | "validator": { "version": "13.11.0" }, 27 | "winston-daily-rotate-file": { "version": "4.7.1" }, 28 | "winston": { "version": "3.10.0" }, 29 | "xss-clean": { "version": "0.1.4" }, 30 | "yamljs": { "version": "0.3.0" } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend-app/tests/db_config.spec.ts: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const expect = require('chai').expect; 3 | import '@config/app_config'; 4 | import createDefaultUser from '@utils/create_default_user'; 5 | import createRoles from '@utils/authorization/roles/create_roles'; 6 | // import logger from '@root/utils/logger'; 7 | 8 | before(async () => { 9 | mongoose.set('strictQuery', false); 10 | await mongoose.connect(process.env.MONGO_URI_TEST, { 11 | useNewUrlParser: true, 12 | useUnifiedTopology: true, 13 | }); 14 | await createRoles(); 15 | await createDefaultUser(); 16 | }); 17 | after(async () => { 18 | await mongoose.connection.dropDatabase(); 19 | await mongoose.connection.close(); 20 | }); 21 | 22 | describe('Database connection', () => { 23 | it('should connect to the database', () => { 24 | expect(mongoose.connection.readyState).to.equal(1); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /backend-app/tests/e2e/auth/auth.spec.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | import mongoose from 'mongoose'; 4 | import app from '@root/app'; 5 | import User from '@models/user/user_model'; 6 | import AuthUtils from '@utils/authorization/auth_utils'; 7 | import * as supertest from 'supertest'; 8 | const { generateAccessToken } = AuthUtils; 9 | const agent = supertest.agent(app); 10 | const expect = chai.expect; 11 | import request from 'supertest'; 12 | 13 | let res: request.Response; 14 | 15 | describe('Auth API', () => { 16 | describe('POST /signup', () => { 17 | it('should return an error if email is not provided', async () => { 18 | res = await agent.post('/api/auth/signup').send({ 19 | name: 'Test User', 20 | password: 'password123', 21 | }); 22 | 23 | expect(res.status).to.equal(400); 24 | expect(res.body).to.have.property( 25 | 'message', 26 | 'User validation failed: email: Please fill your email' 27 | ); 28 | }); 29 | 30 | it('should return an error if email is invalid', async () => { 31 | res = await agent.post('/api/auth/signup').send({ 32 | name: 'Test User', 33 | email: 'invalidemail', 34 | password: 'password123', 35 | }); 36 | 37 | expect(res.status).to.equal(400); 38 | expect(res.body).to.have.property( 39 | 'message', 40 | 'User validation failed: email: Please provide a valid email' 41 | ); 42 | }); 43 | 44 | it('should return an error if password is not provided', async () => { 45 | res = await agent.post('/api/auth/signup').send({ 46 | name: 'Test User', 47 | email: 'testuser@example.com', 48 | }); 49 | 50 | expect(res.status).to.equal(400); 51 | expect(res.body).to.have.property( 52 | 'message', 53 | 'Please provide a password' 54 | ); 55 | }); 56 | 57 | it('should create a new user', async () => { 58 | res = await agent.post('/api/auth/signup').send({ 59 | name: 'Test User', 60 | email: 'testuser@example.com', 61 | password: 'password123', 62 | }); 63 | 64 | expect(res.status).to.equal(201); 65 | expect(res.body).to.have.property('accessToken'); 66 | expect(res.body).to.have.property('user'); 67 | expect(res.body.user).to.have.property('name', 'Test User'); 68 | expect(res.body.user).to.have.property( 69 | 'email', 70 | 'testuser@example.com' 71 | ); 72 | expect(res.body.user).to.not.have.property('password'); 73 | expect(res.body.user).to.not.have.property('activationKey'); 74 | }); 75 | }); 76 | 77 | afterEach(function () { 78 | const errorBody = res && res.body; 79 | if (this.currentTest.state === 'failed' && errorBody) { 80 | console.debug('res: ', errorBody); 81 | } 82 | 83 | res = null; 84 | }); 85 | describe('POST /login', () => { 86 | it('should login a user', async () => { 87 | res = await agent.post('/api/auth/login').send({ 88 | email: 'testuser@example.com', 89 | password: 'password123', 90 | }); 91 | 92 | expect(res.status).to.equal(200); 93 | expect(res.body).to.have.property('accessToken'); 94 | expect(res.body).to.have.property('user'); 95 | expect(res.body.user).to.have.property('name', 'Test User'); 96 | expect(res.body.user).to.have.property( 97 | 'email', 98 | 'testuser@example.com' 99 | ); 100 | expect(res.body.user).to.not.have.property('password'); 101 | expect(res.body.user).to.not.have.property('activationKey'); 102 | }); 103 | 104 | it('should return an error if email is not provided', async () => { 105 | res = await agent.post('/api/auth/login').send({ 106 | password: 'password123', 107 | }); 108 | 109 | expect(res.status).to.equal(400); 110 | expect(res.body).to.have.property( 111 | 'message', 112 | 'Invalid email or password' 113 | ); 114 | }); 115 | 116 | it('should return an error if email is invalid', async () => { 117 | res = await agent.post('/api/auth/login').send({ 118 | email: 'invalidemail', 119 | password: 'password123', 120 | }); 121 | 122 | expect(res.status).to.equal(400); 123 | expect(res.body).to.have.property( 124 | 'message', 125 | 'Invalid email or password' 126 | ); 127 | }); 128 | 129 | it('should return an error if password is not provided', async () => { 130 | res = await agent.post('/api/auth/login').send({ 131 | email: 'testuser@example.com', 132 | }); 133 | 134 | expect(res.status).to.equal(400); 135 | expect(res.body).to.have.property( 136 | 'message', 137 | 'Please provide a password' 138 | ); 139 | }); 140 | 141 | it('should return an error if email or password is incorrect', async () => { 142 | res = await agent.post('/api/auth/login').send({ 143 | email: 'testuser@example.com', 144 | password: 'wrongpassword', 145 | }); 146 | 147 | expect(res.status).to.equal(401); 148 | expect(res.body).to.have.property( 149 | 'message', 150 | 'Email or Password is wrong' 151 | ); 152 | }); 153 | }); 154 | 155 | describe('GET /activate', () => { 156 | let user: any; 157 | 158 | // login user each time 159 | beforeEach(async () => { 160 | user = await User.findOne({ email: 'testuser@example.com' }).select( 161 | '+activationKey' 162 | ); 163 | }); 164 | 165 | it('should return an error if activation key is not provided', async () => { 166 | res = await agent.get( 167 | `/api/auth/activate?id=${user._id.toString()}` 168 | ); 169 | 170 | expect(res.status).to.equal(400); 171 | expect(res.body).to.have.property( 172 | 'message', 173 | 'Please provide activation key' 174 | ); 175 | }); 176 | 177 | it('should return an error if user id is not provided', async () => { 178 | res = await agent.get( 179 | `/api/auth/activate?activationKey=${user.activationKey}` 180 | ); 181 | 182 | expect(res.status).to.equal(400); 183 | expect(res.body).to.have.property( 184 | 'message', 185 | 'Please provide user id' 186 | ); 187 | }); 188 | 189 | it('should return an error if user id is invalid', async () => { 190 | res = await agent.get( 191 | `/api/auth/activate?id=invalidid&activationKey=${user.activationKey}` 192 | ); 193 | 194 | expect(res.status).to.equal(400); 195 | expect(res.body).to.have.property( 196 | 'message', 197 | 'Please provide a valid user id' 198 | ); 199 | }); 200 | 201 | it('should return an error if activation key is invalid', async () => { 202 | res = await agent.get( 203 | `/api/auth/activate?id=${user._id.toString()}&activationKey=invalidkey` 204 | ); 205 | 206 | expect(res.status).to.equal(400); 207 | expect(res.body).to.have.property( 208 | 'message', 209 | 'Invalid activation key' 210 | ); 211 | }); 212 | 213 | it('should return an error if user does not exist', async () => { 214 | res = await agent.get( 215 | `/api/auth/activate?id=${new mongoose.Types.ObjectId()}&activationKey=${ 216 | user.activationKey 217 | }` 218 | ); 219 | 220 | expect(res.status).to.equal(404); 221 | expect(res.body).to.have.property('message', 'User does not exist'); 222 | }); 223 | 224 | it('should activate a user account', async () => { 225 | res = await agent.get( 226 | `/api/auth/activate?id=${user._id.toString()}&activationKey=${ 227 | user.activationKey 228 | }` 229 | ); 230 | 231 | expect(res.status).to.equal(200); 232 | expect(res.body).to.have.property('user'); 233 | expect(res.body.user).to.have.property('name', 'Test User'); 234 | expect(res.body.user).to.have.property( 235 | 'email', 236 | 'testuser@example.com' 237 | ); 238 | expect(res.body.user).to.not.have.property('password'); 239 | expect(res.body.user).to.not.have.property('activationKey'); 240 | expect(res.body.user).to.have.property('active', true); 241 | }); 242 | it('should return an error if user is already active', async () => { 243 | res = await agent.get( 244 | `/api/auth/activate?id=${user._id.toString()}&activationKey=${ 245 | user.activationKey 246 | }` 247 | ); 248 | 249 | expect(res.status).to.equal(409); 250 | expect(res.body).to.have.property( 251 | 'message', 252 | 'User is already active' 253 | ); 254 | }); 255 | }); 256 | 257 | describe(' GET /auth/refreshToken', () => { 258 | let accessToken: string; 259 | let refreshToken: string; 260 | 261 | // login user each time 262 | beforeEach(async () => { 263 | const user = await User.findOne({ email: 'testuser@example.com' }); 264 | accessToken = AuthUtils.generateAccessToken(user._id.toString()); 265 | refreshToken = AuthUtils.generateRefreshToken(user._id.toString()); 266 | }); 267 | 268 | it('should return an error if refresh token is invalid', async () => { 269 | res = await agent 270 | .get('/api/auth/refreshToken') 271 | .set('Cookie', `access_token=${accessToken}`) 272 | .set('Cookie', `refresh_token=invalidtoken; HttpOnly`); 273 | 274 | expect(res.status).to.equal(400); 275 | expect(res.body).to.have.property( 276 | 'message', 277 | 'Invalid refresh token' 278 | ); 279 | }); 280 | 281 | it('should return an error if refresh token is not provided', async () => { 282 | res = await agent 283 | .get('/api/auth/refreshToken') 284 | .set('Cookie', `access_token=${accessToken}`); 285 | 286 | expect(res.status).to.equal(400); 287 | expect(res.body).to.have.property( 288 | 'message', 289 | 'You have to login to continue.' 290 | ); 291 | }); 292 | 293 | it('should refresh access token', async () => { 294 | // refresh token and access token to be sent in cookies 295 | res = await agent 296 | .get('/api/auth/refreshToken') 297 | .set('Cookie', `access_token=${accessToken}`) 298 | .set('Cookie', `refresh_token=${refreshToken}; HttpOnly`); 299 | 300 | expect(res.status).to.equal(204); 301 | expect(res.header['set-cookie']).to.not.include('access_token'); 302 | }); 303 | }); 304 | 305 | describe('POST /auth/logout', () => { 306 | it('should logout a user', async () => { 307 | const user = await User.findOne({ email: 'testuser@example.com' }); 308 | const accessToken = generateAccessToken(user._id.toString()); 309 | 310 | res = await agent 311 | .delete('/api/auth/logout') 312 | .set('Cookie', `access_token=${accessToken}`); 313 | 314 | expect(res.status).to.equal(204); 315 | const setCookieHeader = res.headers['set-cookie']; 316 | if (setCookieHeader) { 317 | const cookies = setCookieHeader.map( 318 | (cookie: string) => cookie.split(';')[0] 319 | ); 320 | expect(cookies).to.not.include('access_token'); 321 | expect(cookies).to.not.include('refresh_token'); 322 | } 323 | }); 324 | 325 | it('should return an error if access token is not provided', async () => { 326 | res = await agent.delete('/api/auth/logout'); 327 | 328 | expect(res.status).to.equal(400); 329 | expect(res.body).to.have.property( 330 | 'message', 331 | 'Please provide access token' 332 | ); 333 | }); 334 | }); 335 | 336 | // delete user after all tests 337 | // after(async () => { 338 | // await User.deleteMany({ email: 'testuser@example.com' }); 339 | // }); 340 | }); 341 | describe('User API', () => { 342 | let user: any; 343 | let accessToken: string; 344 | beforeEach(async () => { 345 | res = await agent.post('/api/auth/login').send({ 346 | email: 'testuser@example.com', 347 | password: 'password123', 348 | }); 349 | 350 | user = res.body.user; 351 | accessToken = res.body.accessToken; 352 | }); 353 | afterEach(function () { 354 | const errorBody = res && res.body; 355 | if (this.currentTest.state === 'failed' && errorBody) { 356 | console.debug('res: ', errorBody); 357 | } 358 | 359 | res = null; 360 | }); 361 | 362 | describe('GET /api/users/me', () => { 363 | it('should return the current user', async () => { 364 | res = await agent 365 | .get('/api/users/me') 366 | .set('Cookie', `access_token=${accessToken}`); 367 | 368 | expect(res.status).to.equal(200); 369 | expect(res.body).to.have.property('user'); 370 | expect(res.body.user).to.have.property( 371 | '_id', 372 | user._id.toString().toString() 373 | ); 374 | expect(res.body.user).to.have.property('name', user.name); 375 | expect(res.body.user).to.have.property('email', user.email); 376 | }); 377 | }); 378 | 379 | describe('PATCH /api/users/me', () => { 380 | it('should return an error if the user tries to update their password', async () => { 381 | res = await agent 382 | .patch('/api/users/me') 383 | .set('Cookie', `access_token=${accessToken}`) 384 | .send({ 385 | password: 'newpassword123', 386 | passwordConfirm: 'newpassword123', 387 | }); 388 | 389 | expect(res.status).to.equal(400); 390 | expect(res.body).to.have.property( 391 | 'message', 392 | 'This route is not for password updates. Please use /updateMyPassword' 393 | ); 394 | }); 395 | 396 | it('should return an error if the user tries to update their role', async () => { 397 | res = await agent 398 | .patch('/api/users/me') 399 | .set('Cookie', `access_token=${accessToken}`) 400 | .send({ 401 | roles: ['admin'], 402 | }); 403 | 404 | expect(res.status).to.equal(400); 405 | expect(res.body).to.have.property( 406 | 'message', 407 | 'This route is not for role updates. Please use /updateRole' 408 | ); 409 | }); 410 | 411 | it('should return an error if the user provides invalid data', async () => { 412 | res = await agent 413 | .patch('/api/users/me') 414 | .set('Cookie', `access_token=${accessToken}`) 415 | .send({ 416 | email: 'invalidemail', 417 | }); 418 | 419 | expect(res.status).to.equal(400); 420 | expect(res.body).to.have.property( 421 | 'message', 422 | 'Validation failed: email: Please provide a valid email' 423 | ); 424 | }); 425 | 426 | it('should update the current user', async () => { 427 | res = await agent 428 | .patch('/api/users/me') 429 | .set('Cookie', `access_token=${accessToken}`) 430 | .send({ 431 | name: 'Updated Name', 432 | }); 433 | 434 | expect(res.status).to.equal(200); 435 | expect(res.body).to.have.property('doc'); 436 | expect(res.body.doc).to.have.property( 437 | '_id', 438 | user._id.toString().toString() 439 | ); 440 | expect(res.body.doc).to.have.property('name', 'Updated Name'); 441 | 442 | // Check if the user was updated in the database 443 | const updatedUser = await User.findById(user._id.toString()); 444 | expect(updatedUser).to.have.property('name', 'Updated Name'); 445 | }); 446 | }); 447 | describe('DELETE /api/users/me', () => { 448 | it('should delete the current user', async () => { 449 | res = await agent 450 | .delete('/api/users/me') 451 | .set('Cookie', `access_token=${accessToken}`); 452 | 453 | expect(res.status).to.equal(204); 454 | 455 | // Check if the user was deleted from the database 456 | const deletedUser = await User.findById(user._id.toString()); 457 | expect(deletedUser).to.not.exist; 458 | }); 459 | }); 460 | }); 461 | -------------------------------------------------------------------------------- /backend-app/tests/env.test.ts: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test'; 2 | -------------------------------------------------------------------------------- /backend-app/tests/init/x.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'mocha'; 2 | 3 | describe('Your test suite', () => { 4 | it('Your test case', () => { 5 | // Your test assertion goes here 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /backend-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "outDir": "build", 6 | "baseUrl": ".", 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | 10 | "noImplicitAny": true, 11 | "noImplicitThis": true, 12 | "alwaysStrict": true, 13 | 14 | "noUnusedLocals": true, 15 | "noImplicitReturns": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "noUncheckedIndexedAccess": true, 18 | 19 | "moduleResolution": "node", 20 | "resolveJsonModule": true, 21 | 22 | "experimentalDecorators": true, 23 | "emitDecoratorMetadata": true, 24 | 25 | "sourceMap": true, 26 | "allowJs": true, 27 | "allowUnreachableCode": false, 28 | 29 | "paths": { 30 | "@root/*": ["./*"], 31 | "@config/*": ["./config/*"], 32 | "@utils/*": ["./utils/*"], 33 | "@models/*": ["./models/*"], 34 | "@controllers/*": ["./controllers/*"], 35 | "@middlewares/*": ["./middlewares/*"], 36 | "@constants/*": ["./constants/*"], 37 | "@interfaces/*": ["./interfaces/*"], 38 | "tsoa": ["./node_modules/tsoa"] 39 | }, 40 | "typeRoots": ["./typings"] 41 | }, 42 | "include": ["./**/*"], 43 | "exclude": ["node_modules", "build"] 44 | } 45 | -------------------------------------------------------------------------------- /backend-app/tsoa.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryFile": "./server.ts", 3 | "noImplicitAdditionalProperties": "throw-on-extras", 4 | "controllerPathGlobs": ["./controllers/**/*.ts"], 5 | "spec": { 6 | "outputDirectory": "./docs/api_docs/", 7 | "specVersion": 2, 8 | "specValidation": false 9 | }, 10 | "routes": { 11 | "routesDir": "routes" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend-app/typings/express-serve-static-core.d.ts: -------------------------------------------------------------------------------- 1 | import { Express } from 'express-serve-static-core'; 2 | import { ObjectId } from 'mongoose'; 3 | declare module 'express-serve-static-core' { 4 | interface Request { 5 | user: 6 | | { 7 | _id: ObjectId; 8 | name?: string; 9 | email: string; 10 | roles?: string[]; 11 | authorities?: string[]; 12 | restrictions?: string[]; 13 | active: boolean; 14 | githubOauthAccessToken?: string; 15 | } 16 | | undefined; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend-app/typings/xss-clean.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'xss-clean' { 2 | const value: Function; 3 | 4 | export default value; 5 | } 6 | -------------------------------------------------------------------------------- /backend-app/utils/api_features.ts: -------------------------------------------------------------------------------- 1 | import { Document, Query } from 'mongoose'; // Import appropriate types for your use case 2 | 3 | class APIFeatures { 4 | query: Query; 5 | queryString: Record; 6 | constructor( 7 | query: Query, 8 | queryString: Record 9 | ) { 10 | this.query = query; 11 | this.queryString = queryString; 12 | } 13 | 14 | sort(): this { 15 | if (this.queryString.sort) { 16 | const sortBy = (this.queryString.sort as string) 17 | .split(',') 18 | .join(' '); 19 | this.query = this.query.sort(sortBy); 20 | } 21 | return this; 22 | } 23 | 24 | paginate(): this { 25 | const page = Number(this.queryString.page) || 1; 26 | const limit = Number(this.queryString.limit) || 10; 27 | const skip = (page - 1) * limit; 28 | 29 | this.query = this.query.skip(skip).limit(limit); 30 | return this; 31 | } 32 | 33 | // Field Limiting ex: -----/user?fields=name,email,address 34 | limitFields(): this { 35 | if (this.queryString.fields) { 36 | const fields = (this.queryString.fields as string) 37 | .split(',') 38 | .join(' '); 39 | this.query = this.query.select(fields); 40 | } 41 | return this; 42 | } 43 | } 44 | 45 | export default APIFeatures; 46 | -------------------------------------------------------------------------------- /backend-app/utils/app_error.ts: -------------------------------------------------------------------------------- 1 | export default class AppError extends Error { 2 | statusCode: Number; 3 | status: string; 4 | path: any; 5 | constructor(statusCode: Number, message: string, path: any = false) { 6 | super(message); 7 | this.statusCode = statusCode; 8 | this.message = message; 9 | this.path = !path ? false : path; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend-app/utils/authorization/auth_utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ACCESS_TOKEN_SECRET, 3 | ACCESS_TOKEN_EXPIRY_TIME, 4 | REFRESH_TOKEN_SECRET, 5 | REFRESH_TOKEN_EXPIRY_TIME, 6 | ACCESS_TOKEN_COOKIE_EXPIRY_TIME, 7 | REFRESH_TOKEN_COOKIE_EXPIRY_TIME, 8 | } from '@config/app_config'; 9 | import AppError from '@utils/app_error'; 10 | import jwt from 'jsonwebtoken'; 11 | import { promisify } from 'util'; 12 | 13 | class AuthUtils { 14 | static generateAccessToken(_id: string): string { 15 | return jwt.sign({ _id }, ACCESS_TOKEN_SECRET, { 16 | expiresIn: ACCESS_TOKEN_EXPIRY_TIME, 17 | }); 18 | } 19 | static generateRefreshToken(_id: string): string { 20 | return jwt.sign({ _id }, REFRESH_TOKEN_SECRET, { 21 | expiresIn: REFRESH_TOKEN_EXPIRY_TIME, 22 | }); 23 | } 24 | static setAccessTokenCookie(res: any, accessToken: string): AuthUtils { 25 | res.cookie('access_token', accessToken, { 26 | secure: true, 27 | sameSite: 'strict', 28 | maxAge: ACCESS_TOKEN_COOKIE_EXPIRY_TIME, 29 | }); 30 | return this; 31 | } 32 | static setRefreshTokenCookie(res: any, refreshToken: string): AuthUtils { 33 | res.cookie('refresh_token', refreshToken, { 34 | httpOnly: true, 35 | secure: true, 36 | sameSite: 'strict', 37 | maxAge: REFRESH_TOKEN_COOKIE_EXPIRY_TIME, 38 | }); 39 | return this; 40 | } 41 | static async verifyAccessToken(token: string): Promise { 42 | const result = await promisify(jwt.verify.bind(jwt))( 43 | token, 44 | ACCESS_TOKEN_SECRET 45 | ); 46 | return result; 47 | } 48 | static async verifyRefreshToken(token: string): Promise { 49 | try { 50 | return await promisify(jwt.verify.bind(jwt))( 51 | token, 52 | REFRESH_TOKEN_SECRET 53 | ); 54 | } catch (error) { 55 | throw new AppError(400, 'Invalid refresh token'); 56 | } 57 | } 58 | } 59 | export default AuthUtils; 60 | -------------------------------------------------------------------------------- /backend-app/utils/authorization/generate_tokens.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ACCESS_TOKEN_SECRET, 3 | ACCESS_TOKEN_EXPIRY_TIME, 4 | REFRESH_TOKEN_SECRET, 5 | REFRESH_TOKEN_EXPIRY_TIME, 6 | } from '@config/app_config'; 7 | import AppError from '@utils/app_error'; 8 | import jwt from 'jsonwebtoken'; 9 | 10 | const generateTokens = (id: string) => { 11 | try { 12 | const accessToken = jwt.sign({ id }, ACCESS_TOKEN_SECRET, { 13 | expiresIn: ACCESS_TOKEN_EXPIRY_TIME, 14 | }); 15 | const refreshToken = jwt.sign({ id }, REFRESH_TOKEN_SECRET, { 16 | expiresIn: REFRESH_TOKEN_EXPIRY_TIME, 17 | }); 18 | if (!accessToken || !refreshToken) 19 | throw new AppError( 20 | 500, 21 | 'Something went wrong. Please try again later.' 22 | ); 23 | return { accessToken, refreshToken }; 24 | } catch (err) { 25 | throw new AppError( 26 | 500, 27 | 'Something went wrong. Please try again later.' 28 | ); 29 | } 30 | }; 31 | export default generateTokens; 32 | -------------------------------------------------------------------------------- /backend-app/utils/authorization/github.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OAUTH_CLIENT_ID_GITHUB, 3 | OAUTH_CLIENT_SECRET_GITHUB, 4 | } from '@config/app_config'; 5 | import qs from 'qs'; 6 | import axios from 'axios'; 7 | import AppError from '@utils/app_error'; 8 | 9 | export const getGithubOAuthToken = async ( 10 | code: string 11 | ): Promise<{ access_token: string }> => { 12 | const rootUrl = 'https://github.com/login/oauth/access_token'; 13 | 14 | const queryString = qs.stringify({ 15 | client_id: OAUTH_CLIENT_ID_GITHUB, 16 | client_secret: OAUTH_CLIENT_SECRET_GITHUB, 17 | code, 18 | }); 19 | try { 20 | const { data } = await axios.post(`${rootUrl}?${queryString}`, null, { 21 | headers: { 22 | 'Content-Type': 'application/x-www-form-urlencoded', 23 | }, 24 | }); 25 | 26 | const decoded = qs.parse(data); 27 | 28 | return decoded as unknown as Promise<{ access_token: string }>; 29 | } catch (err) { 30 | throw new AppError(400, 'Invalid code'); 31 | } 32 | }; 33 | export const getGithubOAuthUser = async (access_token: string) => { 34 | try { 35 | const { data } = await axios.get('https://api.github.com/user', { 36 | headers: { 37 | Authorization: `Bearer ${access_token}`, 38 | }, 39 | }); 40 | 41 | return data; 42 | } catch (err) { 43 | throw new AppError(400, 'Invalid access token'); 44 | } 45 | }; 46 | export const getGithubOAuthUserPrimaryEmail = async (access_token: string) => { 47 | try { 48 | const { data } = await axios.get('https://api.github.com/user/emails', { 49 | headers: { 50 | Authorization: `Bearer ${access_token}`, 51 | }, 52 | }); 53 | const primaryEmail = data.find((email: any) => email.primary === true); 54 | return primaryEmail.email; 55 | } catch (err) { 56 | throw new AppError(400, 'Invalid access token'); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /backend-app/utils/authorization/roles/create_roles.ts: -------------------------------------------------------------------------------- 1 | import Actions from '@constants/actions'; 2 | import Role from './role'; 3 | import logger from '@utils/logger'; 4 | 5 | interface RoleType { 6 | type: string; 7 | authorities: string[]; 8 | restrictions: string[]; 9 | } 10 | 11 | const superAdmin: RoleType = { 12 | type: 'SUPER_ADMIN', 13 | authorities: Object.values(Actions), 14 | restrictions: [], 15 | }; 16 | const admin: RoleType = { 17 | type: 'ADMIN', 18 | authorities: [Actions.BAN_USER, Actions.DELETE_USER], 19 | restrictions: [], 20 | }; 21 | const user: RoleType = { 22 | type: 'USER', 23 | authorities: [Actions.UPDATE_CALANDER], 24 | restrictions: [], 25 | }; 26 | 27 | const createRoles = async (): Promise => { 28 | const roles = await Role.getRoles(); 29 | const roleArr = Object.keys(roles); 30 | try { 31 | if ( 32 | roleArr.length > 2 && 33 | roleArr.includes(superAdmin.type) && 34 | roleArr.includes(admin.type) && 35 | roleArr.includes(user.type) 36 | ) 37 | return; 38 | await Role.createRole( 39 | superAdmin.type, 40 | superAdmin.authorities, 41 | superAdmin.restrictions 42 | ); 43 | await Role.createRole( 44 | admin.type, 45 | admin.authorities, 46 | admin.restrictions 47 | ); 48 | await Role.createRole(user.type, user.authorities, user.restrictions); 49 | logger.info( 50 | `[ ${superAdmin.type}, ${admin.type}, ${user.type} ] ROLES CREATED!` 51 | ); 52 | // logger.warn(await role.getRoles()); 53 | } catch (err) { 54 | logger.error(err.stack); 55 | } 56 | }; 57 | export default createRoles; 58 | -------------------------------------------------------------------------------- /backend-app/utils/authorization/roles/role.ts: -------------------------------------------------------------------------------- 1 | import AppError from '@utils/app_error'; 2 | import roleModel from '@models/user/role_model'; 3 | import Actions from '@constants/actions'; 4 | 5 | interface RoleData { 6 | name: string; 7 | authorities: string[]; 8 | restrictions: string[]; 9 | } 10 | 11 | function isRoleName(roleName: any): void { 12 | if (!roleName || typeof roleName !== 'string') 13 | throw new AppError(400, 'Invalid role name!'); 14 | } 15 | 16 | /** 17 | * Get all roles from database. 18 | */ 19 | async function getRoles(): Promise<{ [key: string]: RoleData }> { 20 | const data: { [key: string]: RoleData } = {}; 21 | const roles = await roleModel.find(); 22 | 23 | roles.forEach((role) => { 24 | data[role.name] = { 25 | name: role.name, 26 | authorities: role.authorities, 27 | restrictions: role.restrictions, 28 | }; 29 | }); 30 | return data; 31 | } 32 | 33 | /** 34 | * Get role by name from database. 35 | * @param {string} roleName 36 | * @returns {Promise} 37 | */ 38 | async function getRoleByName(roleName: string): Promise { 39 | isRoleName(roleName); 40 | const role = await roleModel.findOne({ name: roleName }); 41 | if (!role) return null; 42 | return { 43 | name: role.name, 44 | authorities: role.authorities, 45 | restrictions: role.restrictions, 46 | }; 47 | } 48 | 49 | /** 50 | * Delete role by name from database. 51 | * @param {string} roleName 52 | * @returns {Promise} 53 | */ 54 | async function deleteRoleByName(roleName: string): Promise { 55 | isRoleName(roleName); 56 | const role = await getRoleByName(roleName); 57 | if (!role) throw new AppError(404, 'Role not found!'); 58 | const deletedRole = await roleModel.findOneAndDelete( 59 | { name: roleName }, 60 | { new: true } 61 | ); 62 | if (!deletedRole) throw new AppError(404, 'Role not found!'); 63 | return { 64 | name: deletedRole.name, 65 | authorities: deletedRole.authorities, 66 | restrictions: deletedRole.restrictions, 67 | }; 68 | } 69 | 70 | /** 71 | * Create role by name from database. 72 | * @param {string} roleName 73 | * @param {string[]} authorities 74 | * @param {string[]} restrictions 75 | * @returns {Promise} 76 | */ 77 | async function createRole( 78 | roleName: string, 79 | authorities: string[] = [], 80 | restrictions: string[] = [] 81 | ): Promise { 82 | isRoleName(roleName); 83 | if (await getRoleByName(roleName)) 84 | throw new AppError(400, 'Role already exists'); 85 | authorities.forEach((authority) => { 86 | if (!Object.values(Actions).includes(authority)) 87 | throw new AppError(400, `Invalid authority ${authority}`); 88 | }); 89 | restrictions.forEach((restriction) => { 90 | if (!Object.values(Actions).includes(restriction)) 91 | throw new AppError(400, `Invalid restriction ${restriction}`); 92 | }); 93 | const role = await roleModel.create({ 94 | name: roleName, 95 | authorities, 96 | restrictions, 97 | }); 98 | return { 99 | name: role.name, 100 | authorities: role.authorities, 101 | restrictions: role.restrictions, 102 | }; 103 | } 104 | 105 | /** 106 | * Delete default roles from database. 107 | */ 108 | async function deleteDefaultRoles(): Promise { 109 | await roleModel.deleteMany({ 110 | name: { $in: ['SUPER_ADMIN', 'ADMIN', 'USER'] }, 111 | }); 112 | } 113 | 114 | /** 115 | * 116 | * @param {string} roleName 117 | * @param {string[]} authorities 118 | * @param {string[]} restrictions 119 | */ 120 | async function updateRoleByName( 121 | roleName: string, 122 | authorities: string[] = [], 123 | restrictions: string[] = [] 124 | ): Promise { 125 | authorities.forEach((authority) => { 126 | if (!Object.values(Actions).includes(authority)) 127 | throw new AppError(400, `Invalid authority ${authority}`); 128 | }); 129 | restrictions.forEach((restriction) => { 130 | if (!Object.values(Actions).includes(restriction)) 131 | throw new AppError(400, `Invalid restriction ${restriction}`); 132 | }); 133 | const exists = await getRoleByName(roleName); 134 | if (!exists) throw new AppError(404, 'Role does not exist'); 135 | 136 | const updatedRole = await roleModel.findOneAndUpdate( 137 | { 138 | name: roleName, 139 | }, 140 | { 141 | authorities: Array.from( 142 | new Set([...exists.authorities, ...authorities]) 143 | ), 144 | restrictions: Array.from( 145 | new Set([...exists.restrictions, ...restrictions]) 146 | ), 147 | } 148 | ); 149 | if (!updatedRole) throw new AppError(404, 'Role not found!'); 150 | return { 151 | name: updatedRole.name, 152 | authorities: updatedRole.authorities, 153 | restrictions: updatedRole.restrictions, 154 | }; 155 | } 156 | 157 | export default { 158 | getRoles, 159 | getRoleByName, 160 | deleteRoleByName, 161 | createRole, 162 | deleteDefaultRoles, 163 | updateRoleByName, 164 | }; 165 | -------------------------------------------------------------------------------- /backend-app/utils/authorization/validate_actions.ts: -------------------------------------------------------------------------------- 1 | import Actions from '@constants/actions'; 2 | 3 | /** 4 | * Validate all the actions in the array 5 | * @param {string[]} actions 6 | * @returns boolean 7 | */ 8 | const validateActions = (actions: string[]): boolean => 9 | actions.every((action) => Object.values(Actions).includes(action)); 10 | 11 | export default validateActions; 12 | -------------------------------------------------------------------------------- /backend-app/utils/create_default_user.ts: -------------------------------------------------------------------------------- 1 | import User from '@models/user/user_model'; 2 | import { ADMIN_EMAIL, ADMIN_PASSWORD } from '@config/app_config'; 3 | import logger from '@utils/logger'; 4 | import Role from '@utils/authorization/roles/role'; 5 | 6 | const createAdminUser = async () => { 7 | try { 8 | const user = await User.findOne({ email: ADMIN_EMAIL }); 9 | if (!user) { 10 | const roles = await Role.getRoles(); 11 | await User.create({ 12 | name: 'Supper Admin', 13 | email: ADMIN_EMAIL, 14 | password: ADMIN_PASSWORD, 15 | roles: [roles.SUPER_ADMIN.name], 16 | authorities: roles.SUPER_ADMIN.authorities, 17 | restrictions: roles.SUPER_ADMIN.restrictions, 18 | active: true, 19 | }); 20 | } 21 | } catch (err) { 22 | logger.error(err.stack); 23 | } 24 | }; 25 | 26 | export default createAdminUser; 27 | -------------------------------------------------------------------------------- /backend-app/utils/logger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description - This file contains the configuration for the logger 3 | */ 4 | import { createLogger, transports } from 'winston'; 5 | import DailyRotateFile from 'winston-daily-rotate-file'; 6 | import { fileOptions, consoleOptions } from '@config/logger_config'; 7 | 8 | // Define the transport for the logger 9 | const consoleTransport = new transports.Console(consoleOptions); 10 | const fileTransport = new DailyRotateFile(fileOptions); 11 | 12 | const logger = createLogger({ 13 | transports: [consoleTransport, fileTransport], // add the transport to the logger 14 | exceptionHandlers: [fileTransport], 15 | exitOnError: false, // do not exit on handled exceptions 16 | }); 17 | // Export the logger 18 | export default logger; 19 | -------------------------------------------------------------------------------- /backend-app/utils/register_paths.ts: -------------------------------------------------------------------------------- 1 | import * as tsConfigPaths from 'tsconfig-paths'; 2 | import * as tsConfig from '../tsconfig.json'; 3 | import * as path from 'path'; 4 | 5 | const baseUrl = path.join(__dirname, '../'); 6 | tsConfigPaths.register({ 7 | baseUrl, 8 | paths: tsConfig.compilerOptions.paths, 9 | }); 10 | -------------------------------------------------------------------------------- /backend-app/utils/searchCookie.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | 3 | const searchCookies = ( 4 | req: Request, 5 | cookieName: string 6 | ): string | undefined => { 7 | const cookies = req.cookies; 8 | if (!cookies || !cookies[cookieName]) { 9 | return undefined; 10 | } 11 | return cookies[cookieName]; 12 | }; 13 | 14 | export default searchCookies; 15 | -------------------------------------------------------------------------------- /backend-app/utils/swagger/index.ts: -------------------------------------------------------------------------------- 1 | import { CURRENT_ENV } from '@config/app_config'; 2 | import { IRes } from '@interfaces/vendors'; 3 | import swaggerUi from 'swagger-ui-express'; 4 | import * as swaggerjson from '@root/docs/api_docs/swagger.json'; 5 | 6 | /** 7 | * This function configures the swagger documentation 8 | * @param { application } app - The express application 9 | * @returns {void} 10 | */ 11 | const swaggerDocs = (app: any): void => { 12 | if (CURRENT_ENV === 'production') return; 13 | app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerjson)); 14 | // Get docs in JSON format 15 | app.get('/docs-json', (_: any, res: IRes) => { 16 | res.setHeader('Content-Type', 'application/json'); 17 | res.send(swaggerjson); 18 | }); 19 | }; 20 | 21 | export default swaggerDocs; 22 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | backend-api: 3 | build: ./backend-app 4 | ports: 5 | - "8000:8000" 6 | frontend: 7 | build: ./frontend-app 8 | ports: 9 | - "3000:3000" 10 | depends_on: 11 | - backend-api -------------------------------------------------------------------------------- /frontend-app/.dockerignore: -------------------------------------------------------------------------------- 1 | # Include any files or directories that you don't want to be copied to your 2 | # container here (e.g., local build artifacts, temporary files, etc.). 3 | # 4 | # For more help, visit the .dockerignore file reference guide at 5 | # https://docs.docker.com/engine/reference/builder/#dockerignore-file 6 | 7 | **/.classpath 8 | **/.dockerignore 9 | **/.env 10 | **/.git 11 | **/.gitignore 12 | **/.project 13 | **/.settings 14 | **/.toolstarget 15 | **/.vs 16 | **/.vscode 17 | **/.next 18 | **/.cache 19 | **/*.*proj.user 20 | **/*.dbmdl 21 | **/*.jfm 22 | **/charts 23 | **/docker-compose* 24 | **/compose* 25 | **/Dockerfile* 26 | **/node_modules 27 | **/npm-debug.log 28 | **/obj 29 | **/secrets.dev.yaml 30 | **/values.dev.yaml 31 | **/build 32 | **/dist 33 | LICENSE 34 | README.md 35 | -------------------------------------------------------------------------------- /frontend-app/.env.local: -------------------------------------------------------------------------------- 1 | DATA_API_KEY = workflow team 2 | DATA_SOURCE_URL = http://localhost:5000/api/v1/auth/ 3 | NODE_ENV = production -------------------------------------------------------------------------------- /frontend-app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /frontend-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | # .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /frontend-app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | WORKDIR /app 3 | 4 | COPY package*.json . 5 | RUN npm ci 6 | 7 | COPY . . 8 | 9 | RUN npm run build 10 | 11 | EXPOSE 3000 12 | 13 | ENV PORT 3000 14 | 15 | CMD ["npm", "start"] 16 | -------------------------------------------------------------------------------- /frontend-app/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 18 | 19 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /frontend-app/mui-icons.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@mui/icons-material' { 2 | export * from '@mui/icons-material'; 3 | } -------------------------------------------------------------------------------- /frontend-app/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /frontend-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@emotion/react": "^11.11.1", 13 | "@emotion/styled": "^11.11.0", 14 | "@fortawesome/fontawesome-svg-core": "^6.4.2", 15 | "@fortawesome/free-brands-svg-icons": "^6.4.2", 16 | "@fortawesome/free-solid-svg-icons": "^6.4.2", 17 | "@fortawesome/react-fontawesome": "^0.2.0", 18 | "@mui/icons-material": "^5.14.3", 19 | "@mui/material": "^5.14.3", 20 | "@types/node": "20.3.1", 21 | "@types/react": "18.2.13", 22 | "@types/react-dom": "18.2.6", 23 | "autoprefixer": "10.4.14", 24 | "dayjs": "^1.11.9", 25 | "eslint": "8.43.0", 26 | "eslint-config-next": "13.4.6", 27 | "next": "13.5.4", 28 | "postcss": "8.4.31", 29 | "react": "18.2.0", 30 | "react-dom": "18.2.0", 31 | "react-icons": "^4.10.1", 32 | "react-router-dom": "^6.14.2", 33 | "tailwind-merge": "^1.14.0", 34 | "tailwindcss": "3.3.2", 35 | "typescript": "5.1.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /frontend-app/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend-app/public/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-ninja11/planemaker/2f778de23c977ddadf968d2905ee2706c121afd1/frontend-app/public/logo.jpg -------------------------------------------------------------------------------- /frontend-app/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend-app/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend-app/src/app/calendar/loading.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | const Loading = () => { 4 | return ( 5 |
6 |
7 |

Loading...

8 |

This may take a few seconds, please dont close this page.

9 |
10 | ); 11 | } 12 | 13 | export default Loading; 14 | -------------------------------------------------------------------------------- /frontend-app/src/app/calendar/page.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/rules-of-hooks */ 2 | "use client"; 3 | 4 | // import NavBar from "@/components/NavBar"; 5 | import Link from "next/link"; 6 | import React, { Suspense,useState } from "react"; 7 | import { GrFormNext, GrFormPrevious } from "react-icons/gr"; 8 | import { cn } from "@/lib/utils" 9 | import { generateDate, months } from "@/components/ui/calendar"; 10 | import dayjs from "dayjs"; 11 | import AddTaskIcon from '@mui/icons-material/AddTask'; 12 | 13 | export default function page() { 14 | 15 | const days = ["S", "M", "T", "W", "T", "F", "S"]; 16 | const currentDate = dayjs(); 17 | const [today, setToday] = useState(currentDate); 18 | const [selectDate, setSelectDate] = useState(currentDate); 19 | 20 | return ( 21 |
22 | {/* 23 | Loading feed...

}> 24 |

hiiiii

25 |
*/} 26 | 27 |
28 |
Organize Your Life
29 | 33 |

34 | 35 |
36 |
37 |
38 |

39 | {months[today.month()]}, {today.year()} 40 |

41 |
42 | { 45 | setToday(today.month(today.month() - 1)); 46 | }} 47 | /> 48 |

{ 51 | setToday(currentDate); 52 | }} 53 | > 54 | Today 55 |

56 | { 59 | setToday(today.month(today.month() + 1)); 60 | }} 61 | /> 62 |
63 |
64 |
65 | {days.map((day, index) => { 66 | return ( 67 |

71 | {day} 72 |

73 | ); 74 | })} 75 |
76 | 77 |
78 | {generateDate(today.month(), today.year()).map( 79 | ({ date, currentMonth, today }, index) => { 80 | return ( 81 |
85 |

{ 100 | setSelectDate(date); 101 | }} 102 | > 103 | {date.date()} 104 |

105 |
106 | ); 107 | } 108 | )} 109 |
110 |
111 |
112 |

113 | Schedule for {selectDate.toDate().toDateString()} 114 |

115 |

No meetings for today.

116 |
117 |
118 |
119 | ); 120 | } -------------------------------------------------------------------------------- /frontend-app/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-ninja11/planemaker/2f778de23c977ddadf968d2905ee2706c121afd1/frontend-app/src/app/favicon.ico -------------------------------------------------------------------------------- /frontend-app/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | } 7 | 8 | @media (prefers-color-scheme: dark) { 9 | :root { 10 | } 11 | } 12 | 13 | body { 14 | } 15 | 16 | .loader { 17 | border-top-color: #3498db; 18 | -webkit-animation: spinner 1.5s linear infinite; 19 | animation: spinner 1.5s linear infinite; 20 | } 21 | 22 | @-webkit-keyframes spinner { 23 | 0% { 24 | -webkit-transform: rotate(0deg); 25 | } 26 | 100% { 27 | -webkit-transform: rotate(360deg); 28 | } 29 | } 30 | 31 | @keyframes spinner { 32 | 0% { 33 | transform: rotate(0deg); 34 | } 35 | 100% { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | 40 | /* :root { 41 | --max-width: 1100px; 42 | --border-radius: 12px; 43 | --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 44 | 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 45 | 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; 46 | 47 | --foreground-rgb: 0, 0, 0; 48 | --background-start-rgb: 214, 219, 220; 49 | --background-end-rgb: 255, 255, 255; 50 | 51 | --primary-glow: conic-gradient( 52 | from 180deg at 50% 50%, 53 | #16abff33 0deg, 54 | #0885ff33 55deg, 55 | #54d6ff33 120deg, 56 | #0071ff33 160deg, 57 | transparent 360deg 58 | ); 59 | --secondary-glow: radial-gradient( 60 | rgba(255, 255, 255, 1), 61 | rgba(255, 255, 255, 0) 62 | ); 63 | 64 | --tile-start-rgb: 239, 245, 249; 65 | --tile-end-rgb: 228, 232, 233; 66 | --tile-border: conic-gradient( 67 | #00000080, 68 | #00000040, 69 | #00000030, 70 | #00000020, 71 | #00000010, 72 | #00000010, 73 | #00000080 74 | ); 75 | 76 | --callout-rgb: 238, 240, 241; 77 | --callout-border-rgb: 172, 175, 176; 78 | --card-rgb: 180, 185, 188; 79 | --card-border-rgb: 131, 134, 135; 80 | } 81 | 82 | @media (prefers-color-scheme: dark) { 83 | :root { 84 | --foreground-rgb: 255, 255, 255; 85 | --background-start-rgb: 0, 0, 0; 86 | --background-end-rgb: 0, 0, 0; 87 | 88 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); 89 | --secondary-glow: linear-gradient( 90 | to bottom right, 91 | rgba(1, 65, 255, 0), 92 | rgba(1, 65, 255, 0), 93 | rgba(1, 65, 255, 0.3) 94 | ); 95 | 96 | --tile-start-rgb: 2, 13, 46; 97 | --tile-end-rgb: 2, 5, 19; 98 | --tile-border: conic-gradient( 99 | #ffffff80, 100 | #ffffff40, 101 | #ffffff30, 102 | #ffffff20, 103 | #ffffff10, 104 | #ffffff10, 105 | #ffffff80 106 | ); 107 | 108 | --callout-rgb: 20, 20, 20; 109 | --callout-border-rgb: 108, 108, 108; 110 | --card-rgb: 100, 100, 100; 111 | --card-border-rgb: 200, 200, 200; 112 | } 113 | } 114 | 115 | * { 116 | box-sizing: border-box; 117 | padding: 0; 118 | margin: 0; 119 | } 120 | 121 | html, 122 | body { 123 | max-width: 100vw; 124 | overflow-x: hidden; 125 | } 126 | 127 | body { 128 | color: rgb(var(--foreground-rgb)); 129 | background: linear-gradient( 130 | to bottom, 131 | transparent, 132 | rgb(var(--background-end-rgb)) 133 | ) 134 | rgb(var(--background-start-rgb)); 135 | } 136 | 137 | a { 138 | color: inherit; 139 | text-decoration: none; 140 | } 141 | 142 | @media (prefers-color-scheme: dark) { 143 | html { 144 | color-scheme: dark; 145 | } 146 | } */ -------------------------------------------------------------------------------- /frontend-app/src/app/home/page.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/rules-of-hooks */ 2 | "use client"; 3 | 4 | // import NavBar from "@/components/NavBar"; 5 | import Link from "next/link"; 6 | import React from "react"; 7 | 8 | export default function page() { 9 | 10 | return ( 11 | <> 12 | {/* */} 13 | 14 |
15 | {/* add here all what need in home page */} 16 |
17 | 18 | 19 | ); 20 | } -------------------------------------------------------------------------------- /frontend-app/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | import { Inter } from 'next/font/google' 3 | import NavBar from "@/components/NavBar"; 4 | 5 | const inter = Inter({ subsets: ['latin'] }) 6 | 7 | export const metadata = { 8 | title: 'Student Workflow Organizer', 9 | description: 'Generated by Best Team', 10 | } 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: { 15 | children: React.ReactNode 16 | }) { 17 | return ( 18 | 19 | 20 | 21 | {children} 22 | 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /frontend-app/src/app/login/page.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/rules-of-hooks */ 2 | "use client"; 3 | 4 | import Link from "next/link"; 5 | import React, { useState } from "react"; 6 | 7 | export default function page() { 8 | 9 | const DATA_SOURCE_URL: string = process.env.DATA_SOURCE_URL as string 10 | const API_KEY: string = process.env.DATA_API_KEY as string 11 | 12 | const [email, setEmail] = useState(''); 13 | const [password, setPassword] = useState(''); 14 | 15 | const submitHandler = async (e: React.FormEvent) => { 16 | e.preventDefault(); 17 | 18 | if ( !email || !password ) console.log("Missing required data"); 19 | 20 | try { 21 | const res = await fetch(DATA_SOURCE_URL, { 22 | method: 'POST', 23 | headers: { 24 | 'Content-Type': 'application/json', 25 | 'API-Key': API_KEY 26 | }, 27 | body: JSON.stringify({ 28 | email,password 29 | }) 30 | }) 31 | const data = await res.json() 32 | console.log(data) 33 | } catch (error) { 34 | console.log(error) 35 | } 36 | }; 37 | 38 | 39 | return ( 40 |
41 |
42 |
43 |

Login

44 |

Welcome to our plateform

45 |

If you have an account, please login

46 |
47 |
48 | 49 | setEmail(e.target.value)} 55 | autoFocus required/> 56 |
57 | 58 |
59 | 60 | setPassword(e.target.value)} 66 | required/> 67 |
68 | 69 |
70 | Forgot Password? 71 |
72 | 73 | 75 |
76 | 77 |
78 |
79 |

OR

80 |
81 |
82 | 83 | 98 | 99 | 106 | 107 |
108 |

If you dont have an account

109 | Sign Up 110 |
111 |
112 | 113 |
114 | page img 115 |
116 | 117 |
118 |
119 | ); 120 | }; 121 | -------------------------------------------------------------------------------- /frontend-app/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | export default function Home() { 4 | return ( 5 |
6 | Home page 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /frontend-app/src/app/signup/page.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/rules-of-hooks */ 2 | "use client"; 3 | 4 | import Link from "next/link"; 5 | import React, { useState } from "react"; 6 | 7 | export default function page() { 8 | 9 | const DATA_SOURCE_URL: string = process.env.DATA_SOURCE_URL as string 10 | const API_KEY: string = process.env.DATA_API_KEY as string 11 | 12 | const [name, setName] = useState(''); 13 | const [email, setEmail] = useState(''); 14 | const [password, setPassword] = useState(''); 15 | const [passwordConfirm,setPasswordConfirm] = useState(''); 16 | 17 | const submitHandler = async (e: React.FormEvent) => { 18 | e.preventDefault(); 19 | 20 | if ( !name || !email || !password || !passwordConfirm ) console.log("Missing required data"); 21 | 22 | try { 23 | const res = await fetch(DATA_SOURCE_URL, { 24 | method: 'POST', 25 | headers: { 26 | 'Content-Type': 'application/json', 27 | 'API-Key': API_KEY 28 | }, 29 | body: JSON.stringify({ 30 | name,email,password,passwordConfirm 31 | }) 32 | }) 33 | const data = await res.json() 34 | console.log(data) 35 | } catch (error) { 36 | console.log(error) 37 | } 38 | 39 | }; 40 | 41 | return ( 42 |
43 |
44 |
45 |

Sign Up

46 |

Welcome to our plateform

47 |
48 | 49 |
50 | 51 | setName(e.target.value)} 57 | autoFocus required/> 58 |
59 | 60 |
61 | 62 | setEmail(e.target.value)} 68 | autoFocus required/> 69 |
70 | 71 |
72 | 73 | setPassword(e.target.value)} 79 | required/> 80 |
81 | 82 |
83 | 84 | setPasswordConfirm(e.target.value)} 90 | required/> 91 |
92 | 93 | 96 |
97 | 98 |
99 |
100 |
101 |
102 | 103 |
104 |

If you have an account

105 | Login 106 |
107 |
108 | 109 |
110 | page img 111 |
112 | 113 |
114 |
115 | ); 116 | }; 117 | -------------------------------------------------------------------------------- /frontend-app/src/components/NavBar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState } from 'react'; 4 | import Link from "next/link"; 5 | import HomeIcon from '@mui/icons-material/Home'; 6 | import CalendarMonthIcon from '@mui/icons-material/CalendarMonth'; 7 | import SchoolIcon from '@mui/icons-material/School'; 8 | import GroupIcon from '@mui/icons-material/Group'; 9 | import EventIcon from '@mui/icons-material/Event'; 10 | import OverviewIcon from '@mui/icons-material/Event'; 11 | import TaskIcon from '@mui/icons-material/Assignment'; 12 | import EmojiEventsIcon from '@mui/icons-material/EmojiEvents'; 13 | 14 | const NavBar = () => { 15 | const [divHidden, setDivHidden] = useState(true); 16 | const [droplistMekhebi, setDroplistMekhebi] = useState(true); 17 | 18 | // show profile setting 19 | const changeCurrntAction = () => { 20 | setDivHidden((prevIsActive) => !prevIsActive); 21 | }; 22 | 23 | // action for show droplist of overView 24 | const ShowDropList = () =>{ 25 | setDroplistMekhebi((prevIsActive) => !prevIsActive) 26 | } 27 | 28 | return ( 29 | <> 30 |

59 | 60 | 84 | 85 | 148 | 149 | ); 150 | }; 151 | 152 | export default NavBar; 153 | -------------------------------------------------------------------------------- /frontend-app/src/components/ui/calendar.tsx: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | 3 | export const generateDate = ( month = dayjs().month(),year = dayjs().year() ) => { 4 | const firstDateOfMonth = dayjs().year(year).month(month).startOf("month"); 5 | const lastDateOfMonth = dayjs().year(year).month(month).endOf("month"); 6 | 7 | const arrayOfDate = []; 8 | 9 | // create prefix date 10 | for (let i = 0; i < firstDateOfMonth.day(); i++) { 11 | const date = firstDateOfMonth.day(i); 12 | 13 | arrayOfDate.push({ 14 | currentMonth: false, 15 | date, 16 | }); 17 | } 18 | 19 | // generate current date 20 | for (let i = firstDateOfMonth.date(); i <= lastDateOfMonth.date(); i++) { 21 | arrayOfDate.push({ 22 | currentMonth: true, 23 | date: firstDateOfMonth.date(i), 24 | today: 25 | firstDateOfMonth.date(i).toDate().toDateString() === 26 | dayjs().toDate().toDateString(), 27 | }); 28 | } 29 | 30 | const remaining = 42 - arrayOfDate.length; 31 | 32 | for ( 33 | let i = lastDateOfMonth.date() + 1; 34 | i <= lastDateOfMonth.date() + remaining; 35 | i++ 36 | ) { 37 | arrayOfDate.push({ 38 | currentMonth: false, 39 | date: lastDateOfMonth.date(i), 40 | }); 41 | } 42 | return arrayOfDate; 43 | }; 44 | 45 | export const months = [ 46 | "January", 47 | "February", 48 | "March", 49 | "April", 50 | "May", 51 | "June", 52 | "July", 53 | "August", 54 | "September", 55 | "October", 56 | "November", 57 | "December", 58 | ]; -------------------------------------------------------------------------------- /frontend-app/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /frontend-app/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | 3 | const allowedOrigins = process.env.NODE_ENV === 'production' 4 | ? ['http://localhost:5000', 'http://localhost:4000'] 5 | : ['http://localhost:3000'] 6 | 7 | export function middleware(request: Request) { 8 | 9 | const origin = request.headers.get('origin') 10 | console.log(origin) 11 | 12 | if (origin && !allowedOrigins.includes(origin)) { 13 | return new NextResponse(null, { 14 | status: 400, 15 | statusText: "Bad Request", 16 | headers: { 17 | 'Content-Type': 'text/plain' 18 | } 19 | }) 20 | } 21 | 22 | console.log('Middleware!') 23 | 24 | console.log(request.method) 25 | console.log(request.url) 26 | 27 | 28 | 29 | return NextResponse.next() 30 | } 31 | 32 | export const config = { 33 | matcher: '/api/:path*', 34 | } -------------------------------------------------------------------------------- /frontend-app/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 5 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 6 | './src/app/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | theme: { 9 | extend: { 10 | height: { 11 | '128': '41.8rem', 12 | '228': '82%' 13 | }, 14 | backgroundImage: { 15 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 16 | 'gradient-conic': 17 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 18 | }, 19 | }, 20 | }, 21 | plugins: [], 22 | } 23 | -------------------------------------------------------------------------------- /frontend-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /frontend-app/types.d.ts: -------------------------------------------------------------------------------- 1 | type UserData = { 2 | name?: string, 3 | email?: string, 4 | password?: string, 5 | confirmPassword?: string, 6 | } --------------------------------------------------------------------------------