├── .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 |
76 |
77 |
82 |
83 |
98 |
99 |
106 |
107 |
108 |
If you dont have an account
109 |
Sign Up
110 |
111 |
112 |
113 |
114 |

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 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
If you have an account
105 |
Login
106 |
107 |
108 |
109 |
110 |

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 |
61 |
62 |
63 | anounymous tikhit
64 |
65 |
66 | anounymous.tikhit@gmail.com
67 |
68 |
69 |
70 | -
71 | Dashboard
72 |
73 | -
74 | Settings
75 |
76 | -
77 | Calendar
78 |
79 | -
80 | Sign out
81 |
82 |
83 |
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 | }
--------------------------------------------------------------------------------