├── .gitattributes ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── labeler.yml └── workflows │ ├── deploy-docs.yml │ ├── docs-check.yml │ ├── jest-test.yml │ ├── labeler.yml │ └── linter.yml ├── .gitignore ├── .husky └── pre-commit ├── LICENSE ├── README.md ├── backend ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .firebaserc ├── .gitignore ├── .prettierrc.js ├── firebase.json ├── functions │ ├── .gitignore │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── jest.config.js ├── package.json ├── pnpm-lock.yaml ├── scripts │ └── generate-openapi.ts ├── src │ ├── config │ │ ├── app.ts │ │ ├── db.ts │ │ └── sentry.ts │ ├── index.ts │ ├── instrument.ts │ ├── modules │ │ ├── auth │ │ │ ├── classes │ │ │ │ └── validators │ │ │ │ │ ├── AuthValidators.ts │ │ │ │ │ └── index.ts │ │ │ ├── controllers │ │ │ │ ├── AuthController.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── interfaces │ │ │ │ ├── IAuthService.ts │ │ │ │ └── index.ts │ │ │ ├── services │ │ │ │ ├── FirebaseAuthService.ts │ │ │ │ └── index.ts │ │ │ └── tests │ │ │ │ └── AuthController.test.ts │ │ ├── courses │ │ │ ├── classes │ │ │ │ ├── transformers │ │ │ │ │ ├── Course.ts │ │ │ │ │ ├── CourseVersion.ts │ │ │ │ │ ├── Item.ts │ │ │ │ │ ├── Module.ts │ │ │ │ │ ├── Section.ts │ │ │ │ │ └── index.ts │ │ │ │ └── validators │ │ │ │ │ ├── CourseValidators.ts │ │ │ │ │ ├── CourseVersionValidators.ts │ │ │ │ │ ├── ItemValidators.ts │ │ │ │ │ ├── ModuleValidators.ts │ │ │ │ │ ├── SectionValidators.ts │ │ │ │ │ └── index.ts │ │ │ ├── controllers │ │ │ │ ├── CourseController.ts │ │ │ │ ├── CourseVersionController.ts │ │ │ │ ├── ItemController.ts │ │ │ │ ├── ModuleController.ts │ │ │ │ ├── SectionController.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── services │ │ │ │ ├── CourseService.ts │ │ │ │ ├── CourseVersionService.ts │ │ │ │ ├── ItemService.ts │ │ │ │ ├── ModuleService.ts │ │ │ │ ├── SectionService.ts │ │ │ │ └── index.ts │ │ │ ├── tests │ │ │ │ ├── CourseController.test.ts │ │ │ │ ├── CourseVersionController.test.ts │ │ │ │ ├── ItemsController.test.ts │ │ │ │ ├── ModuleController.test.ts │ │ │ │ └── SectionController.test.ts │ │ │ └── utils │ │ │ │ └── calculateNewOrder.ts │ │ ├── docs │ │ │ ├── index.ts │ │ │ └── services │ │ │ │ └── OpenApiSpecService.ts │ │ ├── index.ts │ │ ├── quizzes │ │ │ ├── classes │ │ │ │ ├── transformers │ │ │ │ │ ├── Question.ts │ │ │ │ │ └── index.ts │ │ │ │ └── validators │ │ │ │ │ ├── QuestionValidator.ts │ │ │ │ │ └── index.ts │ │ │ ├── controllers │ │ │ │ ├── QuestionController.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── question-processing │ │ │ │ ├── QuestionProcessor.ts │ │ │ │ ├── index.ts │ │ │ │ ├── renderers │ │ │ │ │ ├── BaseQuestionRenderer.ts │ │ │ │ │ ├── DESQuestionRenderer.ts │ │ │ │ │ ├── NATQuestionRenderer.ts │ │ │ │ │ ├── OTLQuestionRenderer.ts │ │ │ │ │ ├── SMLQuestionRenderer.ts │ │ │ │ │ ├── SOLQuestionRenderer.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── interfaces │ │ │ │ │ │ └── RenderViews.ts │ │ │ │ ├── tag-parser │ │ │ │ │ ├── TagParser.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── tags │ │ │ │ │ │ ├── NumExprTag.ts │ │ │ │ │ │ ├── NumExprTexTag.ts │ │ │ │ │ │ ├── QParamTag.ts │ │ │ │ │ │ └── Tag.ts │ │ │ │ └── validators │ │ │ │ │ ├── BaseQuestionValidator.ts │ │ │ │ │ ├── DESQuestionValidator.ts │ │ │ │ │ ├── NATQuestionValidator.ts │ │ │ │ │ ├── OTLQuestionValidator.ts │ │ │ │ │ ├── SMLQuestionValidator.ts │ │ │ │ │ ├── SOLQuestionValidator.ts │ │ │ │ │ └── index.ts │ │ │ ├── services │ │ │ │ ├── QuestionService.ts │ │ │ │ └── index.ts │ │ │ ├── tests │ │ │ │ └── QuestionController.test.ts │ │ │ └── utils │ │ │ │ └── functions │ │ │ │ └── generateRandomParameterMap.ts │ │ └── users │ │ │ ├── classes │ │ │ ├── transformers │ │ │ │ ├── Enrollment.ts │ │ │ │ ├── Progress.ts │ │ │ │ └── index.ts │ │ │ └── validators │ │ │ │ ├── EnrollmentValidators.ts │ │ │ │ ├── ProgressValidators.ts │ │ │ │ └── index.ts │ │ │ ├── controllers │ │ │ ├── EnrollmentController.ts │ │ │ ├── ProgressController.ts │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── services │ │ │ ├── EnrollmentService.ts │ │ │ ├── ProgressService.ts │ │ │ └── index.ts │ │ │ └── tests │ │ │ ├── EnrollementController.test.ts │ │ │ ├── ProgressController.test.ts │ │ │ ├── common.ts │ │ │ └── utils │ │ │ ├── createCourse.ts │ │ │ ├── createEnrollment.ts │ │ │ ├── createUser.ts │ │ │ ├── startStopAndUpdateProgress.ts │ │ │ └── verifyProgressInDatabase.ts │ ├── shared │ │ ├── constants │ │ │ └── transformerConstants.ts │ │ ├── database │ │ │ ├── index.ts │ │ │ ├── interfaces │ │ │ │ ├── ICourseRepository.ts │ │ │ │ ├── IDatabase.ts │ │ │ │ ├── IItemRepository.ts │ │ │ │ └── IUserRepository.ts │ │ │ └── providers │ │ │ │ ├── MongoDatabaseProvider.ts │ │ │ │ └── mongo │ │ │ │ ├── MongoDatabase.ts │ │ │ │ └── repositories │ │ │ │ ├── CourseRepository.ts │ │ │ │ ├── EnrollmentRepository.ts │ │ │ │ ├── ItemRepository.ts │ │ │ │ ├── ProgressRepository.ts │ │ │ │ └── UserRepository.ts │ │ ├── errors │ │ │ └── errors.ts │ │ ├── functions │ │ │ └── authorizationChecker.ts │ │ ├── interfaces │ │ │ ├── Models.ts │ │ │ └── quiz.ts │ │ ├── middleware │ │ │ ├── errorHandler.ts │ │ │ ├── loggingHandler.ts │ │ │ └── rateLimiter.ts │ │ └── types.ts │ └── utils │ │ ├── env.ts │ │ └── to-bool.ts ├── tests │ └── setup.ts ├── tsconfig.json └── typedoc.json ├── cli ├── package-lock.json ├── package.json ├── src │ ├── cli.ts │ ├── commands │ │ ├── help.ts │ │ ├── setup.ts │ │ ├── start.ts │ │ └── test.ts │ ├── findRoot.ts │ └── steps │ │ ├── env.ts │ │ ├── firebase-emulators.ts │ │ ├── firebase-login.ts │ │ ├── mongodb-binary.ts │ │ └── welcome.ts └── templates │ └── test-file.txt ├── docs ├── .gitignore ├── README.md ├── blog │ ├── 2019-05-28-first-blog-post.md │ ├── 2019-05-29-long-blog-post.md │ ├── 2021-08-01-mdx-blog-post.mdx │ ├── 2021-08-26-welcome │ │ ├── docusaurus-plushie-banner.jpeg │ │ └── index.md │ ├── authors.yml │ └── tags.yml ├── docs │ ├── api │ │ ├── _category_.json │ │ └── backend │ │ │ ├── Auth │ │ │ ├── Errors │ │ │ │ └── auth.ChangePasswordError.md │ │ │ ├── Interfaces │ │ │ │ └── auth.IAuthService.md │ │ │ └── Services │ │ │ │ └── auth.FirebaseAuthService.md │ │ │ ├── Backend.md │ │ │ ├── Courses │ │ │ ├── Transformers │ │ │ │ ├── Item │ │ │ │ │ └── courses.Item.md │ │ │ │ ├── courses.Course.md │ │ │ │ ├── courses.CourseVersion.md │ │ │ │ ├── courses.ItemsGroup.md │ │ │ │ ├── courses.Module.md │ │ │ │ └── courses.Section.md │ │ │ └── Validators │ │ │ │ └── CourseVersionValidators │ │ │ │ ├── courses.DeleteCourseVersionParams.md │ │ │ │ └── courses.DeleteModuleParams.md │ │ │ ├── Other │ │ │ ├── auth.authModuleOptions.md │ │ │ ├── auth.md │ │ │ ├── auth.setupAuthModuleDependencies.md │ │ │ ├── courses.coursesModuleOptions.md │ │ │ ├── courses.md │ │ │ └── courses.setupCoursesModuleDependencies.md │ │ │ └── typedoc-sidebar.cjs │ ├── cli │ │ └── intro.md │ ├── concepts │ │ ├── _category_.json │ │ └── continuos-active-learning.md │ ├── contributing │ │ ├── _category_.json │ │ ├── conventions │ │ │ ├── _category_.json │ │ │ ├── commit-guide.md │ │ │ ├── naming-guide.md │ │ │ └── pr-guide.md │ │ ├── how-to-contribute.md │ │ └── plan.mdx │ ├── development │ │ ├── _category_.json │ │ └── architecture.md │ ├── getting-started │ │ ├── _category_.json │ │ ├── intro.md │ │ └── project-structure.md │ ├── intro.md │ ├── mcp-server │ │ ├── _category_.json │ │ └── setting-up.md │ └── onboarding │ │ ├── _category_.json │ │ ├── common-tasks.md │ │ ├── tech-stack.md │ │ └── workflow.md ├── docusaurus.config.ts ├── newdocs │ ├── Express │ │ ├── 1. Getting Started with Express.md │ │ ├── 10. Dependency Injection.md │ │ ├── 2. Organizing Your Express Project for scalability.md │ │ ├── 3. HTTP Methods & Status Codes.md │ │ ├── 4. Request_Response.md │ │ ├── 5. Routing Controllers.md │ │ ├── 6. Middleware.md │ │ ├── 7. Request Validation.md │ │ ├── 8. MVC Pattern.md │ │ └── 9. Repository Pattern.md │ ├── Github Tutorial │ │ ├── Github Tutorial 1.md │ │ ├── Github Tutorial 2.md │ │ ├── Github Tutorial 3.md │ │ ├── Github Tutorial 4.md │ │ └── Github Tutorial 5.md │ ├── Mongo DB │ │ ├── Aggregation Framework.md │ │ ├── CRUD Operations.md │ │ └── Transactions.md │ ├── React │ │ ├── Advanced State Management with Zustand.md │ │ ├── Bundle Analysis.md │ │ ├── Lazy Loading.md │ │ ├── Memoization.md │ │ ├── Routing.md │ │ ├── State Management in React.md │ │ ├── TSX & Typed Components_.md │ │ ├── TSX & Typed Components_Type Safety.md │ │ ├── Testing & Debugging React Apps with TypeScript.md │ │ └── Zustand Slices and Modular State Architecture.md │ └── Typescript │ │ ├── 1. Introduction to TypeScript │ │ └── 1.1 Introduction to Typescript.md │ │ ├── 10. Conditional Logics │ │ └── Conditional Logic in TypeScript.md │ │ ├── 10. Generics.md │ │ ├── 11. Advanced Types.md │ │ ├── 11. Mastering Loops │ │ └── Mastering Loops in TypeScript.md │ │ ├── 12. Decorators.md │ │ ├── 12. Mastering Functions │ │ └── Mastering Functions in TypeScript.md │ │ ├── 13. Design Patterns.md │ │ ├── 13. Optional and Default Parameters │ │ └── Optional and Default Parameters in TypeScript.md │ │ ├── 14. Dependency Injection.md │ │ ├── 15. IoC Containers & Advanced Dependency Management.md │ │ ├── 2. Basic Syntax │ │ └── 2.1 Basic Syntax in Typescript.md │ │ ├── 3. Variables in TypeScript │ │ └── 3.1 Variables in Typescript.md │ │ ├── 4. let & const │ │ └── 4.1 let & const.md │ │ ├── 5. Any type in TypeScript │ │ └── 5.1 Any type in Typescript.md │ │ ├── 6. Built-in Types │ │ └── 6.1 Built in Types in Typescript.md │ │ ├── 7. User Defined Types │ │ └── 7.1 User defined Types in Typescript.md │ │ ├── 8. Null vs Undefined │ │ └── 8.1 Null vs Undefined.md │ │ ├── 9. Classes & Access Modifiers.md │ │ └── 9. Type Aliases │ │ └── Type Aliases.md ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── sidebars.ts ├── sidebarsNew.ts ├── src │ ├── components │ │ └── HomepageFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.module.css │ │ ├── index.tsx │ │ └── markdown-page.md ├── static │ ├── .nojekyll │ └── img │ │ ├── docusaurus-social-card.jpg │ │ ├── docusaurus.png │ │ ├── favicon.ico │ │ ├── home_one.png │ │ ├── home_three.png │ │ ├── home_two.png │ │ ├── logo.png │ │ ├── logo_old.png │ │ ├── logo_old2.png │ │ ├── undraw_docusaurus_mountain.svg │ │ ├── undraw_docusaurus_react.svg │ │ └── undraw_docusaurus_tree.svg └── tsconfig.json ├── frontend ├── .eslintignore ├── .eslintrc.json ├── .firebaserc ├── .gitignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── components.json ├── eslint.config.js ├── firebase.json ├── index.html ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public │ └── vite.svg ├── src │ ├── app │ │ └── store.tsx │ ├── components │ │ └── ui │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── sidebar.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── textarea.tsx │ │ │ └── tooltip.tsx │ ├── context │ │ └── auth.tsx │ ├── features │ │ └── auth │ │ │ └── auth-slice.ts │ ├── hooks │ │ └── use-mobile.ts │ ├── layouts │ │ ├── components │ │ │ ├── app-sidebar.tsx │ │ │ ├── nav-main.tsx │ │ │ ├── nav-projects.tsx │ │ │ ├── nav-user.tsx │ │ │ └── team-switcher.tsx │ │ └── teacher-layout.tsx │ ├── lib │ │ ├── firebase.ts │ │ └── utils.ts │ ├── main.tsx │ ├── pages │ │ ├── auth-page.tsx │ │ ├── teacher │ │ │ ├── create-article.tsx │ │ │ ├── create-course.tsx │ │ │ └── dashboard.tsx │ │ └── testing-proctoring │ │ │ ├── CameraProcessor.ts │ │ │ ├── face-detector-worker.ts │ │ │ ├── face-detectors.tsx │ │ │ ├── models │ │ │ └── blaze_face_detector.tflite │ │ │ └── useCameraProcessor.ts │ ├── routes │ │ ├── index.tsx │ │ └── teacher-routes.tsx │ ├── styles │ │ └── globals.css │ └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── scripts ├── preload-mongo-binary.ts ├── setup-mcp.ts ├── setup-unix.sh └── setup-win.ps1 └── setup.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Force text files to use LF line endings in the repository 2 | * text=auto 3 | *.sh text eol=lf 4 | *.py text eol=lf 5 | *.js text eol=lf 6 | *.ts text eol=lf 7 | *.html text eol=lf 8 | *.css text eol=lf 9 | 10 | # Specify binary files to ensure they are not affected by line ending changes 11 | *.png binary 12 | *.jpg binary 13 | *.gif binary 14 | *.exe binary 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | frontend: 2 | - changed-files: 3 | - any-glob-to-any-file: ["frontend/**"] 4 | 5 | backend: 6 | - changed-files: 7 | - any-glob-to-any-file: ["backend/**"] 8 | 9 | devops: 10 | - changed-files: 11 | - any-glob-to-any-file: 12 | - ".github/**" 13 | - ".husky/**" 14 | 15 | docs: 16 | - changed-files: 17 | - any-glob-to-any-file: 18 | - "**/README.md" 19 | - "**/docs/**" 20 | 21 | dependencies: 22 | - changed-files: 23 | - any-glob-to-any-file: 24 | - "frontend/package*.json" 25 | - "backend/package*.json" -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Docusaurus to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master # or whichever branch you want to deploy from 7 | workflow_dispatch: # allows manual trigger from the Actions tab 8 | 9 | permissions: 10 | contents: write # Required by actions-gh-pages to push to gh-pages branch 11 | 12 | jobs: 13 | deploy: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 # required for deploying with full git history 21 | 22 | - name: Setup Node.js 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: 18 # Docusaurus recommends 18 LTS (use 22 only if needed) 26 | 27 | - name: Install pnpm 28 | uses: pnpm/action-setup@v2 29 | with: 30 | version: 8 31 | 32 | - name: Install dependencies 33 | run: pnpm install 34 | working-directory: docs # Path to your Docusaurus project 35 | 36 | - name: Build Docusaurus site 37 | run: pnpm run build 38 | working-directory: docs 39 | 40 | - name: Deploy to GitHub Pages 41 | uses: peaceiris/actions-gh-pages@v3 42 | with: 43 | github_token: ${{ secrets.GITHUB_TOKEN }} 44 | publish_dir: docs/build 45 | publish_branch: gh-pages # Default, but good to be explicit 46 | -------------------------------------------------------------------------------- /.github/workflows/docs-check.yml: -------------------------------------------------------------------------------- 1 | name: Docusaurus Pre-checks 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'docs/**' 7 | pull_request: 8 | paths: 9 | - 'docs/**' 10 | # Add manual trigger capability 11 | workflow_dispatch: 12 | inputs: 13 | reason: 14 | description: 'Reason for manual trigger' 15 | required: false 16 | default: 'Manual check of documentation' 17 | 18 | jobs: 19 | docs-build: 20 | name: Check Docusaurus Build 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v3 26 | 27 | - name: Setup Node.js 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: '18' 31 | 32 | - name: Install pnpm 33 | uses: pnpm/action-setup@v2 34 | with: 35 | version: 8 36 | 37 | - name: Get pnpm store directory 38 | id: pnpm-cache 39 | shell: bash 40 | run: | 41 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 42 | 43 | - name: Setup pnpm cache 44 | uses: actions/cache@v3 45 | with: 46 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 47 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 48 | restore-keys: | 49 | ${{ runner.os }}-pnpm-store- 50 | 51 | - name: Install dependencies 52 | working-directory: ./docs 53 | run: pnpm install 54 | 55 | - name: Build Docusaurus site 56 | working-directory: ./docs 57 | run: pnpm build -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Request Labeler" 2 | on: 3 | pull_request_target: 4 | types: [opened, reopened, synchronize] 5 | 6 | 7 | jobs: 8 | triage: 9 | name: Add PR labels 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/labeler@v5 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | sync-labels: true 17 | 18 | - name: Add 'revert' label to relevant PRs 19 | if: contains(github.event.pull_request.title, 'Revert') 20 | uses: actions/github-script@v7 21 | with: 22 | script: | 23 | const { owner, repo } = context.repo; 24 | github.rest.issues.addLabels({ 25 | owner, repo, 26 | issue_number: context.payload.pull_request.number, 27 | labels: ['revert'], 28 | }); 29 | 30 | permissions: 31 | contents: read 32 | issues: write 33 | pull-requests: write 34 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Format Check on Pull Request 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main # Or your main branch name 7 | paths: 8 | - 'backend/**' # Only trigger when files in backend folder are changed 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | lint: 16 | name: Lint Backend 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | 23 | - name: Setup pnpm 24 | uses: pnpm/action-setup@v2 25 | with: 26 | version: 8 # or your pnpm version 27 | 28 | - name: Install dependencies 29 | run: pnpm install 30 | working-directory: backend 31 | 32 | - name: Lint Backend Code 33 | run: pnpm run lint 34 | working-directory: backend -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS Specific 2 | .DS_Store 3 | Thumbs.db 4 | 5 | # Ignore database files anywhere in the project 6 | **/lms_db/ 7 | 8 | 9 | # Ignore credentials folder anywhere 10 | **/credentials/ 11 | 12 | # IDEs and Editors 13 | .idea/ 14 | .vscode/ 15 | !.vscode/settings.json 16 | *.swp 17 | *.swo 18 | 19 | # Logs 20 | *.log 21 | *.tmp 22 | 23 | # Environment Variables 24 | .env 25 | .env.* 26 | 27 | # Python Bytecode 28 | *.pyc 29 | *.pyo 30 | *.pyd 31 | __pycache__/ 32 | 33 | # Node Modules (Frontend) 34 | node_modules/ 35 | 36 | # Docker 37 | *.pid 38 | .env.docker 39 | 40 | # Production Builds 41 | /dist 42 | /build 43 | 44 | **/coverage/ 45 | 46 | 47 | # Ignore all .py files in migrations folders 48 | # **/migrations/*.py 49 | # Except __init__.py 50 | # !**/migrations/__init__.py 51 | 52 | docs/static/openapi 53 | backend/openapi 54 | 55 | # ignore sentry files 56 | .sentryclirc 57 | .sentryclirc.local 58 | .vibe.json 59 | 60 | .venv 61 | 62 | mcp -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm lint-staged -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 sudarshansudarshan 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 | # ViBe 2 | 3 | ViBe is an innovative educational platform that enhances learning through continuous assessment and interactive challenges. Designed to ensure that every student fully masters the material before progressing, ViBe uses smart question generation and adaptive reviews to reinforce understanding and foster deeper learning. 4 | 5 | ## Key Features 6 | 7 | - **Active Learning Through Adaptive Challenges:** 8 | ViBe continuously assesses student comprehension and prompts a review of the material when needed, ensuring robust mastery before advancement. 9 | 10 | - **AI-Enhanced Question Generation:** 11 | Advanced algorithms generate contextually relevant questions that are both challenging and informative, helping to solidify knowledge. 12 | 13 | - **Secure and Integrity-Assured Assessments:** 14 | ViBe incorporates positive, AI-driven monitoring features that promote a fair and secure testing environment. These integrity safeguards include: 15 | - **Smart Proctoring:** AI-powered monitoring ensures that assessments are conducted honestly, providing a supportive framework that maintains academic integrity. 16 | - **Engagement Verification:** The system checks that students are actively engaged, reinforcing a positive learning atmosphere. 17 | 18 | ## Inspiration 19 | 20 | ViBe draws inspiration from the classical Indian tale of Vikram and Betaal. In the story, Betaal challenges King Vikramaditya with riddles, and any incorrect answer prompts a review of the challenge. Similarly, ViBe reinforces learning by requiring students to revisit content if their responses do not meet the mark, ensuring a deep and lasting understanding of the material. 21 | 22 | ## Quick Start 23 | 24 | For detailed setup instructions and comprehensive guides for both developers and end users, please refer to our [Documentation(In Progress)](https://continuousactivelearning.github.io/vibe/). 25 | 26 | ## License 27 | 28 | ViBe is licensed under the [MIT License](LICENSE). 29 | 30 | ## Feedback and Contributions 31 | 32 | We welcome your feedback, contributions, and suggestions. Please: 33 | - **Report Issues:** Open an issue on the repository. 34 | - **Contribute:** Fork the repository, create a feature branch, and submit a pull request. 35 | - **Contact:** Reach out to us at [dled@iitrpr.ac.in](mailto:dled@iitrpr.ac.in). 36 | 37 | --- 38 | 39 | Explore our [Documentation](https://continuousactivelearning.github.io/vibe/) for further details on usage, setup, and development. 40 | -------------------------------------------------------------------------------- /backend/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /backend/.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /backend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/", 3 | "rules": { 4 | "@typescript-eslint/no-unused-vars": "off", 5 | "no-unused-vars": "off" 6 | }, 7 | "overrides": [ 8 | { 9 | "files": ["**/*.test.ts", "**/*.spec.ts", "**/tests/**"], 10 | "rules": { 11 | "n/no-unpublished-import": "off", 12 | "@typescript-eslint/no-unused-vars": "off", 13 | "no-unused-vars": "off" 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /backend/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "demo-test" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | firebase-debug.*.log* 9 | 10 | # Firebase cache 11 | .firebase/ 12 | 13 | # Firebase config 14 | 15 | # Uncomment this if you'd like others to create their own Firebase project. 16 | # For a team working on the same Firebase project(s), it is recommended to leave 17 | # it commented so all members can deploy to the same project(s) in .firebaserc. 18 | # .firebaserc 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (http://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | 68 | # dataconnect generated files 69 | .dataconnect 70 | -------------------------------------------------------------------------------- /backend/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('gts/.prettierrc.json'), 3 | }; 4 | -------------------------------------------------------------------------------- /backend/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "emulators": { 3 | "auth": { 4 | "port": 9099 5 | }, 6 | "functions": { 7 | "port": 5001 8 | }, 9 | "ui": { 10 | "enabled": true 11 | }, 12 | "singleProjectMode": true 13 | }, 14 | "functions": [ 15 | { 16 | "source": "functions", 17 | "codebase": "default", 18 | "ignore": [ 19 | "node_modules", 20 | ".git", 21 | "firebase-debug.log", 22 | "firebase-debug.*.log", 23 | "*.local" 24 | ], 25 | "predeploy": ["npm --prefix \"$RESOURCE_DIR\" run build"] 26 | }, 27 | { 28 | "source": "firebase", 29 | "codebase": "firebase", 30 | "ignore": [ 31 | "node_modules", 32 | ".git", 33 | "firebase-debug.log", 34 | "firebase-debug.*.log", 35 | "*.local" 36 | ], 37 | "predeploy": ["npm --prefix \"$RESOURCE_DIR\" run build"] 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /backend/functions/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled JavaScript files 2 | lib/**/*.js 3 | lib/**/*.js.map 4 | 5 | # TypeScript v1 declaration files 6 | typings/ 7 | 8 | # Node.js dependency directory 9 | node_modules/ 10 | *.local -------------------------------------------------------------------------------- /backend/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "build": "tsc", 5 | "build:watch": "tsc --watch", 6 | "serve": "pnpm run build && firebase emulators:start --only functions", 7 | "shell": "pnpm run build && firebase functions:shell", 8 | "start": "pnpm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "engines": { 13 | "node": "22" 14 | }, 15 | "main": "src/index.ts", 16 | "dependencies": { 17 | "firebase-admin": "^12.6.0", 18 | "firebase-functions": "^6.0.1" 19 | }, 20 | "devDependencies": { 21 | "typescript": "^4.9.0", 22 | "firebase-functions-test": "^3.1.0" 23 | }, 24 | "private": true 25 | } 26 | -------------------------------------------------------------------------------- /backend/functions/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Import function triggers from their respective submodules: 3 | * 4 | * import {onCall} from "firebase-functions/v2/https"; 5 | * import {onDocumentWritten} from "firebase-functions/v2/firestore"; 6 | * 7 | * See a full list of supported triggers at https://firebase.google.com/docs/functions 8 | */ 9 | 10 | import {onRequest} from 'firebase-functions/v2/https'; 11 | import * as logger from 'firebase-functions/logger'; 12 | 13 | // Start writing functions 14 | // https://firebase.google.com/docs/functions/typescript 15 | 16 | export const helloWorld = onRequest((request, response) => { 17 | logger.info('Hello logs!', {structuredData: true}); 18 | response.send('Hello from Firebase!'); 19 | }); 20 | -------------------------------------------------------------------------------- /backend/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "esModuleInterop": true, 5 | "moduleResolution": "nodenext", 6 | "noImplicitReturns": true, 7 | "noUnusedLocals": true, 8 | "outDir": "lib", 9 | "sourceMap": true, 10 | "strict": true, 11 | "target": "es2017" 12 | }, 13 | "compileOnSave": true, 14 | "include": ["src"] 15 | } 16 | -------------------------------------------------------------------------------- /backend/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | testMatch: ['**/tests/**/*.test.ts'], 5 | setupFilesAfterEnv: ['/tests/setup.ts'], 6 | moduleNameMapper: { 7 | '^@/(.*)$': '/src/$1', 8 | '^shared/(.*)$': '/src/shared/$1', // Resolve shared path in Jest 9 | '^modules/(.*)$': '/src/modules/$1', // Resolve modules path if used 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /backend/src/config/app.ts: -------------------------------------------------------------------------------- 1 | import {env} from '../utils/env'; 2 | 3 | function getAppPath() { 4 | let currentDir = __dirname; 5 | currentDir = currentDir.replace('/config', ''); 6 | 7 | return currentDir; 8 | } 9 | 10 | export const appConfig = { 11 | node: env('NODE_ENV') || 'development', 12 | isProduction: env('NODE_ENV') === 'production', 13 | isStaging: env('NODE_ENV') === 'staging', 14 | isDevelopment: env('NODE_ENV') === 'development', 15 | name: env('APP_NAME'), 16 | port: Number(env('APP_PORT')) || 4000, 17 | routePrefix: env('APP_ROUTE_PREFIX'), 18 | url: env('APP_URL'), 19 | appPath: getAppPath(), 20 | }; 21 | -------------------------------------------------------------------------------- /backend/src/config/db.ts: -------------------------------------------------------------------------------- 1 | import {env} from '../utils/env'; 2 | 3 | export const dbConfig = { 4 | url: env('DB_URL'), 5 | dbName: env('DB_NAME') || 'vibe', 6 | }; 7 | -------------------------------------------------------------------------------- /backend/src/config/sentry.ts: -------------------------------------------------------------------------------- 1 | import {env} from 'utils/env'; 2 | 3 | export const sentryDSN = env('SENTRY_DSN'); 4 | -------------------------------------------------------------------------------- /backend/src/instrument.ts: -------------------------------------------------------------------------------- 1 | import Sentry from '@sentry/node'; 2 | import {nodeProfilingIntegration} from '@sentry/profiling-node'; 3 | import {sentryDSN} from 'config/sentry'; 4 | 5 | // Ensure to call this before importing any other modules! 6 | Sentry.init({ 7 | dsn: sentryDSN, 8 | integrations: [ 9 | // Add our Profiling integration 10 | nodeProfilingIntegration(), 11 | ], 12 | 13 | // Add Tracing by setting tracesSampleRate 14 | // We recommend adjusting this value in production 15 | tracesSampleRate: 1.0, 16 | 17 | // Set sampling rate for profiling 18 | // This is relative to tracesSampleRate 19 | profilesSampleRate: 1.0, 20 | }); 21 | -------------------------------------------------------------------------------- /backend/src/modules/auth/classes/validators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AuthValidators'; 2 | -------------------------------------------------------------------------------- /backend/src/modules/auth/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AuthController'; 2 | -------------------------------------------------------------------------------- /backend/src/modules/auth/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file index.ts 3 | * @description This file exports all the DTOs used in the auth module. 4 | * @module auth 5 | * 6 | * @author Aditya BMV 7 | * @organization DLED 8 | * @license MIT 9 | * @created 2025-03-06 10 | */ 11 | 12 | import 'reflect-metadata'; 13 | import {Action, RoutingControllersOptions} from 'routing-controllers'; 14 | import {AuthController} from './controllers/AuthController'; 15 | import {Container} from 'typedi'; 16 | import {useContainer} from 'routing-controllers'; 17 | import {IAuthService} from './interfaces/IAuthService'; 18 | import {FirebaseAuthService} from './services/FirebaseAuthService'; 19 | 20 | import {dbConfig} from '../../config/db'; 21 | import {IDatabase, IUserRepository} from 'shared/database'; 22 | import { 23 | MongoDatabase, 24 | UserRepository, 25 | } from 'shared/database/providers/MongoDatabaseProvider'; 26 | 27 | useContainer(Container); 28 | 29 | export function setupAuthModuleDependencies(): void { 30 | if (!Container.has('Database')) { 31 | Container.set( 32 | 'Database', 33 | new MongoDatabase(dbConfig.url, 'vibe'), 34 | ); 35 | } 36 | 37 | if (!Container.has('UserRepository')) { 38 | Container.set( 39 | 'UserRepository', 40 | new UserRepository(Container.get('Database')), 41 | ); 42 | } 43 | 44 | if (!Container.has('AuthService')) { 45 | Container.set( 46 | 'AuthService', 47 | new FirebaseAuthService(Container.get('UserRepository')), 48 | ); 49 | } 50 | } 51 | 52 | setupAuthModuleDependencies(); 53 | 54 | export const authModuleOptions: RoutingControllersOptions = { 55 | controllers: [AuthController], 56 | authorizationChecker: async function (action: Action, roles: string[]) { 57 | // Use the auth service to check if the user is authorized 58 | const authService = Container.get('AuthService'); 59 | const token = action.request.headers['authorization']?.split(' ')[1]; 60 | if (!token) { 61 | return false; 62 | } 63 | try { 64 | const user = await authService.verifyToken(token); 65 | action.request.user = user; 66 | 67 | // Check if the user's roles match the required roles 68 | if (roles.length > 0 && !roles.some(role => user.roles.includes(role))) { 69 | return false; 70 | } 71 | 72 | return true; 73 | } catch (error) { 74 | return false; 75 | } 76 | }, 77 | }; 78 | 79 | export * from './classes/validators/index'; 80 | export * from './controllers/index'; 81 | export * from './interfaces/index'; 82 | export * from './services/index'; 83 | -------------------------------------------------------------------------------- /backend/src/modules/auth/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export {IAuthService} from './IAuthService'; 2 | -------------------------------------------------------------------------------- /backend/src/modules/auth/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './FirebaseAuthService'; 2 | -------------------------------------------------------------------------------- /backend/src/modules/courses/classes/transformers/CourseVersion.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import {Expose, Transform, Type} from 'class-transformer'; 3 | import { 4 | ObjectIdToString, 5 | StringToObjectId, 6 | } from 'shared/constants/transformerConstants'; 7 | import {ICourseVersion} from 'shared/interfaces/Models'; 8 | import {ID} from 'shared/types'; 9 | import {Module} from './Module'; 10 | import {CreateCourseVersionBody} from '../validators'; 11 | 12 | /** 13 | * Course version data transformation. 14 | * 15 | * @category Courses/Transformers 16 | */ 17 | class CourseVersion implements ICourseVersion { 18 | @Expose() 19 | @Transform(ObjectIdToString.transformer, {toPlainOnly: true}) 20 | @Transform(StringToObjectId.transformer, {toClassOnly: true}) 21 | _id?: ID; 22 | 23 | @Expose() 24 | @Transform(ObjectIdToString.transformer, {toPlainOnly: true}) 25 | @Transform(StringToObjectId.transformer, {toClassOnly: true}) 26 | courseId: ID; 27 | 28 | @Expose() 29 | version: string; 30 | 31 | @Expose() 32 | description: string; 33 | 34 | @Expose() 35 | @Type(() => Module) 36 | modules: Module[]; 37 | 38 | @Expose() 39 | @Type(() => Date) 40 | createdAt: Date; 41 | 42 | @Expose() 43 | @Type(() => Date) 44 | updatedAt: Date; 45 | 46 | constructor(courseVersionBody?: CreateCourseVersionBody) { 47 | if (courseVersionBody) { 48 | this.courseId = courseVersionBody.courseId; 49 | this.version = courseVersionBody.version; 50 | this.description = courseVersionBody.description; 51 | } 52 | this.modules = []; 53 | this.createdAt = new Date(); 54 | this.updatedAt = new Date(); 55 | } 56 | } 57 | 58 | export {CourseVersion}; 59 | -------------------------------------------------------------------------------- /backend/src/modules/courses/classes/transformers/Module.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import {Expose, Transform, Type} from 'class-transformer'; 3 | import {calculateNewOrder} from 'modules/courses/utils/calculateNewOrder'; 4 | import {ObjectId} from 'mongodb'; 5 | import { 6 | ObjectIdToString, 7 | StringToObjectId, 8 | } from 'shared/constants/transformerConstants'; 9 | import {IModule} from 'shared/interfaces/Models'; 10 | import {ID} from 'shared/types'; 11 | import {CreateModuleBody} from '../validators/ModuleValidators'; 12 | import {Section} from './Section'; 13 | 14 | /** 15 | * Module data transformation. 16 | * 17 | * @category Courses/Transformers 18 | */ 19 | class Module implements IModule { 20 | @Expose() 21 | @Transform(ObjectIdToString.transformer, {toPlainOnly: true}) 22 | @Transform(StringToObjectId.transformer, {toClassOnly: true}) 23 | moduleId?: ID; 24 | 25 | @Expose() 26 | name: string; 27 | 28 | @Expose() 29 | description: string; 30 | 31 | @Expose() 32 | order: string; 33 | 34 | @Expose() 35 | @Type(() => Section) 36 | sections: Section[]; 37 | 38 | @Expose() 39 | @Type(() => Date) 40 | createdAt: Date; 41 | 42 | @Expose() 43 | @Type(() => Date) 44 | updatedAt: Date; 45 | 46 | constructor(moduleBody: CreateModuleBody, existingModules: IModule[]) { 47 | if (moduleBody) { 48 | this.name = moduleBody.name; 49 | this.description = moduleBody.description; 50 | } 51 | const sortedModules = existingModules.sort((a, b) => 52 | a.order.localeCompare(b.order), 53 | ); 54 | this.moduleId = new ObjectId(); 55 | this.order = calculateNewOrder( 56 | sortedModules, 57 | 'moduleId', 58 | moduleBody.afterModuleId, 59 | moduleBody.beforeModuleId, 60 | ); 61 | this.sections = []; 62 | this.createdAt = new Date(); 63 | this.updatedAt = new Date(); 64 | } 65 | } 66 | 67 | export {Module}; 68 | -------------------------------------------------------------------------------- /backend/src/modules/courses/classes/transformers/Section.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import {Expose, Transform, Type} from 'class-transformer'; 3 | import {calculateNewOrder} from 'modules/courses/utils/calculateNewOrder'; 4 | import {ObjectId} from 'mongodb'; 5 | import { 6 | ObjectIdToString, 7 | StringToObjectId, 8 | } from 'shared/constants/transformerConstants'; 9 | import {ISection} from 'shared/interfaces/Models'; 10 | import {ID} from 'shared/types'; 11 | import {CreateSectionBody} from '../validators/SectionValidators'; 12 | 13 | /** 14 | * Section data transformation. 15 | * 16 | * @category Courses/Transformers 17 | */ 18 | class Section implements ISection { 19 | @Expose() 20 | @Transform(ObjectIdToString.transformer, {toPlainOnly: true}) 21 | @Transform(StringToObjectId.transformer, {toClassOnly: true}) 22 | sectionId?: ID; 23 | 24 | @Expose() 25 | name: string; 26 | 27 | @Expose() 28 | description: string; 29 | 30 | @Expose() 31 | order: string; 32 | 33 | @Expose() 34 | itemsGroupId: ID; 35 | 36 | @Expose() 37 | @Type(() => Date) 38 | createdAt: Date; 39 | 40 | @Expose() 41 | @Type(() => Date) 42 | updatedAt: Date; 43 | 44 | constructor(sectionBody: CreateSectionBody, existingSections: ISection[]) { 45 | if (sectionBody) { 46 | this.name = sectionBody.name; 47 | this.description = sectionBody.description; 48 | } 49 | const sortedSections = existingSections.sort((a, b) => 50 | a.order.localeCompare(b.order), 51 | ); 52 | this.sectionId = new ObjectId(); 53 | this.order = calculateNewOrder( 54 | sortedSections, 55 | 'sectionId', 56 | sectionBody.afterSectionId, 57 | sectionBody.beforeSectionId, 58 | ); 59 | this.createdAt = new Date(); 60 | this.updatedAt = new Date(); 61 | } 62 | } 63 | 64 | export {Section}; 65 | -------------------------------------------------------------------------------- /backend/src/modules/courses/classes/transformers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Course'; 2 | export * from './CourseVersion'; 3 | export * from './Module'; 4 | export * from './Section'; 5 | export * from './Item'; 6 | -------------------------------------------------------------------------------- /backend/src/modules/courses/classes/validators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CourseValidators'; 2 | export * from './CourseVersionValidators'; 3 | export * from './ModuleValidators'; 4 | export * from './SectionValidators'; 5 | export * from './ItemValidators'; 6 | -------------------------------------------------------------------------------- /backend/src/modules/courses/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CourseController'; 2 | export * from './CourseVersionController'; 3 | export * from './ModuleController'; 4 | export * from './SectionController'; 5 | export * from './ItemController'; 6 | -------------------------------------------------------------------------------- /backend/src/modules/courses/services/CourseService.ts: -------------------------------------------------------------------------------- 1 | import {ICourseRepository} from 'shared/database'; 2 | import {Inject, Service} from 'typedi'; 3 | import {Course} from '../classes/transformers'; 4 | import {InternalServerError, NotFoundError} from 'routing-controllers'; 5 | import {ReadConcern, ReadPreference, WriteConcern} from 'mongodb'; 6 | 7 | @Service() 8 | class CourseService { 9 | constructor( 10 | @Inject('CourseRepo') 11 | private readonly courseRepo: ICourseRepository, 12 | ) {} 13 | 14 | private readonly transactionOptions = { 15 | readPreference: ReadPreference.primary, 16 | writeConcern: new WriteConcern('majority'), 17 | readConcern: new ReadConcern('majority'), 18 | }; 19 | 20 | async createCourse(course: Course): Promise { 21 | const session = (await this.courseRepo.getDBClient()).startSession(); 22 | 23 | try { 24 | await session.startTransaction(this.transactionOptions); 25 | const createdCourse = await this.courseRepo.create(course, session); 26 | if (!createdCourse) { 27 | throw new InternalServerError( 28 | 'Failed to create course. Please try again later.', 29 | ); 30 | } 31 | await session.commitTransaction(); 32 | return createdCourse; 33 | } catch (error) { 34 | await session.abortTransaction(); 35 | throw error; 36 | } finally { 37 | await session.endSession(); 38 | } 39 | } 40 | 41 | async readCourse(id: string): Promise { 42 | const course = await this.courseRepo.read(id); 43 | if (!course) { 44 | throw new NotFoundError( 45 | 'No course found with the specified ID. Please verify the ID and try again.', 46 | ); 47 | } 48 | return course; 49 | } 50 | 51 | async updateCourse( 52 | id: string, 53 | data: Pick, 54 | ): Promise { 55 | const updatedCourse = await this.courseRepo.update(id, data); 56 | if (!updatedCourse) { 57 | throw new NotFoundError( 58 | 'No course found with the specified ID. Please verify the ID and try again.', 59 | ); 60 | } 61 | return updatedCourse; 62 | } 63 | } 64 | 65 | export {CourseService}; 66 | -------------------------------------------------------------------------------- /backend/src/modules/courses/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CourseService'; 2 | export * from './ItemService'; 3 | export * from './ModuleService'; 4 | export * from './CourseVersionService'; 5 | export * from './SectionService'; 6 | -------------------------------------------------------------------------------- /backend/src/modules/courses/utils/calculateNewOrder.ts: -------------------------------------------------------------------------------- 1 | import {LexoRank} from 'lexorank'; 2 | 3 | /** 4 | * Calculates the order for a new entity (Module, Section, or Item) 5 | */ 6 | 7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 8 | export function calculateNewOrder>( 9 | sortedEntities: T[], 10 | idField: keyof T, 11 | afterId?: string, 12 | beforeId?: string, 13 | ): string { 14 | if (sortedEntities.length === 0) { 15 | return LexoRank.middle().toString(); 16 | } 17 | 18 | if (!sortedEntities.every(entity => entity.order)) { 19 | throw new Error("Some entities are missing the 'order' field."); 20 | } 21 | 22 | if (!afterId && !beforeId) { 23 | // console.log('Adding in the end'); 24 | return LexoRank.parse(sortedEntities[sortedEntities.length - 1].order) 25 | .genNext() 26 | .toString(); 27 | } 28 | 29 | if (afterId) { 30 | // console.log('Adding after', afterId); 31 | const afterIndex = sortedEntities.findIndex(m => m[idField] === afterId); 32 | if (afterIndex === sortedEntities.length - 1) { 33 | return LexoRank.parse(sortedEntities[afterIndex].order) 34 | .genNext() 35 | .toString(); 36 | } 37 | // console.log(sortedEntities); 38 | // console.log('After order', sortedEntities[afterIndex].order); 39 | // console.log('After +1 order', sortedEntities[afterIndex + 1].order); 40 | return LexoRank.parse(sortedEntities[afterIndex].order) 41 | .between(LexoRank.parse(sortedEntities[afterIndex + 1].order)) 42 | .toString(); 43 | } 44 | 45 | if (beforeId) { 46 | // console.log('Adding before', beforeId); 47 | const beforeIndex = sortedEntities.findIndex(m => m[idField] === beforeId); 48 | if (beforeIndex === 0) { 49 | return LexoRank.parse(sortedEntities[beforeIndex].order) 50 | .genPrev() 51 | .toString(); 52 | } 53 | 54 | // console.log('Before -1 order', sortedEntities[beforeIndex - 1].order); 55 | // console.log('Before order', sortedEntities[beforeIndex].order); 56 | 57 | return LexoRank.parse(sortedEntities[beforeIndex - 1].order) 58 | .between(LexoRank.parse(sortedEntities[beforeIndex].order)) 59 | .toString(); 60 | } 61 | 62 | return LexoRank.middle().toString(); 63 | } 64 | -------------------------------------------------------------------------------- /backend/src/modules/docs/index.ts: -------------------------------------------------------------------------------- 1 | import {RoutingControllersOptions, useContainer} from 'routing-controllers'; 2 | import {Container} from 'typedi'; 3 | import {OpenApiSpecService} from './services/OpenApiSpecService'; 4 | 5 | // Set up TypeDI container 6 | useContainer(Container); 7 | 8 | // Export empty array for controllers since we're handling docs differently 9 | export const docsModuleOptions: RoutingControllersOptions = { 10 | controllers: [], 11 | routePrefix: '', 12 | defaultErrorHandler: true, 13 | }; 14 | 15 | export * from './services/OpenApiSpecService'; 16 | -------------------------------------------------------------------------------- /backend/src/modules/index.ts: -------------------------------------------------------------------------------- 1 | export * from './courses/index'; 2 | export * from './auth/index'; 3 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/classes/transformers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Question'; 2 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/classes/validators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './QuestionValidator'; 2 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/controllers/QuestionController.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { 3 | JsonController, 4 | Authorized, 5 | Post, 6 | Body, 7 | Get, 8 | Put, 9 | Delete, 10 | Params, 11 | HttpCode, 12 | OnUndefined, 13 | BadRequestError, 14 | } from 'routing-controllers'; 15 | import {Service, Inject} from 'typedi'; 16 | import {CreateQuestionBody} from '../classes/validators/QuestionValidator'; 17 | import {QuestionFactory} from '../classes/transformers/Question'; 18 | import {QuestionProcessor} from '../question-processing/QuestionProcessor'; 19 | 20 | @JsonController('/questions') 21 | @Service() 22 | export class QuestionController { 23 | constructor() {} 24 | 25 | @Authorized(['admin', 'instructor']) 26 | @Post('/', {transformResponse: true}) 27 | @HttpCode(201) 28 | @OnUndefined(201) 29 | async create(@Body() body: CreateQuestionBody) { 30 | const question = QuestionFactory.createQuestion(body); 31 | try { 32 | const questionProcessor = new QuestionProcessor(question); 33 | questionProcessor.validate(); 34 | 35 | const renderedQuestion = questionProcessor.render(); 36 | return renderedQuestion; 37 | } catch (error) { 38 | throw new BadRequestError((error as Error).message); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './QuestionController'; 2 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/index.ts: -------------------------------------------------------------------------------- 1 | import {useContainer} from 'class-validator'; 2 | import {MongoDatabase} from 'shared/database/providers/mongo/MongoDatabase'; 3 | import Container from 'typedi'; 4 | import {dbConfig} from '../../config/db'; 5 | import {RoutingControllersOptions} from 'routing-controllers'; 6 | import {QuestionController} from './controllers'; 7 | 8 | // useContainer(Container); 9 | 10 | // export function setupQuizzesModuleDependencies(): void { 11 | // if (!Container.has('Database')) { 12 | // Container.set('Database', new MongoDatabase(dbConfig.url, 'vibe')); 13 | // } 14 | // } 15 | 16 | export const quizzesModuleOptions: RoutingControllersOptions = { 17 | controllers: [QuestionController], 18 | middlewares: [], 19 | defaultErrorHandler: true, 20 | authorizationChecker: async function () { 21 | return true; 22 | }, 23 | validation: true, 24 | }; 25 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/question-processing/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/continuousactivelearning/vibe/ec151a7a5a3b1b89b3b1564b30bd9db23f10176d/backend/src/modules/quizzes/question-processing/index.ts -------------------------------------------------------------------------------- /backend/src/modules/quizzes/question-processing/renderers/BaseQuestionRenderer.ts: -------------------------------------------------------------------------------- 1 | import {BaseQuestion} from 'modules/quizzes/classes/transformers'; 2 | import {TagParser, ParameterMap} from '../tag-parser'; 3 | import {IQuestionRenderView} from './interfaces/RenderViews'; 4 | 5 | class BaseQuestionRenderer { 6 | question: BaseQuestion; 7 | tagParser: TagParser; 8 | 9 | constructor(question: BaseQuestion, tagParser: TagParser) { 10 | this.question = question; 11 | this.tagParser = tagParser; 12 | } 13 | 14 | render(parameterMap: ParameterMap): BaseQuestion | IQuestionRenderView { 15 | if (!this.question.isParameterized || !this.question.parameters?.length) { 16 | return this.question; 17 | } 18 | 19 | const renderedQuestion: BaseQuestion = JSON.parse( 20 | JSON.stringify(this.question), 21 | ); 22 | 23 | // Apply a function for all these fields in question text, hint, correctItem.text, correctItem.hint 24 | renderedQuestion.text = this.tagParser.processText( 25 | this.question.text, 26 | parameterMap, 27 | ); 28 | if (this.question.hint) { 29 | renderedQuestion.hint = this.tagParser.processText( 30 | this.question.hint, 31 | parameterMap, 32 | ); 33 | } 34 | return renderedQuestion; 35 | } 36 | } 37 | 38 | export {BaseQuestionRenderer}; 39 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/question-processing/renderers/DESQuestionRenderer.ts: -------------------------------------------------------------------------------- 1 | import {DESQuestion} from 'modules/quizzes/classes/transformers'; 2 | import {TagParser, ParameterMap} from '../tag-parser'; 3 | import {BaseQuestionRenderer} from './BaseQuestionRenderer'; 4 | import {DESQuestionRenderView} from './interfaces/RenderViews'; 5 | 6 | class DESQuestionRenderer extends BaseQuestionRenderer { 7 | declare question: DESQuestion; 8 | declare tagParser: TagParser; 9 | 10 | constructor(question: DESQuestion, tagParser: TagParser) { 11 | super(question, tagParser); 12 | } 13 | 14 | render(parameterMap: ParameterMap): DESQuestionRenderView { 15 | const renderedQuestion: DESQuestion = super.render( 16 | parameterMap, 17 | ) as DESQuestion; 18 | 19 | // Process solutionText with parameter values if present 20 | const processedSolutionText = renderedQuestion.solutionText 21 | ? this.tagParser.processText(renderedQuestion.solutionText, parameterMap) 22 | : ''; 23 | 24 | const renderedQuestionView: DESQuestionRenderView = { 25 | _id: renderedQuestion._id, 26 | type: renderedQuestion.type, 27 | isParameterized: renderedQuestion.isParameterized, 28 | text: renderedQuestion.text, 29 | hint: renderedQuestion.hint, 30 | points: renderedQuestion.points, 31 | timeLimitSeconds: renderedQuestion.timeLimitSeconds, 32 | solutionText: processedSolutionText, 33 | parameterMap: parameterMap, 34 | }; 35 | 36 | return renderedQuestionView; 37 | } 38 | } 39 | 40 | export {DESQuestionRenderer}; 41 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/question-processing/renderers/NATQuestionRenderer.ts: -------------------------------------------------------------------------------- 1 | import {NATQuestion} from 'modules/quizzes/classes/transformers'; 2 | import {TagParser, ParameterMap} from '../tag-parser'; 3 | import {BaseQuestionRenderer} from './BaseQuestionRenderer'; 4 | import {NATQuestionRenderView} from './interfaces/RenderViews'; 5 | 6 | class NATQuestionRenderer extends BaseQuestionRenderer { 7 | declare question: NATQuestion; 8 | declare tagParser: TagParser; 9 | 10 | constructor(question: NATQuestion, tagParser: TagParser) { 11 | super(question, tagParser); 12 | } 13 | 14 | render(parameterMap: ParameterMap): NATQuestionRenderView { 15 | // Use the base renderer to process text and hint 16 | const renderedQuestion: NATQuestion = super.render( 17 | parameterMap, 18 | ) as NATQuestion; 19 | 20 | const renderedQuestionView: NATQuestionRenderView = { 21 | _id: renderedQuestion._id, 22 | type: renderedQuestion.type, 23 | isParameterized: renderedQuestion.isParameterized, 24 | text: renderedQuestion.text, 25 | hint: renderedQuestion.hint, 26 | points: renderedQuestion.points, 27 | timeLimitSeconds: renderedQuestion.timeLimitSeconds, 28 | decimalPrecision: renderedQuestion.decimalPrecision, 29 | upperLimit: renderedQuestion.upperLimit, 30 | lowerLimit: renderedQuestion.lowerLimit, 31 | value: renderedQuestion.value, 32 | expression: renderedQuestion.expression, 33 | parameterMap: parameterMap, 34 | }; 35 | 36 | return renderedQuestionView; 37 | } 38 | } 39 | 40 | export {NATQuestionRenderer}; 41 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/question-processing/renderers/OTLQuestionRenderer.ts: -------------------------------------------------------------------------------- 1 | import {OTLQuestion} from 'modules/quizzes/classes/transformers'; 2 | import {ILotItem} from 'shared/interfaces/quiz'; 3 | import {TagParser, ParameterMap} from '../tag-parser'; 4 | import {BaseQuestionRenderer} from './BaseQuestionRenderer'; 5 | import {OTLQuestionRenderView} from './interfaces/RenderViews'; 6 | 7 | class OTLQuestionRenderer extends BaseQuestionRenderer { 8 | declare question: OTLQuestion; 9 | declare tagParser: TagParser; 10 | 11 | constructor(question: OTLQuestion, tagParser: TagParser) { 12 | super(question, tagParser); 13 | } 14 | 15 | render(parameterMap: ParameterMap): OTLQuestionRenderView { 16 | const renderedQuestion: OTLQuestion = super.render( 17 | parameterMap, 18 | ) as OTLQuestion; 19 | 20 | // Extract lot items from ordering 21 | const lotItems: ILotItem[] = renderedQuestion.ordering.map( 22 | order => order.lotItem, 23 | ); 24 | 25 | // Process text and explanation for each lot item 26 | const processedLotItems = lotItems.map(item => ({ 27 | ...item, 28 | text: this.tagParser.processText(item.text, parameterMap), 29 | explaination: this.tagParser.processText(item.explaination, parameterMap), 30 | })); 31 | 32 | // Shuffle the processed lot items 33 | const shuffledLotItems = processedLotItems.sort(() => Math.random() - 0.5); 34 | 35 | const renderedQuestionWithLotItems: OTLQuestionRenderView = { 36 | _id: renderedQuestion._id, 37 | type: renderedQuestion.type, 38 | isParameterized: renderedQuestion.isParameterized, 39 | text: renderedQuestion.text, 40 | hint: renderedQuestion.hint, 41 | points: renderedQuestion.points, 42 | timeLimitSeconds: renderedQuestion.timeLimitSeconds, 43 | lotItems: shuffledLotItems, 44 | parameterMap: parameterMap, 45 | }; 46 | 47 | return renderedQuestionWithLotItems; 48 | } 49 | } 50 | 51 | export {OTLQuestionRenderer}; 52 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/question-processing/renderers/SMLQuestionRenderer.ts: -------------------------------------------------------------------------------- 1 | import {SMLQuestion} from 'modules/quizzes/classes/transformers'; 2 | import {ILotItem} from 'shared/interfaces/quiz'; 3 | import {TagParser, ParameterMap} from '../tag-parser'; 4 | import {BaseQuestionRenderer} from './BaseQuestionRenderer'; 5 | import {SMLQuestionRenderView} from './interfaces/RenderViews'; 6 | 7 | class SMLQuestionRenderer extends BaseQuestionRenderer { 8 | declare question: SMLQuestion; 9 | declare tagParser: TagParser; 10 | 11 | constructor(question: SMLQuestion, tagParser: TagParser) { 12 | super(question, tagParser); 13 | } 14 | 15 | render(parameterMap: ParameterMap): SMLQuestionRenderView { 16 | const renderedQuestion: SMLQuestion = super.render( 17 | parameterMap, 18 | ) as SMLQuestion; 19 | 20 | // Combine all lot items (correct and incorrect) 21 | const lotItems: ILotItem[] = [ 22 | ...renderedQuestion.correctLotItems, 23 | ...renderedQuestion.incorrectLotItems, 24 | ]; 25 | 26 | // Process text and explanation for each lot item 27 | const processedLotItems = lotItems.map(item => ({ 28 | ...item, 29 | text: this.tagParser.processText(item.text, parameterMap), 30 | explaination: this.tagParser.processText(item.explaination, parameterMap), 31 | })); 32 | 33 | // Shuffle the lot items 34 | const shuffledLotItems = processedLotItems.sort(() => Math.random() - 0.5); 35 | 36 | const renderedQuestionWithLotItems: SMLQuestionRenderView = { 37 | _id: renderedQuestion._id, 38 | type: renderedQuestion.type, 39 | isParameterized: renderedQuestion.isParameterized, 40 | text: renderedQuestion.text, 41 | hint: renderedQuestion.hint, 42 | points: renderedQuestion.points, 43 | timeLimitSeconds: renderedQuestion.timeLimitSeconds, 44 | lotItems: shuffledLotItems, 45 | parameterMap: parameterMap, 46 | }; 47 | 48 | return renderedQuestionWithLotItems; 49 | } 50 | } 51 | 52 | export {SMLQuestionRenderer}; 53 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/question-processing/renderers/SOLQuestionRenderer.ts: -------------------------------------------------------------------------------- 1 | import {SOLQuestion} from 'modules/quizzes/classes/transformers'; 2 | import {ILotItem} from 'shared/interfaces/quiz'; 3 | import {TagParser, ParameterMap} from '../tag-parser'; 4 | import {BaseQuestionRenderer} from './BaseQuestionRenderer'; 5 | import {SOLQuestionRenderView} from './interfaces/RenderViews'; 6 | 7 | class SOLQuestionRenderer extends BaseQuestionRenderer { 8 | declare question: SOLQuestion; 9 | declare tagParser: TagParser; 10 | 11 | constructor(question: SOLQuestion, tagParser: TagParser) { 12 | super(question, tagParser); 13 | } 14 | render(parameterMap: ParameterMap): SOLQuestionRenderView { 15 | const renderedQuestion: SOLQuestion = super.render( 16 | parameterMap, 17 | ) as SOLQuestion; 18 | 19 | const lotItems: ILotItem[] = [ 20 | renderedQuestion.correctLotItem, 21 | ...renderedQuestion.incorrectLotItems, 22 | ]; 23 | const processedLotItems = lotItems.map(item => ({ 24 | ...item, 25 | text: this.tagParser.processText(item.text, parameterMap), 26 | explaination: this.tagParser.processText(item.explaination, parameterMap), 27 | })); 28 | 29 | //Shuffle the lot 30 | const shuffledLotItems = processedLotItems.sort(() => Math.random() - 0.5); 31 | 32 | const renderedQuestionWithLotItems: SOLQuestionRenderView = { 33 | _id: renderedQuestion._id, 34 | type: renderedQuestion.type, 35 | isParameterized: renderedQuestion.isParameterized, 36 | text: renderedQuestion.text, 37 | hint: renderedQuestion.hint, 38 | points: renderedQuestion.points, 39 | timeLimitSeconds: renderedQuestion.timeLimitSeconds, 40 | lotItems: shuffledLotItems, 41 | parameterMap: parameterMap, 42 | }; 43 | 44 | return renderedQuestionWithLotItems; 45 | } 46 | } 47 | 48 | export {SOLQuestionRenderer}; 49 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/question-processing/renderers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interfaces/RenderViews'; 2 | export * from './BaseQuestionRenderer'; 3 | export * from './SOLQuestionRenderer'; 4 | export * from './SMLQuestionRenderer'; 5 | export * from './OTLQuestionRenderer'; 6 | export * from './NATQuestionRenderer'; 7 | export * from './DESQuestionRenderer'; 8 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/question-processing/renderers/interfaces/RenderViews.ts: -------------------------------------------------------------------------------- 1 | import {BaseQuestion} from 'modules/quizzes/classes/transformers'; 2 | import {ILotItem} from 'shared/interfaces/quiz'; 3 | import {ParameterMap} from '../../tag-parser'; 4 | 5 | interface IQuestionRenderView extends BaseQuestion { 6 | parameterMap?: ParameterMap; 7 | } 8 | 9 | interface SOLQuestionRenderView extends IQuestionRenderView { 10 | lotItems: ILotItem[]; 11 | } 12 | 13 | interface SMLQuestionRenderView extends IQuestionRenderView { 14 | lotItems: ILotItem[]; 15 | } 16 | 17 | interface OTLQuestionRenderView extends IQuestionRenderView { 18 | lotItems: ILotItem[]; 19 | } 20 | 21 | interface NATQuestionRenderView extends IQuestionRenderView { 22 | decimalPrecision: number; 23 | upperLimit: number; 24 | lowerLimit: number; 25 | value?: number; 26 | expression?: string; 27 | } 28 | 29 | interface DESQuestionRenderView extends IQuestionRenderView { 30 | solutionText: string; 31 | } 32 | 33 | export { 34 | IQuestionRenderView, 35 | SOLQuestionRenderView, 36 | SMLQuestionRenderView, 37 | OTLQuestionRenderView, 38 | NATQuestionRenderView, 39 | DESQuestionRenderView, 40 | }; 41 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/question-processing/tag-parser/TagParser.ts: -------------------------------------------------------------------------------- 1 | import {IQuestionParameter} from 'shared/interfaces/quiz'; 2 | import {Tag, ParameterMap} from './tags/Tag'; 3 | 4 | class TagParser { 5 | constructor(private processors: Record) {} 6 | 7 | processText(text: string, context: ParameterMap): string { 8 | return text.replace(/<(\w+)>(.*?)<\/\1>/g, (_, tagName, inner) => { 9 | const processor = this.processors[tagName]; 10 | return processor ? processor.process(inner, context) : inner; 11 | }); 12 | } 13 | 14 | isAnyValidTagPresent(text: string): boolean { 15 | //loop over all processors and run extract function from each and store the resulting list in gobal list. 16 | const allContents: string[] = []; 17 | 18 | for (const processor of Object.values(this.processors)) { 19 | const tagContents = processor.extract(text); 20 | allContents.push(...tagContents); 21 | } 22 | 23 | //If tagContents list is empty, return false 24 | return allContents.length === 0 ? false : true; 25 | } 26 | 27 | validateTags(text: string, parameters?: IQuestionParameter[]): void { 28 | text.replace(/<(\w+)>(.*?)<\/\1>/g, (matchString, tagName, inner) => { 29 | const processor = this.processors[tagName]; 30 | processor.validate(inner, parameters); 31 | return matchString; // Return the original substring to satisfy the type signature 32 | }); 33 | } 34 | } 35 | 36 | export {TagParser}; 37 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/question-processing/tag-parser/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TagParser'; 2 | export * from './tags/Tag'; 3 | export * from './tags/NumExprTag'; 4 | export * from './tags/NumExprTexTag'; 5 | export * from './tags/QParamTag'; 6 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/question-processing/tag-parser/tags/NumExprTag.ts: -------------------------------------------------------------------------------- 1 | import {evaluate, parse, SymbolNode} from 'mathjs'; 2 | import {Tag, ParameterMap} from './Tag'; 3 | import {IQuestionParameter} from 'shared/interfaces/quiz'; 4 | 5 | class NumExprTag extends Tag { 6 | validate(text: string, parameters?: IQuestionParameter[]): void { 7 | parse(text); 8 | if (parameters) { 9 | const paramMap = new Map(parameters.map(p => [p.name, p])); 10 | const parsedNode = parse(text); 11 | const symbols = []; 12 | 13 | // Traverse the parsed node to collect all symbols 14 | parsedNode.traverse((node: SymbolNode) => { 15 | if (node.isSymbolNode) { 16 | symbols.push(node.name); 17 | } 18 | }); 19 | 20 | const uniqueSymbols = Array.from(new Set(symbols)); 21 | 22 | for (const symbol of uniqueSymbols) { 23 | // Check if all symbols are defined in parameters 24 | if (!paramMap.has(symbol)) { 25 | throw new Error(`Variable '${symbol}' not found in parameters.`); 26 | } 27 | // Check if the type of the symbol is 'number' 28 | const param = paramMap.get(symbol); 29 | if (param && param.type !== 'number') { 30 | throw new Error(`Variable '${symbol}' must be of type 'number'.`); 31 | } 32 | } 33 | } 34 | } 35 | 36 | extract(text: string): string[] { 37 | const regex = /(.*?)<\/NumExpr>/gs; 38 | const matches: string[] = []; 39 | let match; 40 | while ((match = regex.exec(text)) !== null) { 41 | matches.push(match[1]); 42 | } 43 | return matches; 44 | } 45 | 46 | process(text: string, context: ParameterMap): string { 47 | try { 48 | const result = evaluate(text, context); 49 | return result.toString(); 50 | } catch (err) { 51 | console.error('Error evaluating expression:', text, context); 52 | throw new Error(`Invalid math expression: ${text}`); 53 | } 54 | } 55 | } 56 | 57 | export {NumExprTag}; 58 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/question-processing/tag-parser/tags/NumExprTexTag.ts: -------------------------------------------------------------------------------- 1 | import {parse, SymbolNode} from 'mathjs'; 2 | import {Tag, ParameterMap} from './Tag'; 3 | import {IQuestionParameter} from 'shared/interfaces/quiz'; 4 | 5 | class NumExprTexTag extends Tag { 6 | validate(text: string, parameters: IQuestionParameter[]): void { 7 | parse(text); 8 | if (parameters) { 9 | const paramMap = new Map(parameters.map(p => [p.name, p])); 10 | const parsedNode = parse(text); 11 | const symbols = []; 12 | 13 | // Traverse the parsed node to collect all symbols 14 | parsedNode.traverse((node: SymbolNode) => { 15 | if (node.isSymbolNode) { 16 | symbols.push(node.name); 17 | } 18 | }); 19 | 20 | const uniqueSymbols = Array.from(new Set(symbols)); 21 | 22 | for (const symbol of uniqueSymbols) { 23 | // Check if all symbols are defined in parameters 24 | if (!paramMap.has(symbol)) { 25 | throw new Error(`Variable '${symbol}' not found in parameters.`); 26 | } 27 | // Check if the type of the symbol is 'number' 28 | const param = paramMap.get(symbol); 29 | if (param && param.type !== 'number') { 30 | throw new Error(`Variable '${symbol}' must be of type 'number'.`); 31 | } 32 | } 33 | } 34 | } 35 | 36 | extract(text: string): string[] { 37 | const regex = /(.*?)<\/NumExprTex>/gs; 38 | const matches: string[] = []; 39 | let match; 40 | while ((match = regex.exec(text)) !== null) { 41 | matches.push(match[1]); 42 | } 43 | return matches; 44 | } 45 | 46 | process(text: string, context: ParameterMap): string { 47 | try { 48 | // Replace all variable names in the expression with their corresponding values from the context. 49 | // This regular expression matches variable-like words (letters, digits, and underscores) 50 | // and replaces them with actual values if found in the context map. 51 | const exprWithValues = text.replace( 52 | /\b([a-zA-Z_][a-zA-Z0-9_]*)\b/g, 53 | match => { 54 | return context[match] !== undefined 55 | ? context[match].toString() 56 | : match; 57 | }, 58 | ); 59 | 60 | const node = parse(exprWithValues); 61 | return `$${node.toTex()}$`; 62 | } catch (err) { 63 | throw new Error(`Invalid TeX expression: ${text}`); 64 | } 65 | } 66 | } 67 | 68 | export {NumExprTexTag}; 69 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/question-processing/tag-parser/tags/QParamTag.ts: -------------------------------------------------------------------------------- 1 | import {Tag, ParameterMap} from './Tag'; 2 | 3 | class QParamTag extends Tag { 4 | validate(text: string): boolean { 5 | // Check if the tag content is a valid parameter name 6 | return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(text.trim()); 7 | } 8 | 9 | extract(text: string): string[] { 10 | const regex = /(.*?)<\/QParam>/gs; 11 | const matches: string[] = []; 12 | let match; 13 | while ((match = regex.exec(text)) !== null) { 14 | matches.push(match[1]); 15 | } 16 | return matches; 17 | } 18 | 19 | process(text: string, context: ParameterMap): string { 20 | const paramName = text.trim(); 21 | const value = context[paramName]; 22 | if (value === undefined) { 23 | throw new Error(`Parameter '${paramName}' not found in context`); 24 | } 25 | return value.toString(); 26 | } 27 | } 28 | 29 | export {QParamTag}; 30 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/question-processing/tag-parser/tags/Tag.ts: -------------------------------------------------------------------------------- 1 | import {IQuestionParameter} from 'shared/interfaces/quiz'; 2 | 3 | type ParameterMap = Record; 4 | abstract class Tag { 5 | abstract validate(text: string, parameters?: IQuestionParameter[]): void; 6 | abstract extract(text: string): string[]; 7 | abstract process(text: string, context: ParameterMap): string; 8 | } 9 | 10 | export {Tag, ParameterMap}; 11 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/question-processing/validators/BaseQuestionValidator.ts: -------------------------------------------------------------------------------- 1 | import {BaseQuestion} from 'modules/quizzes/classes/transformers'; 2 | import {TagParser} from 'modules/quizzes/question-processing/tag-parser/TagParser'; 3 | 4 | export class BaseQuestionValidator { 5 | tagStatus: { 6 | questionHasTag?: boolean; 7 | hintHasTag?: boolean; 8 | } = {}; 9 | 10 | question: BaseQuestion; 11 | tagParserEngine: TagParser; 12 | 13 | constructor(question: BaseQuestion, tagParserEngine: TagParser) { 14 | this.question = question; 15 | this.tagParserEngine = tagParserEngine; 16 | 17 | if (question.isParameterized) { 18 | this.tagStatus.questionHasTag = tagParserEngine.isAnyValidTagPresent( 19 | question.text, 20 | ); 21 | this.tagStatus.hintHasTag = tagParserEngine.isAnyValidTagPresent( 22 | question.hint, 23 | ); 24 | } 25 | } 26 | 27 | validate(): void { 28 | if ( 29 | this.question.isParameterized && 30 | this.question.parameters.length !== 0 31 | ) { 32 | if (!this.tagStatus.questionHasTag) { 33 | throw new Error( 34 | 'Parameterized question must have a valid tag in the question text.', 35 | ); 36 | } 37 | 38 | if (this.tagStatus.questionHasTag) { 39 | this.tagParserEngine.validateTags( 40 | this.question.text, 41 | this.question.parameters, 42 | ); 43 | } 44 | 45 | if (this.tagStatus.hintHasTag) { 46 | this.tagParserEngine.validateTags( 47 | this.question.hint, 48 | this.question.parameters, 49 | ); 50 | } 51 | } 52 | 53 | if ( 54 | !this.question.isParameterized && 55 | this.question.parameters.length !== 0 56 | ) { 57 | throw new Error( 58 | 'Question is not parameterized, but has parameters defined.', 59 | ); 60 | } 61 | 62 | if ( 63 | this.question.isParameterized && 64 | this.question.parameters.length === 0 65 | ) { 66 | throw new Error( 67 | 'Question is parameterized, but has no parameters defined.', 68 | ); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/question-processing/validators/DESQuestionValidator.ts: -------------------------------------------------------------------------------- 1 | import {DESQuestion} from '../../classes/transformers/Question'; 2 | import {TagParser} from 'modules/quizzes/question-processing/tag-parser/TagParser'; 3 | import {BaseQuestionValidator} from './BaseQuestionValidator'; 4 | 5 | export class DESQuestionValidator extends BaseQuestionValidator { 6 | declare question: DESQuestion; 7 | declare tagParserEngine: TagParser; 8 | 9 | constructor(question: DESQuestion, tagParserEngine: TagParser) { 10 | super(question, tagParserEngine); 11 | } 12 | 13 | validate(): void { 14 | super.validate(); 15 | if (this.question.isParameterized) { 16 | // Validate tags in question text 17 | if (this.tagParserEngine.isAnyValidTagPresent(this.question.text)) { 18 | this.tagParserEngine.validateTags( 19 | this.question.text, 20 | this.question.parameters, 21 | ); 22 | } 23 | // Validate tags in hint 24 | if ( 25 | this.question.hint && 26 | this.tagParserEngine.isAnyValidTagPresent(this.question.hint) 27 | ) { 28 | this.tagParserEngine.validateTags( 29 | this.question.hint, 30 | this.question.parameters, 31 | ); 32 | } 33 | // Validate tags in solutionText 34 | if ( 35 | this.question.solutionText && 36 | this.tagParserEngine.isAnyValidTagPresent(this.question.solutionText) 37 | ) { 38 | this.tagParserEngine.validateTags( 39 | this.question.solutionText, 40 | this.question.parameters, 41 | ); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/question-processing/validators/NATQuestionValidator.ts: -------------------------------------------------------------------------------- 1 | import {NATQuestion} from '../../classes/transformers'; 2 | import {TagParser} from 'modules/quizzes/question-processing/tag-parser/TagParser'; 3 | import {BaseQuestionValidator} from './BaseQuestionValidator'; 4 | 5 | export class NATQuestionValidator extends BaseQuestionValidator { 6 | declare question: NATQuestion; 7 | declare tagParserEngine: TagParser; 8 | 9 | constructor(question: NATQuestion, tagParserEngine: TagParser) { 10 | super(question, tagParserEngine); 11 | } 12 | 13 | validate(): void { 14 | super.validate(); 15 | // Parameterization-specific checks 16 | if (this.question.isParameterized) { 17 | // Validate tags in question text 18 | if (this.tagParserEngine.isAnyValidTagPresent(this.question.text)) { 19 | this.tagParserEngine.validateTags( 20 | this.question.text, 21 | this.question.parameters, 22 | ); 23 | } 24 | // Validate tags in hint 25 | if ( 26 | this.question.hint && 27 | this.tagParserEngine.isAnyValidTagPresent(this.question.hint) 28 | ) { 29 | this.tagParserEngine.validateTags( 30 | this.question.hint, 31 | this.question.parameters, 32 | ); 33 | } 34 | // Validate tags in expression (answer) 35 | if ( 36 | this.question.expression && 37 | this.tagParserEngine.isAnyValidTagPresent(this.question.expression) 38 | ) { 39 | this.tagParserEngine.validateTags( 40 | this.question.expression, 41 | this.question.parameters, 42 | ); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/question-processing/validators/OTLQuestionValidator.ts: -------------------------------------------------------------------------------- 1 | import {OTLQuestion} from '../../classes/transformers'; 2 | import {TagParser} from 'modules/quizzes/question-processing/tag-parser/TagParser'; 3 | import {ILotItem} from 'shared/interfaces/quiz'; 4 | import {BaseQuestionValidator} from './BaseQuestionValidator'; 5 | 6 | export class OTLQuestionValidator extends BaseQuestionValidator { 7 | declare tagStatus: { 8 | questionHasTag?: boolean; 9 | hintHasTag?: boolean; 10 | lotItemsWithTag?: boolean[]; 11 | anyLotItemHasTag?: boolean; 12 | anyLotItemExplainationHasTag?: boolean; 13 | }; 14 | declare question: OTLQuestion; 15 | declare tagParserEngine: TagParser; 16 | lotItems: ILotItem[]; 17 | 18 | constructor(question: OTLQuestion, tagParserEngine: TagParser) { 19 | super(question, tagParserEngine); 20 | 21 | if (question.isParameterized) { 22 | // Extract lot items from ordering 23 | this.lotItems = question.ordering.map(order => order.lotItem); 24 | this.tagStatus.anyLotItemHasTag = this.lotItems.some(item => 25 | tagParserEngine.isAnyValidTagPresent(item.text), 26 | ); 27 | this.tagStatus.anyLotItemExplainationHasTag = this.lotItems.some(item => 28 | tagParserEngine.isAnyValidTagPresent(item.explaination), 29 | ); 30 | this.tagStatus.lotItemsWithTag = this.lotItems.map( 31 | item => 32 | tagParserEngine.isAnyValidTagPresent(item.text) || 33 | tagParserEngine.isAnyValidTagPresent(item.explaination), 34 | ); 35 | } 36 | } 37 | 38 | validate(): void { 39 | super.validate(); 40 | // Parameterization-specific checks 41 | if (this.question.isParameterized) { 42 | if (!this.tagStatus.anyLotItemHasTag) { 43 | throw new Error('At least one LotItem must contain a valid tag.'); 44 | } else { 45 | this.tagStatus.lotItemsWithTag?.forEach((hasTag, index) => { 46 | const item = this.lotItems[index]; 47 | if (this.tagParserEngine.isAnyValidTagPresent(item.text)) { 48 | this.tagParserEngine.validateTags( 49 | item.text, 50 | this.question.parameters, 51 | ); 52 | } 53 | if (this.tagParserEngine.isAnyValidTagPresent(item.explaination)) { 54 | this.tagParserEngine.validateTags( 55 | item.explaination, 56 | this.question.parameters, 57 | ); 58 | } 59 | }); 60 | } 61 | 62 | if (this.tagStatus.hintHasTag) { 63 | this.tagParserEngine.validateTags( 64 | this.question.hint, 65 | this.question.parameters, 66 | ); 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/question-processing/validators/SMLQuestionValidator.ts: -------------------------------------------------------------------------------- 1 | import {SMLQuestion} from '../../classes/transformers'; 2 | import {TagParser} from 'modules/quizzes/question-processing/tag-parser/TagParser'; 3 | import {ILotItem} from 'shared/interfaces/quiz'; 4 | import {BaseQuestionValidator} from './BaseQuestionValidator'; 5 | 6 | export class SMLQuestionValidator extends BaseQuestionValidator { 7 | declare tagStatus: { 8 | questionHasTag?: boolean; 9 | hintHasTag?: boolean; 10 | lotItemsWithTag?: boolean[]; 11 | anyLotItemHasTag?: boolean; 12 | anyLotItemExplainationHasTag?: boolean; 13 | }; 14 | declare question: SMLQuestion; 15 | declare tagParserEngine: TagParser; 16 | lotItems: ILotItem[]; 17 | 18 | constructor(question: SMLQuestion, tagParserEngine: TagParser) { 19 | super(question, tagParserEngine); 20 | 21 | if (question.isParameterized) { 22 | this.lotItems = [ 23 | ...question.correctLotItems, 24 | ...question.incorrectLotItems, 25 | ]; 26 | this.tagStatus.anyLotItemHasTag = this.lotItems.some(item => 27 | tagParserEngine.isAnyValidTagPresent(item.text), 28 | ); 29 | this.tagStatus.anyLotItemExplainationHasTag = this.lotItems.some(item => 30 | tagParserEngine.isAnyValidTagPresent(item.explaination), 31 | ); 32 | this.tagStatus.lotItemsWithTag = this.lotItems.map( 33 | item => 34 | tagParserEngine.isAnyValidTagPresent(item.text) || 35 | tagParserEngine.isAnyValidTagPresent(item.explaination), 36 | ); 37 | } 38 | } 39 | 40 | validate(): void { 41 | super.validate(); 42 | // Parameterization-specific checks 43 | if (this.question.isParameterized) { 44 | if (!this.tagStatus.anyLotItemHasTag) { 45 | throw new Error('At least one LotItem must contain a valid tag.'); 46 | } else { 47 | this.tagStatus.lotItemsWithTag?.forEach((hasTag, index) => { 48 | const item = this.lotItems[index]; 49 | if (this.tagParserEngine.isAnyValidTagPresent(item.text)) { 50 | this.tagParserEngine.validateTags( 51 | item.text, 52 | this.question.parameters, 53 | ); 54 | } 55 | if (this.tagParserEngine.isAnyValidTagPresent(item.explaination)) { 56 | this.tagParserEngine.validateTags( 57 | item.explaination, 58 | this.question.parameters, 59 | ); 60 | } 61 | }); 62 | } 63 | 64 | if (this.tagStatus.hintHasTag) { 65 | this.tagParserEngine.validateTags( 66 | this.question.hint, 67 | this.question.parameters, 68 | ); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/question-processing/validators/SOLQuestionValidator.ts: -------------------------------------------------------------------------------- 1 | import {SOLQuestion} from '../../classes/transformers'; 2 | import {TagParser} from 'modules/quizzes/question-processing/tag-parser/TagParser'; 3 | import {ILotItem} from 'shared/interfaces/quiz'; 4 | import {BaseQuestionValidator} from './BaseQuestionValidator'; 5 | 6 | export class SOLQuestionValidator extends BaseQuestionValidator { 7 | declare tagStatus: { 8 | questionHasTag?: boolean; 9 | hintHasTag?: boolean; 10 | lotItemsWithTag?: boolean[]; 11 | anyLotItemHasTag?: boolean; 12 | anyLotItemExplainationHasTag?: boolean; 13 | }; 14 | declare question: SOLQuestion; 15 | declare tagParserEngine: TagParser; 16 | lotItems: ILotItem[]; 17 | 18 | constructor(question: SOLQuestion, tagParserEngine: TagParser) { 19 | super(question, tagParserEngine); 20 | 21 | if (question.isParameterized) { 22 | this.lotItems = [question.correctLotItem, ...question.incorrectLotItems]; 23 | this.tagStatus.anyLotItemHasTag = this.lotItems.some(item => 24 | tagParserEngine.isAnyValidTagPresent(item.text), 25 | ); 26 | this.tagStatus.anyLotItemExplainationHasTag = this.lotItems.some(item => 27 | tagParserEngine.isAnyValidTagPresent(item.explaination), 28 | ); 29 | this.tagStatus.lotItemsWithTag = this.lotItems.map( 30 | item => 31 | tagParserEngine.isAnyValidTagPresent(item.text) || 32 | tagParserEngine.isAnyValidTagPresent(item.explaination), 33 | ); 34 | } 35 | } 36 | 37 | validate(): void { 38 | super.validate(); 39 | // Parameterization-specific checks 40 | if (this.question.isParameterized) { 41 | if (!this.tagStatus.anyLotItemHasTag) { 42 | throw new Error('At least one LotItem must contain a valid tag.'); 43 | } 44 | 45 | if ( 46 | this.tagStatus.anyLotItemHasTag || 47 | this.tagStatus.anyLotItemExplainationHasTag 48 | ) { 49 | this.tagStatus.lotItemsWithTag?.forEach((hasTag, index) => { 50 | const item = this.lotItems[index]; 51 | if (this.tagParserEngine.isAnyValidTagPresent(item.text)) { 52 | this.tagParserEngine.validateTags( 53 | item.text, 54 | this.question.parameters, 55 | ); 56 | } 57 | if (this.tagParserEngine.isAnyValidTagPresent(item.explaination)) { 58 | this.tagParserEngine.validateTags( 59 | item.explaination, 60 | this.question.parameters, 61 | ); 62 | } 63 | }); 64 | } 65 | 66 | if (this.tagStatus.hintHasTag) { 67 | this.tagParserEngine.validateTags( 68 | this.question.hint, 69 | this.question.parameters, 70 | ); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/question-processing/validators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BaseQuestionValidator'; 2 | export * from './SOLQuestionValidator'; 3 | export * from './SMLQuestionValidator'; 4 | export * from './OTLQuestionValidator'; 5 | export * from './NATQuestionValidator'; 6 | export * from './DESQuestionValidator'; 7 | -------------------------------------------------------------------------------- /backend/src/modules/quizzes/services/QuestionService.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/continuousactivelearning/vibe/ec151a7a5a3b1b89b3b1564b30bd9db23f10176d/backend/src/modules/quizzes/services/QuestionService.ts -------------------------------------------------------------------------------- /backend/src/modules/quizzes/services/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/continuousactivelearning/vibe/ec151a7a5a3b1b89b3b1564b30bd9db23f10176d/backend/src/modules/quizzes/services/index.ts -------------------------------------------------------------------------------- /backend/src/modules/quizzes/utils/functions/generateRandomParameterMap.ts: -------------------------------------------------------------------------------- 1 | import {QuestionParameter} from 'modules/quizzes/classes/validators'; 2 | 3 | function generate(parameter: QuestionParameter): string | number { 4 | const values = parameter.possibleValues; 5 | const randIndex = Math.floor(Math.random() * values.length); 6 | return parameter.type === 'number' 7 | ? Number(values[randIndex]) 8 | : values[randIndex]; 9 | } 10 | 11 | function generateRandomParameterMap(params: QuestionParameter[]): ParameterMap { 12 | const map: ParameterMap = {}; 13 | for (const p of params) { 14 | map[p.name] = generate(p); 15 | } 16 | return map; 17 | } 18 | 19 | interface ParameterMap { 20 | [key: string]: string | number; 21 | } 22 | 23 | export {generateRandomParameterMap}; 24 | -------------------------------------------------------------------------------- /backend/src/modules/users/classes/transformers/Enrollment.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import {Expose, Transform, Type} from 'class-transformer'; 3 | import {ObjectId} from 'mongodb'; 4 | import { 5 | ObjectIdToString, 6 | StringToObjectId, 7 | } from 'shared/constants/transformerConstants'; 8 | import {IEnrollment} from 'shared/interfaces/Models'; 9 | import {ID} from 'shared/types'; 10 | 11 | @Expose() 12 | export class Enrollment implements IEnrollment { 13 | @Expose({toClassOnly: true}) 14 | @Transform(ObjectIdToString.transformer, {toPlainOnly: true}) 15 | @Transform(StringToObjectId.transformer, {toClassOnly: true}) 16 | _id?: ID; 17 | 18 | @Expose() 19 | @Transform(ObjectIdToString.transformer, {toPlainOnly: true}) 20 | @Transform(StringToObjectId.transformer, {toClassOnly: true}) 21 | userId: ID; 22 | 23 | @Expose() 24 | @Transform(ObjectIdToString.transformer, {toPlainOnly: true}) 25 | @Transform(StringToObjectId.transformer, {toClassOnly: true}) 26 | courseId: ID; 27 | 28 | @Expose() 29 | @Transform(ObjectIdToString.transformer, {toPlainOnly: true}) 30 | @Transform(StringToObjectId.transformer, {toClassOnly: true}) 31 | courseVersionId: ID; 32 | 33 | @Expose() 34 | status: 'active' | 'inactive'; 35 | 36 | @Expose() 37 | @Type(() => Date) 38 | enrollmentDate: Date; 39 | 40 | constructor(userId?: string, courseId?: string, courseVersionId?: string) { 41 | if (userId && courseId && courseVersionId) { 42 | this.userId = new ObjectId(userId); 43 | this.courseId = new ObjectId(courseId); 44 | this.courseVersionId = new ObjectId(courseVersionId); 45 | this.status = 'active'; 46 | this.enrollmentDate = new Date(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /backend/src/modules/users/classes/transformers/Progress.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import {Expose, Transform, Type} from 'class-transformer'; 3 | import {ObjectId} from 'mongodb'; 4 | import { 5 | ObjectIdToString, 6 | StringToObjectId, 7 | } from 'shared/constants/transformerConstants'; 8 | import {IProgress} from 'shared/interfaces/Models'; 9 | import {ID} from 'shared/types'; 10 | 11 | @Expose() 12 | export class Progress implements IProgress { 13 | @Expose() 14 | @Transform(ObjectIdToString.transformer, {toPlainOnly: true}) 15 | @Transform(StringToObjectId.transformer, {toClassOnly: true}) 16 | _id?: ID; 17 | 18 | @Expose() 19 | @Transform(ObjectIdToString.transformer, {toPlainOnly: true}) 20 | @Transform(StringToObjectId.transformer, {toClassOnly: true}) 21 | userId: ID; 22 | 23 | @Expose() 24 | @Transform(ObjectIdToString.transformer, {toPlainOnly: true}) 25 | @Transform(StringToObjectId.transformer, {toClassOnly: true}) 26 | courseId: ID; 27 | 28 | @Expose() 29 | @Transform(ObjectIdToString.transformer, {toPlainOnly: true}) 30 | @Transform(StringToObjectId.transformer, {toClassOnly: true}) 31 | courseVersionId: ID; 32 | 33 | @Expose() 34 | @Transform(ObjectIdToString.transformer, {toPlainOnly: true}) 35 | @Transform(StringToObjectId.transformer, {toClassOnly: true}) 36 | currentModule: ID; 37 | 38 | @Expose() 39 | @Transform(ObjectIdToString.transformer, {toPlainOnly: true}) 40 | @Transform(StringToObjectId.transformer, {toClassOnly: true}) 41 | currentSection: ID; 42 | 43 | @Expose() 44 | @Transform(ObjectIdToString.transformer, {toPlainOnly: true}) 45 | @Transform(StringToObjectId.transformer, {toClassOnly: true}) 46 | currentItem: ID; 47 | 48 | @Expose() 49 | completed: boolean; 50 | 51 | constructor( 52 | userId?: string, 53 | courseId?: string, 54 | courseVersionId?: string, 55 | currentModule?: string, 56 | currentSection?: string, 57 | currentItem?: string, 58 | completed = false, 59 | ) { 60 | if ( 61 | userId && 62 | courseId && 63 | courseVersionId && 64 | currentModule && 65 | currentSection && 66 | currentItem 67 | ) { 68 | this.userId = new ObjectId(userId); 69 | this.courseId = new ObjectId(courseId); 70 | this.courseVersionId = new ObjectId(courseVersionId); 71 | this.currentModule = new ObjectId(currentModule); 72 | this.currentSection = new ObjectId(currentSection); 73 | this.currentItem = new ObjectId(currentItem); 74 | this.completed = completed; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /backend/src/modules/users/classes/transformers/index.ts: -------------------------------------------------------------------------------- 1 | import {Expose, Type} from 'class-transformer'; 2 | import {Enrollment} from './Enrollment'; 3 | import {Progress} from './Progress'; 4 | import {Token} from 'typedi'; 5 | 6 | export * from './Enrollment'; 7 | export * from './Progress'; 8 | 9 | @Expose({toPlainOnly: true}) 10 | export class EnrollUserResponse { 11 | @Expose() 12 | @Type(() => Enrollment) 13 | enrollment: Enrollment; 14 | 15 | @Expose() 16 | @Type(() => Progress) 17 | progress: Progress; 18 | 19 | constructor(enrollment: Enrollment, progress: Progress) { 20 | this.enrollment = enrollment; 21 | this.progress = progress; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /backend/src/modules/users/classes/validators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './EnrollmentValidators'; 2 | export * from './ProgressValidators'; 3 | -------------------------------------------------------------------------------- /backend/src/modules/users/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './EnrollmentController'; 2 | export * from './ProgressController'; 3 | -------------------------------------------------------------------------------- /backend/src/modules/users/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './EnrollmentService'; 2 | -------------------------------------------------------------------------------- /backend/src/modules/users/tests/utils/createEnrollment.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import Express from 'express'; 3 | import {r} from '@faker-js/faker/dist/airline-BUL6NtOJ'; 4 | 5 | export interface EnrollmentParams { 6 | userId: string; 7 | courseId: string; 8 | courseVersionId: string; 9 | } 10 | 11 | export async function createEnrollment( 12 | app: typeof Express, 13 | userId: string, 14 | courseId: string, 15 | courseVersionId: string, 16 | firstModuleId: string, 17 | firstSectionId: string, 18 | firstItemId: string, 19 | ) { 20 | // Perform the request, and assert status 21 | const response = await request(app) 22 | .post( 23 | `/users/${userId}/enrollments/courses/${courseId}/versions/${courseVersionId}`, 24 | ) 25 | .expect(200); 26 | 27 | // Build up the expected “shape” of the response 28 | const expectedShape = { 29 | enrollment: { 30 | userId, 31 | courseId, 32 | courseVersionId, 33 | }, 34 | progress: { 35 | currentModule: firstModuleId, 36 | currentSection: firstSectionId, 37 | currentItem: firstItemId, 38 | }, 39 | }; 40 | 41 | // assert that the response body contains at least those fields with those exact values 42 | expect(response.body).toMatchObject(expectedShape); 43 | return response.body; // Return the response body for further assertions if needed 44 | } 45 | -------------------------------------------------------------------------------- /backend/src/modules/users/tests/utils/createUser.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import {faker} from '@faker-js/faker'; 3 | import Express from 'express'; 4 | import {IUser} from 'shared/interfaces/Models'; 5 | 6 | export async function createUser(app: typeof Express): Promise { 7 | // Prepare user sign-up data using Faker 8 | const signUpBody = { 9 | email: faker.internet.email(), 10 | password: faker.internet.password(), 11 | firstName: faker.person.firstName().replace(/[^a-zA-Z]/g, ''), 12 | lastName: faker.person.lastName().replace(/[^a-zA-Z]/g, ''), 13 | }; 14 | 15 | // Send POST request to sign up the user 16 | const signUpRes = await request(app) 17 | .post('/auth/signup') 18 | .send(signUpBody) 19 | .expect(201); // Expecting a 201 created status 20 | 21 | // Return the user object 22 | return signUpRes.body as IUser; // Assuming the response body contains the user object 23 | } 24 | -------------------------------------------------------------------------------- /backend/src/modules/users/tests/utils/startStopAndUpdateProgress.ts: -------------------------------------------------------------------------------- 1 | // utils/testProgressTracking.ts 2 | import request from 'supertest'; 3 | import Express from 'express'; 4 | import {ProgressService} from '../../services/ProgressService'; 5 | 6 | export async function startStopAndUpdateProgress({ 7 | userId, 8 | courseId, 9 | courseVersionId, 10 | itemId, 11 | moduleId, 12 | sectionId, 13 | app, 14 | }: { 15 | userId: string; 16 | courseId: string; 17 | courseVersionId: string; 18 | itemId: string; 19 | moduleId: string; 20 | sectionId: string; 21 | app: typeof Express; 22 | }) { 23 | // Start the item progress 24 | const startItemBody = {itemId, moduleId, sectionId}; 25 | const startItemResponse = await request(app) 26 | .post( 27 | `/users/${userId}/progress/courses/${courseId}/versions/${courseVersionId}/start`, 28 | ) 29 | .send(startItemBody) 30 | .expect(200); 31 | 32 | // Stop the item progress 33 | const stopItemBody = { 34 | sectionId, 35 | moduleId, 36 | itemId, 37 | watchItemId: startItemResponse.body.watchItemId, 38 | }; 39 | const stopItemResponse = await request(app) 40 | .post( 41 | `/users/${userId}/progress/courses/${courseId}/versions/${courseVersionId}/stop`, 42 | ) 43 | .send(stopItemBody) 44 | .expect(200); 45 | 46 | // Update the progress 47 | const updateProgressBody = { 48 | moduleId, 49 | sectionId, 50 | itemId, 51 | watchItemId: startItemResponse.body.watchItemId, 52 | }; 53 | 54 | jest 55 | .spyOn(ProgressService.prototype as any, 'isValidWatchTime') 56 | .mockReturnValueOnce(true); 57 | 58 | const updateProgressResponse = await request(app) 59 | .patch( 60 | `/users/${userId}/progress/courses/${courseId}/versions/${courseVersionId}/update`, 61 | ) 62 | .send(updateProgressBody) 63 | .expect(200); 64 | 65 | return {startItemResponse, stopItemResponse, updateProgressResponse}; 66 | } 67 | -------------------------------------------------------------------------------- /backend/src/modules/users/tests/utils/verifyProgressInDatabase.ts: -------------------------------------------------------------------------------- 1 | // utils/testProgressVerification.ts 2 | import request from 'supertest'; 3 | import Express from 'express'; 4 | 5 | export async function verifyProgressInDatabase({ 6 | userId, 7 | courseId, 8 | courseVersionId, 9 | expectedModuleId, 10 | expectedSectionId, 11 | expectedItemId, 12 | expectedCompleted, 13 | app, 14 | }: { 15 | userId: string; 16 | courseId: string; 17 | courseVersionId: string; 18 | expectedModuleId: string; 19 | expectedSectionId: string; 20 | expectedItemId: string; 21 | expectedCompleted: boolean; 22 | app: typeof Express; 23 | }) { 24 | // Find and verify the progress in the database 25 | const findUpdatedProgress = await request(app) 26 | .get( 27 | `/users/${userId}/progress/courses/${courseId}/versions/${courseVersionId}`, 28 | ) 29 | .expect(200); 30 | 31 | // Validate progress data 32 | const progressData = findUpdatedProgress.body; 33 | 34 | // Check that the progress data matches the expected values 35 | expect(progressData).toMatchObject({ 36 | userId, 37 | courseId, 38 | courseVersionId, 39 | currentModule: expectedModuleId, 40 | currentSection: expectedSectionId, 41 | currentItem: expectedItemId, 42 | completed: expectedCompleted, 43 | }); 44 | 45 | return progressData; // Optionally return the progress data if needed for further use 46 | } 47 | -------------------------------------------------------------------------------- /backend/src/shared/constants/transformerConstants.ts: -------------------------------------------------------------------------------- 1 | import {TransformFnParams} from 'class-transformer'; 2 | import {ObjectId} from 'mongodb'; 3 | 4 | type TransformerOptions = { 5 | transformer: (params: TransformFnParams) => unknown; 6 | }; 7 | 8 | const ObjectIdToString: TransformerOptions = { 9 | transformer: ({value}) => 10 | value instanceof ObjectId ? value.toString() : value, 11 | }; 12 | 13 | const StringToObjectId: TransformerOptions = { 14 | transformer: ({value}) => 15 | typeof value === 'string' ? new ObjectId(value) : value, 16 | }; 17 | 18 | const ObjectIdArrayToStringArray: TransformerOptions = { 19 | transformer: ({value}) => 20 | Array.isArray(value) ? value.map(v => v.toString()) : value, 21 | }; 22 | 23 | const StringArrayToObjectIdArray: TransformerOptions = { 24 | transformer: ({value}) => 25 | Array.isArray(value) ? value.map(v => new ObjectId(v)) : value, 26 | }; 27 | 28 | export { 29 | ObjectIdToString, 30 | StringToObjectId, 31 | ObjectIdArrayToStringArray, 32 | StringArrayToObjectIdArray, 33 | TransformerOptions, 34 | }; 35 | -------------------------------------------------------------------------------- /backend/src/shared/database/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interfaces/IDatabase'; 2 | export * from './interfaces/IUserRepository'; 3 | export * from './interfaces/ICourseRepository'; 4 | export * from './interfaces/IItemRepository'; 5 | -------------------------------------------------------------------------------- /backend/src/shared/database/interfaces/ICourseRepository.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CourseVersion, 3 | ItemsGroup, 4 | Module, 5 | } from 'modules/courses/classes/transformers/index'; 6 | import { 7 | ICourse, 8 | ICourseVersion, 9 | IEnrollment, 10 | IModule, 11 | IProgress, 12 | } from 'shared/interfaces/Models'; 13 | import { 14 | ClientSession, 15 | DeleteResult, 16 | MongoClient, 17 | ObjectId, 18 | UpdateResult, 19 | } from 'mongodb'; 20 | 21 | export interface ICourseRepository { 22 | getDBClient(): Promise; 23 | 24 | create(course: ICourse, session?: ClientSession): Promise; 25 | read(id: string): Promise; 26 | update( 27 | id: string, 28 | course: Partial, 29 | session?: ClientSession, 30 | ): Promise; 31 | delete(id: string): Promise; 32 | 33 | createVersion( 34 | courseVersion: ICourseVersion, 35 | session?: ClientSession, 36 | ): Promise; 37 | readVersion( 38 | versionId: string, 39 | session?: ClientSession, 40 | ): Promise; 41 | updateVersion( 42 | versionId: string, 43 | courseVersion: ICourseVersion, 44 | session?: ClientSession, 45 | ): Promise; 46 | deleteVersion( 47 | courseId: string, 48 | versionId: string, 49 | itemGroupsIds: ObjectId[], 50 | session?: ClientSession, 51 | ): Promise; 52 | deleteSection( 53 | versionId: string, 54 | moduleId: string, 55 | sectionId: string, 56 | courseVersion: ICourseVersion, 57 | session?: ClientSession, 58 | ): Promise; 59 | } 60 | -------------------------------------------------------------------------------- /backend/src/shared/database/interfaces/IDatabase.ts: -------------------------------------------------------------------------------- 1 | export interface IDatabase { 2 | database: T | null; 3 | disconnect(): Promise; 4 | isConnected(): boolean; 5 | } 6 | -------------------------------------------------------------------------------- /backend/src/shared/database/interfaces/IItemRepository.ts: -------------------------------------------------------------------------------- 1 | import {ItemsGroup} from 'modules/courses/classes/transformers/index'; 2 | import { 3 | IBaseItem, 4 | IVideoDetails, 5 | IQuizDetails, 6 | IBlogDetails, 7 | ICourseVersion, 8 | } from 'shared/interfaces/Models'; 9 | import {ObjectId, ClientSession} from 'mongodb'; 10 | 11 | export interface IItemRepository { 12 | readItem( 13 | courseVersionId: string, 14 | itemId: string, 15 | session?: ClientSession, 16 | ): Promise; 17 | 18 | deleteItem( 19 | itemGroupsId: string, 20 | itemId: string, 21 | session?: ClientSession, 22 | ): Promise; 23 | 24 | createItemsGroup( 25 | itemsGroup: ItemsGroup, 26 | session?: ClientSession, 27 | ): Promise; 28 | 29 | readItemsGroup( 30 | itemsGroupId: string, 31 | session?: ClientSession, 32 | ): Promise; 33 | 34 | updateItemsGroup( 35 | itemsGroupId: string, 36 | itemsGroup: ItemsGroup, 37 | session?: ClientSession, 38 | ): Promise; 39 | 40 | getFirstOrderItems( 41 | courseVersionId: string, 42 | session?: ClientSession, 43 | ): Promise<{ 44 | moduleId: ObjectId; 45 | sectionId: ObjectId; 46 | itemId: ObjectId; 47 | }>; 48 | 49 | // createVideoDetails(details: IVideoDetails): Promise; 50 | // createQuizDetails(details: IQuizDetails): Promise; 51 | // createBlogDetails(details: IBlogDetails): Promise; 52 | 53 | // readVideoDetails(detailsId: string): Promise; 54 | // readQuizDetails(detailsId: string): Promise; 55 | // readBlogDetails(detailsId: string): Promise; 56 | } 57 | -------------------------------------------------------------------------------- /backend/src/shared/database/interfaces/IUserRepository.ts: -------------------------------------------------------------------------------- 1 | import {ClientSession, MongoClient} from 'mongodb'; 2 | import {IUser} from 'shared/interfaces/Models'; 3 | 4 | /** 5 | * Interface representing a repository for user-related operations. 6 | */ 7 | export interface IUserRepository { 8 | /** 9 | * Get the Client of the Repository. 10 | * @returns A promise that resolves when the initialization is complete. 11 | */ 12 | getDBClient(): Promise; 13 | /** 14 | * Creates a new user. 15 | * @param user - The user to create. 16 | * @returns A promise that resolves to the created user. 17 | */ 18 | create(user: IUser, session?: ClientSession): Promise; 19 | 20 | /** 21 | * Finds a user by their email. 22 | * @param email - The email of the user to find. 23 | * @returns A promise that resolves to the user if found, or null if not found. 24 | */ 25 | findByEmail(email: string, session?: ClientSession): Promise; 26 | 27 | /** 28 | * Adds a role to a user. 29 | * @param userId - The ID of the user to add the role to. 30 | * @param role - The role to add. 31 | * @returns A promise that resolves to the updated user if successful, or null if not. 32 | */ 33 | addRole(userId: string, role: string): Promise; 34 | 35 | /** 36 | * Removes a role from a user. 37 | * @param userId - The ID of the user to remove the role from. 38 | * @param role - The role to remove. 39 | * @returns A promise that resolves to the updated user if successful, or null if not. 40 | */ 41 | removeRole(userId: string, role: string): Promise; 42 | 43 | /** 44 | * Updates the password of a user. 45 | * @param userId - The ID of the user to update the password for. 46 | * @param password - The new password. 47 | * @returns A promise that resolves to the updated user if successful, or null if not. 48 | */ 49 | updatePassword(userId: string, password: string): Promise; 50 | } 51 | -------------------------------------------------------------------------------- /backend/src/shared/database/providers/MongoDatabaseProvider.ts: -------------------------------------------------------------------------------- 1 | // export all repositoryes and mongodatabase class 2 | 3 | export * from './mongo/repositories/UserRepository'; 4 | export * from './mongo/MongoDatabase'; 5 | -------------------------------------------------------------------------------- /backend/src/shared/errors/errors.ts: -------------------------------------------------------------------------------- 1 | import {HttpError} from 'routing-controllers'; 2 | 3 | // 400 - Bad Request 4 | export class CreateError extends HttpError { 5 | constructor(message: string) { 6 | super(400, message); 7 | this.name = 'CreateError'; 8 | } 9 | } 10 | 11 | // 500 - Internal Server Error 12 | export class ReadError extends HttpError { 13 | constructor(message: string) { 14 | super(500, message); 15 | this.name = 'ReadError'; 16 | } 17 | } 18 | 19 | // 400 - Bad Request 20 | export class UpdateError extends HttpError { 21 | constructor(message: string) { 22 | super(400, message); 23 | this.name = 'UpdateError'; 24 | } 25 | } 26 | 27 | // 400 - Bad Request 28 | export class DeleteError extends HttpError { 29 | constructor(message: string) { 30 | super(400, message); 31 | this.name = 'DeleteError'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /backend/src/shared/functions/authorizationChecker.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/continuousactivelearning/vibe/ec151a7a5a3b1b89b3b1564b30bd9db23f10176d/backend/src/shared/functions/authorizationChecker.ts -------------------------------------------------------------------------------- /backend/src/shared/interfaces/quiz.ts: -------------------------------------------------------------------------------- 1 | import {ObjectId} from 'mongodb'; 2 | 3 | type QuestionType = 4 | | 'SELECT_ONE_IN_LOT' 5 | | 'SELECT_MANY_IN_LOT' 6 | | 'ORDER_THE_LOTS' 7 | | 'NUMERIC_ANSWER_TYPE' 8 | | 'DESCRIPTIVE'; 9 | 10 | interface IQuestionParameter { 11 | name: string; 12 | possibleValues: string[]; 13 | type: 'number' | 'string'; 14 | } 15 | 16 | interface IQuestion { 17 | _id?: string | ObjectId; 18 | text: string; 19 | type: QuestionType; 20 | isParameterized: boolean; 21 | parameters?: IQuestionParameter[]; 22 | hint?: string; 23 | timeLimitSeconds: number; 24 | points: number; 25 | } 26 | 27 | interface INATSolution { 28 | decimalPrecision: number; 29 | upperLimit: number; 30 | lowerLimit: number; 31 | value?: number; 32 | expression?: string; 33 | } 34 | 35 | interface ILotItem { 36 | _id?: string | ObjectId; 37 | text: string; 38 | explaination: string; 39 | } 40 | 41 | interface ILotOrder { 42 | lotItem: ILotItem; 43 | order: number; 44 | } 45 | 46 | interface IOTLSolution { 47 | ordering: ILotOrder[]; 48 | } 49 | 50 | interface ISOLSolution { 51 | incorrectLotItems: ILotItem[]; 52 | correctLotItem: ILotItem; 53 | } 54 | 55 | interface ISMLSolution { 56 | incorrectLotItems: ILotItem[]; 57 | correctLotItems: ILotItem[]; 58 | } 59 | 60 | interface IDESSolution { 61 | solutionText: string; 62 | } 63 | 64 | type QuestionQuizView = Omit; 65 | 66 | interface ISOLQuizView extends QuestionQuizView { 67 | lot: ILotItem[]; 68 | } 69 | 70 | interface ISMLQuizView extends QuestionQuizView { 71 | lot: ILotItem[]; 72 | } 73 | 74 | interface IOTLQuizView extends QuestionQuizView { 75 | lot: ILotItem[]; 76 | } 77 | 78 | type INATQuizView = QuestionQuizView; 79 | 80 | type IDESQuizView = QuestionQuizView; 81 | 82 | export { 83 | IQuestion, 84 | IQuestionParameter, 85 | ISOLSolution, 86 | ISMLSolution, 87 | IOTLSolution, 88 | INATSolution, 89 | IDESSolution, 90 | ILotItem, 91 | ILotOrder, 92 | ISOLQuizView, 93 | ISMLQuizView, 94 | IOTLQuizView, 95 | INATQuizView, 96 | IDESQuizView, 97 | QuestionType, 98 | QuestionQuizView, 99 | }; 100 | -------------------------------------------------------------------------------- /backend/src/shared/middleware/loggingHandler.ts: -------------------------------------------------------------------------------- 1 | import {Request, Response, NextFunction} from 'express'; 2 | 3 | export function loggingHandler( 4 | req: Request, 5 | res: Response, 6 | next: NextFunction, 7 | ) { 8 | console.log( 9 | `[${new Date().toISOString()}] METHOD: [${req.method}] URL: [${req.url}] - IP: [${req.socket.remoteAddress}]`, 10 | ); 11 | 12 | res.on('finish', () => { 13 | console.log( 14 | `[${new Date().toISOString()}] METHOD: [${req.method}] URL: [${req.url}] - IP: [${req.socket.remoteAddress}] - STATUS: [${res.statusCode}]`, 15 | ); 16 | }); 17 | 18 | next(); 19 | } 20 | -------------------------------------------------------------------------------- /backend/src/shared/middleware/rateLimiter.ts: -------------------------------------------------------------------------------- 1 | import {Middleware, ExpressMiddlewareInterface} from 'routing-controllers'; 2 | import {Request, Response, NextFunction} from 'express'; 3 | import {Service} from 'typedi'; 4 | import rateLimit from 'express-rate-limit'; 5 | 6 | export const rateLimiter = rateLimit({ 7 | windowMs: 1 * 60 * 1000, // 1 minute 8 | limit: 5, // Limit each IP to 5 requests per window 9 | standardHeaders: true, // Use `RateLimit-*` headers 10 | legacyHeaders: false, // Disable `X-RateLimit-*` headers 11 | message: {error: 'Too many requests, please try again later.'}, 12 | }); 13 | 14 | export const authRateLimiter = rateLimit({ 15 | windowMs: 60 * 60 * 1000, // 1 hour 16 | limit: 3, // Limit each IP to 5 requests per window 17 | standardHeaders: true, // Use `RateLimit-*` headers 18 | legacyHeaders: false, // Disable `X-RateLimit-*` headers 19 | message: {error: 'Too many requests, please try again later.'}, 20 | }); 21 | 22 | export function AuthRateLimiter( 23 | req: Request, 24 | res: Response, 25 | next: NextFunction, 26 | ) { 27 | if (process.env.NODE_ENV === 'production') { 28 | return authRateLimiter(req, res, next); // delegate to express-rate-limit 29 | } else { 30 | next(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/src/shared/types.ts: -------------------------------------------------------------------------------- 1 | import {ObjectId} from 'mongodb'; 2 | 3 | type ID = string | ObjectId | null; 4 | 5 | export {ID}; 6 | -------------------------------------------------------------------------------- /backend/src/utils/env.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | dotenv.config(); // { path: `.env.${process.env.NODE_ENV}` } 3 | 4 | export function env(key: string, defaultValue: null | string = null): string { 5 | return process.env[key] ?? (defaultValue as string); 6 | } 7 | 8 | export function envOrFail(key: string): string { 9 | if (typeof process.env[key] === 'undefined') { 10 | throw new Error(`Environment variable ${key} is not set.`); 11 | } 12 | 13 | return process.env[key] as string; 14 | } 15 | -------------------------------------------------------------------------------- /backend/src/utils/to-bool.ts: -------------------------------------------------------------------------------- 1 | export function toBool(value: string): boolean { 2 | return value === 'true'; 3 | } 4 | -------------------------------------------------------------------------------- /backend/tests/setup.ts: -------------------------------------------------------------------------------- 1 | // import { MongoMemoryServer } from "mongodb-memory-server"; 2 | // import mongoose from "mongoose"; 3 | // import "reflect-metadata"; // Ensure decorators work 4 | 5 | // let mongoServer: MongoMemoryServer; 6 | 7 | // beforeAll(async () => { 8 | // mongoServer = await MongoMemoryServer.create(); 9 | // const mongoUri = mongoServer.getUri(); 10 | // await mongoose.connect(mongoUri); 11 | // }); 12 | 13 | // afterAll(async () => { 14 | // await mongoose.disconnect(); 15 | // await mongoServer.stop(); 16 | // }); 17 | -------------------------------------------------------------------------------- /backend/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin": ["typedoc-plugin-markdown"], 3 | "out": "./docs", 4 | "entryPoints": ["./src"], 5 | "entryPointStrategy": "expand", 6 | "exclude": [ 7 | "**/*.spec.ts", 8 | "**/*.test.ts", 9 | "**/*.tsx", 10 | "**/node_modules/**", 11 | "**/build/**", 12 | "./docusaurus/**" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /cli/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vibe", 3 | "version": "0.1.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "vibe", 9 | "version": "0.1.0", 10 | "dependencies": { 11 | "commander": "^10.0.0" 12 | }, 13 | "bin": { 14 | "vibe": "src/cli.ts" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^22.14.1" 18 | } 19 | }, 20 | "node_modules/@types/node": { 21 | "version": "22.14.1", 22 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", 23 | "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", 24 | "dev": true, 25 | "license": "MIT", 26 | "dependencies": { 27 | "undici-types": "~6.21.0" 28 | } 29 | }, 30 | "node_modules/commander": { 31 | "version": "10.0.1", 32 | "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", 33 | "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", 34 | "license": "MIT", 35 | "engines": { 36 | "node": ">=14" 37 | } 38 | }, 39 | "node_modules/undici-types": { 40 | "version": "6.21.0", 41 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", 42 | "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", 43 | "dev": true, 44 | "license": "MIT" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vibe", 3 | "version": "0.1.0", 4 | "main": "src/cli.ts", 5 | "type": "module", 6 | "bin": { 7 | "vibe": "src/cli.ts" 8 | }, 9 | "dependencies": { 10 | "@inquirer/password": "^4.0.12", 11 | "@inquirer/prompts": "^7.4.1", 12 | "commander": "^10.0.0", 13 | "mongodb-memory-server": "^10.1.4", 14 | "yargs": "^17.7.2" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^22.14.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /cli/src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { Command } from "commander"; 3 | import { runStart } from "./commands/start.ts"; 4 | import { runTest } from "./commands/test.ts"; 5 | import { runHelp } from "./commands/help.ts"; 6 | import { runSetup } from "./commands/setup.ts"; 7 | 8 | const program = new Command(); 9 | 10 | program 11 | .name("vibe") 12 | .description("ViBe Project CLI") 13 | .version("1.0.0"); 14 | 15 | program 16 | .command("start") 17 | .description("Start frontend and backend") 18 | .action(runStart); 19 | 20 | program 21 | .command("help") 22 | .description("help") 23 | .action(runHelp); 24 | 25 | program 26 | .command("test") 27 | .description("Run backend tests and check for failures") 28 | .action(runTest); 29 | 30 | program 31 | .command("setup") 32 | .description("Complete the setup of the project") 33 | .action(runSetup); 34 | 35 | program.parse(process.argv) 36 | -------------------------------------------------------------------------------- /cli/src/commands/help.ts: -------------------------------------------------------------------------------- 1 | export function runHelp() { 2 | console.log(`vibe 3 | 4 | Available commands: 5 | setup Initialize the setup 6 | start Start dev servers ("vibe start help" for more options) 7 | test Run test suites 8 | help Show this help message`) 9 | } 10 | -------------------------------------------------------------------------------- /cli/src/commands/setup.ts: -------------------------------------------------------------------------------- 1 | import { spawnSync } from "child_process"; 2 | import os from "os"; 3 | import path from "path"; 4 | import { findProjectRoot } from "../findRoot.ts"; 5 | 6 | function runStep(file: string) { 7 | const isWindows = os.platform() === "win32"; 8 | // run files in ../steps/ 9 | const result = spawnSync('pnpx', ['ts-node', file], { 10 | cwd: path.join(findProjectRoot(),"cli","src","steps"), 11 | stdio: "inherit", 12 | shell: isWindows 13 | }); 14 | if (result.error) { 15 | console.error(`Error running ${file}:`, result.error); 16 | process.exit(1); 17 | } 18 | else{ 19 | //clear the console 20 | console.clear(); 21 | } 22 | } 23 | 24 | export function runSetup() { 25 | runStep("welcome.ts"); 26 | // runStep("firebase-login.ts"); 27 | runStep("firebase-emulators.ts"); 28 | //runStep("mongodb-binary.ts"); 29 | runStep("env.ts"); 30 | } -------------------------------------------------------------------------------- /cli/src/commands/test.ts: -------------------------------------------------------------------------------- 1 | import { spawnSync } from "child_process"; 2 | import os from "os"; 3 | import path from "path"; 4 | import { findProjectRoot } from "../findRoot.ts"; 5 | 6 | function runTestProcess(name: string, cwd: string): boolean { 7 | console.log(`🧪 Running ${name} tests...`); 8 | 9 | const isWindows = os.platform() === "win32"; 10 | const result = spawnSync("pnpm", ["run", "test:ci"], { 11 | cwd, 12 | stdio: "inherit", 13 | shell: isWindows 14 | }); 15 | 16 | if (result.status === 0) { 17 | console.log(`✅ ${name} tests passed.`); 18 | return true; 19 | } else { 20 | console.error(`❌ ${name} tests failed.`); 21 | return false; 22 | } 23 | } 24 | 25 | export async function runTest() { 26 | console.log("🧪 Running full test suite..."); 27 | const root = findProjectRoot(); 28 | 29 | if (!root) { 30 | console.error("❌ Please run this command from within the vibe project directory."); 31 | process.exit(1); 32 | } 33 | const backendDir = path.join(root, "backend"); 34 | const frontendDir = path.join(root, "frontend"); 35 | const backendPassed = runTestProcess("Backend", backendDir); 36 | const frontendPassed = runTestProcess("Frontend", frontendDir); 37 | 38 | if (!backendPassed || !frontendPassed) { 39 | console.error("❌ One or more test suites failed."); 40 | process.exit(1); 41 | } 42 | 43 | console.log("🎉 All tests passed successfully."); 44 | } 45 | -------------------------------------------------------------------------------- /cli/src/findRoot.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | // Function to get the root directory of the Git repo 6 | function getGitRootDir() { 7 | try { 8 | // Check if inside a Git repo 9 | const gitRootDir = execSync('git rev-parse --show-toplevel').toString().trim(); 10 | return gitRootDir; 11 | } catch (err) { 12 | console.error('Not inside a Git repository.'); 13 | return null; 14 | } 15 | } 16 | 17 | // Function to check the repository name in package.json 18 | function checkRepoName(rootDir) { 19 | try { 20 | const packageJsonPath = path.join(rootDir, 'package.json'); 21 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); 22 | 23 | const expectedRepoName = 'vibe'; 24 | if (packageJson.name === expectedRepoName) { 25 | return true; 26 | } else { 27 | return false; 28 | } 29 | } catch (err) { 30 | console.error('Error reading package.json:', err); 31 | return false; 32 | } 33 | } 34 | 35 | export function findProjectRoot() { 36 | const rootDir = getGitRootDir(); 37 | if (rootDir) { 38 | if (checkRepoName(rootDir)) { 39 | return rootDir; 40 | } else { 41 | return null 42 | } 43 | } else { 44 | return null 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /cli/src/steps/env.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fs from "fs"; 3 | import path from "path"; 4 | import { findProjectRoot } from "../findRoot.ts"; 5 | import password from '@inquirer/password'; 6 | 7 | const rootDir = findProjectRoot(); 8 | const backendDir = path.join(rootDir, "backend"); 9 | const statePath = path.join(rootDir, ".vibe.json"); 10 | const envPath = path.join(backendDir, ".env"); 11 | 12 | // Constants 13 | const STEP_NAME = "Env Variables"; 14 | 15 | async function getMongoUri() { 16 | const userPassword = await password({ 17 | message: 'Enter your MongoDB password:', 18 | mask: '*', 19 | }); 20 | 21 | const encodedPassword = encodeURIComponent(userPassword); 22 | const uriTemplate = 'mongodb+srv://Vibe:@vibe-test.jt5wz7s.mongodb.net/?retryWrites=true&w=majority&appName=vibe-test'; 23 | const finalUri = uriTemplate.replace('', encodedPassword); 24 | 25 | console.log('\nYour MongoDB URI:'); 26 | console.log(finalUri); 27 | 28 | return finalUri; 29 | } 30 | 31 | // Read state 32 | function readState(): Record { 33 | if (fs.existsSync(statePath)) { 34 | return JSON.parse(fs.readFileSync(statePath, "utf8")); 35 | } 36 | return {}; 37 | } 38 | 39 | // Write state 40 | function writeState(state: Record) { 41 | fs.writeFileSync(statePath, JSON.stringify(state, null, 2)); 42 | } 43 | 44 | // Validate MongoDB URI 45 | function isValidMongoUri(uri: string): boolean { 46 | return uri.startsWith("mongodb://") || uri.startsWith("mongodb+srv://"); 47 | } 48 | 49 | const state = readState(); 50 | 51 | if (state[STEP_NAME]) { 52 | console.log("✅ Environment variables already set. Skipping."); 53 | process.exit(0); 54 | } 55 | 56 | console.log(` 57 | 📄 Creating .env file for MongoDB 58 | `); 59 | 60 | const mongoUri = await getMongoUri(); 61 | 62 | // Write to .env file 63 | fs.writeFileSync(envPath, `DB_URL="${mongoUri}"\nFIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099\nFIREBASE_EMULATOR_HOST=127.0.0.1:4000\nGCLOUD_PROJECT=demo-test`); 64 | console.log(`✅ Wrote MongoDB URI to ${envPath}`); 65 | 66 | // Save step complete 67 | state[STEP_NAME] = true; 68 | writeState(state); 69 | -------------------------------------------------------------------------------- /cli/src/steps/firebase-emulators.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fs from "fs"; 3 | import path from "path"; 4 | import { execSync } from "child_process"; 5 | import { findProjectRoot } from "../findRoot.ts"; 6 | 7 | const rootDir = findProjectRoot(); 8 | const backendDir = path.join(rootDir, "backend"); 9 | const statePath = path.join(rootDir, ".vibe.json"); 10 | 11 | // Step name for state tracking 12 | const STEP_NAME = "Emulators"; 13 | 14 | // Load setup state 15 | function readState(): Record { 16 | if (fs.existsSync(statePath)) { 17 | return JSON.parse(fs.readFileSync(statePath, "utf8")); 18 | } 19 | return {}; 20 | } 21 | 22 | // Save updated state 23 | function writeState(state: Record) { 24 | fs.writeFileSync(statePath, JSON.stringify(state, null, 2)); 25 | } 26 | 27 | const state = readState(); 28 | 29 | // Skip step if already done 30 | if (state[STEP_NAME]) { 31 | console.log("✅ Firebase emulators already initialized. Skipping."); 32 | process.exit(0); 33 | } 34 | 35 | const firebasercPath = path.join(backendDir, ".firebaserc"); 36 | const firebaseJsonPath = path.join(backendDir, "firebase.json"); 37 | 38 | if (fs.existsSync(firebasercPath) && fs.existsSync(firebaseJsonPath)) { 39 | console.log("Config files already exist."); 40 | state[STEP_NAME] = true; 41 | writeState(state); 42 | process.exit(0); 43 | } 44 | 45 | console.log(` 46 | 📦 Initializing Firebase Emulators... 47 | 48 | Please choose ONLY the following emulators when prompted: 49 | 50 | ✔ Authentication Emulator 51 | ✔ Functions Emulator 52 | ✔ Emulator UI [optional but recommended] 53 | `); 54 | 55 | try { 56 | execSync("firebase init emulators", { 57 | cwd: backendDir, 58 | stdio: "inherit" 59 | }); 60 | 61 | state[STEP_NAME] = true; 62 | writeState(state); 63 | 64 | console.log("✅ Firebase emulators initialized successfully."); 65 | } catch (err) { 66 | console.error("❌ Failed to initialize Firebase emulators."); 67 | process.exit(1); 68 | } 69 | -------------------------------------------------------------------------------- /cli/src/steps/firebase-login.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fs from "fs"; 3 | import path from "path"; 4 | import { execSync } from "child_process"; 5 | import { findProjectRoot } from "../findRoot.ts"; 6 | 7 | const stateFile = path.resolve(findProjectRoot(), ".vibe.json"); 8 | 9 | // Constants 10 | const STEP_NAME = "Firebase Login"; 11 | 12 | // Read setup state 13 | function readState(): Record { 14 | if (fs.existsSync(stateFile)) { 15 | return JSON.parse(fs.readFileSync(stateFile, "utf-8")); 16 | } 17 | return {}; 18 | } 19 | 20 | // Write setup state 21 | function writeState(state: Record) { 22 | fs.writeFileSync(stateFile, JSON.stringify(state, null, 2)); 23 | } 24 | 25 | // Load state 26 | const state = readState(); 27 | 28 | // Skip if already logged in 29 | if (state[STEP_NAME]) { 30 | console.log("✅ Firebase login already verified. Skipping."); 31 | process.exit(0); 32 | } 33 | 34 | console.log("🔐 Checking Firebase login..."); 35 | 36 | try { 37 | const result = execSync("firebase login:list", { 38 | encoding: "utf-8", 39 | stdio: "pipe" // So we can capture the output 40 | }); 41 | 42 | if (result.includes("No authorized accounts")) { 43 | console.log("🔓 No authorized Firebase accounts found. Logging in..."); 44 | execSync("firebase login", { stdio: "inherit" }); 45 | } else { 46 | console.log("✅ Firebase already logged in."); 47 | } 48 | } catch (err) { 49 | console.error("❌ Error checking Firebase login status."); 50 | process.exit(1); 51 | } 52 | 53 | // Mark step complete 54 | state[STEP_NAME] = true; 55 | writeState(state); 56 | console.log("✅ Firebase login step complete."); 57 | -------------------------------------------------------------------------------- /cli/src/steps/mongodb-binary.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fs from "fs"; 3 | import path from "path"; 4 | import { MongoMemoryServer } from "mongodb-memory-server"; 5 | import { findProjectRoot } from "../findRoot.ts"; 6 | 7 | 8 | const rootDir = findProjectRoot(); 9 | const statePath = path.join(rootDir, ".vibe.json"); 10 | 11 | // Step name constant 12 | const STEP_NAME = "MongoDB Test Binaries"; 13 | 14 | // Read .vibe.json state 15 | function readState(): Record { 16 | if (fs.existsSync(statePath)) { 17 | return JSON.parse(fs.readFileSync(statePath, "utf8")); 18 | } 19 | return {}; 20 | } 21 | 22 | // Write updated state 23 | function writeState(state: Record) { 24 | fs.writeFileSync(statePath, JSON.stringify(state, null, 2)); 25 | } 26 | 27 | // Load state 28 | const state = readState(); 29 | 30 | if (state[STEP_NAME]) { 31 | console.log("✅ MongoDB binaries already ensured. Skipping."); 32 | process.exit(0); 33 | } 34 | 35 | console.log("⬇️ Ensuring MongoDB binaries for mongodb-memory-server..."); 36 | 37 | try { 38 | const mongod = await MongoMemoryServer.create(); 39 | await mongod.getUri(); // Triggers binary download 40 | await mongod.stop(); 41 | 42 | state[STEP_NAME] = true; 43 | writeState(state); 44 | console.log("✅ MongoDB test binaries downloaded and ready."); 45 | } catch (err) { 46 | console.error("❌ Failed to download MongoDB binaries."); 47 | console.error(err); 48 | process.exit(1); 49 | } 50 | -------------------------------------------------------------------------------- /cli/src/steps/welcome.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { select } from "@inquirer/prompts"; 3 | import fs from "fs"; 4 | import os from "os"; 5 | import path from "path"; 6 | import { findProjectRoot } from "../findRoot.ts"; 7 | 8 | // Path to .vibe.json state file 9 | const statePath = path.resolve(findProjectRoot(), ".vibe.json"); 10 | 11 | // Read existing state 12 | function readState(): Record { 13 | if (fs.existsSync(statePath)) { 14 | const raw = fs.readFileSync(statePath, "utf-8"); 15 | return JSON.parse(raw); 16 | } 17 | return {}; 18 | } 19 | 20 | // Write updated state 21 | function writeState(state: Record) { 22 | fs.writeFileSync(statePath, JSON.stringify(state, null, 2)); 23 | } 24 | 25 | // Load state 26 | const state = readState(); 27 | 28 | // If step already completed, skip 29 | if (state["Welcome"]) { 30 | console.log("✅ Welcome step already completed."); 31 | process.exit(0); 32 | } 33 | 34 | console.log("\n🚀 Welcome to the ViBe Setup!\n"); 35 | 36 | const platform = os.platform(); // 'win32', 'linux', 'darwin' 37 | console.log(`🔍 Detected platform: ${platform}`); 38 | 39 | // Prompt user to select environment 40 | const environment = await select({ 41 | message: "Choose environment:", 42 | choices: [ 43 | { name: "Development", value: "Development" }, 44 | { name: "Production", value: "Production" } 45 | ] 46 | }); 47 | 48 | // Production path isn't implemented yet 49 | if (environment === "Production") { 50 | console.error("❌ Production setup is not ready yet."); 51 | process.exit(1); 52 | } 53 | 54 | // Save environment choice to state 55 | state["environment"] = environment; 56 | state["Welcome"] = true; 57 | writeState(state); 58 | 59 | console.log(`✅ Environment set to: ${environment}`); 60 | -------------------------------------------------------------------------------- /cli/templates/test-file.txt: -------------------------------------------------------------------------------- 1 | import { MongoMemoryServer } from 'mongodb-memory-server'; 2 | import request from 'supertest'; 3 | import Express from 'express'; 4 | import { useExpressServer } from 'routing-controllers'; 5 | import { Container } from 'typedi'; 6 | 7 | // TODO: Update the import paths below to your project's structure 8 | import { MongoDatabase } from '/MongoDatabase'; 9 | import { Repository } from '/Repository'; 10 | import { ModuleOptions } from ''; 11 | 12 | describe(' Controller Integration Tests', () => { 13 | const appInstance = Express(); 14 | let app; 15 | let mongoServer: MongoMemoryServer; 16 | 17 | beforeAll(async () => { 18 | // Start an in-memory MongoDB servera 19 | mongoServer = await MongoMemoryServer.create(); 20 | const uri = mongoServer.getUri(); 21 | 22 | // Set up the real MongoDatabase and Repository 23 | Container.set('Database', new MongoDatabase(uri, '')); 24 | const repo = new Repository( 25 | Container.get('Database'), 26 | ); 27 | Container.set('Repo', repo); 28 | 29 | // Create the Express app with routing-controllers configuration 30 | app = useExpressServer(appInstance, ModuleOptions); 31 | }); 32 | 33 | afterAll(async () => { 34 | // Stop the in-memory MongoDB server 35 | await mongoServer.stop(); 36 | }); 37 | 38 | beforeEach(async () => { 39 | // TODO: Optionally reset database state before each test 40 | }); 41 | 42 | // ------Tests for Create ------ 43 | describe('CREATE ', () => { 44 | // it('should ...', async () => { 45 | // // Write your test here 46 | // }); 47 | }); 48 | 49 | // ------Tests for Read ------ 50 | describe('READ ', () => { 51 | // it('should ...', async () => { 52 | // // Write your test here 53 | // }); 54 | }); 55 | 56 | // ------Tests for Update ------ 57 | describe('UPDATE ', () => { 58 | // it('should ...', async () => { 59 | // // Write your test here 60 | // }); 61 | }); 62 | 63 | // ------Tests for Delete ------ 64 | describe('DELETE ', () => { 65 | // it('should ...', async () => { 66 | // // Write your test here 67 | // }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /docs/blog/2019-05-28-first-blog-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: first-blog-post 3 | title: First Blog Post 4 | authors: [slorber, yangshun] 5 | tags: [hola, docusaurus] 6 | --- 7 | 8 | Lorem ipsum dolor sit amet... 9 | 10 | 11 | 12 | ...consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet 13 | -------------------------------------------------------------------------------- /docs/blog/2021-08-01-mdx-blog-post.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: mdx-blog-post 3 | title: MDX Blog Post 4 | authors: [slorber] 5 | tags: [docusaurus] 6 | --- 7 | 8 | Blog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/). 9 | 10 | :::tip 11 | 12 | Use the power of React to create interactive blog posts. 13 | 14 | ::: 15 | 16 | {/* truncate */} 17 | 18 | For example, use JSX to create an interactive button: 19 | 20 | ```js 21 | 22 | ``` 23 | 24 | 25 | -------------------------------------------------------------------------------- /docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/continuousactivelearning/vibe/ec151a7a5a3b1b89b3b1564b30bd9db23f10176d/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg -------------------------------------------------------------------------------- /docs/blog/2021-08-26-welcome/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: welcome 3 | title: Welcome 4 | authors: [slorber, yangshun] 5 | tags: [facebook, hello, docusaurus] 6 | --- 7 | 8 | [Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog). 9 | 10 | Here are a few tips you might find useful. 11 | 12 | 13 | 14 | Simply add Markdown files (or folders) to the `blog` directory. 15 | 16 | Regular blog authors can be added to `authors.yml`. 17 | 18 | The blog post date can be extracted from filenames, such as: 19 | 20 | - `2019-05-30-welcome.md` 21 | - `2019-05-30-welcome/index.md` 22 | 23 | A blog post folder can be convenient to co-locate blog post images: 24 | 25 | ![Docusaurus Plushie](./docusaurus-plushie-banner.jpeg) 26 | 27 | The blog supports tags as well! 28 | 29 | **And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config. 30 | -------------------------------------------------------------------------------- /docs/blog/authors.yml: -------------------------------------------------------------------------------- 1 | yangshun: 2 | name: Yangshun Tay 3 | title: Front End Engineer @ Facebook 4 | url: https://github.com/yangshun 5 | image_url: https://github.com/yangshun.png 6 | page: true 7 | socials: 8 | x: yangshunz 9 | github: yangshun 10 | 11 | slorber: 12 | name: Sébastien Lorber 13 | title: Docusaurus maintainer 14 | url: https://sebastienlorber.com 15 | image_url: https://github.com/slorber.png 16 | page: 17 | # customize the url of the author page at /blog/authors/ 18 | permalink: '/all-sebastien-lorber-articles' 19 | socials: 20 | x: sebastienlorber 21 | linkedin: sebastienlorber 22 | github: slorber 23 | newsletter: https://thisweekinreact.com 24 | -------------------------------------------------------------------------------- /docs/blog/tags.yml: -------------------------------------------------------------------------------- 1 | facebook: 2 | label: Facebook 3 | permalink: /facebook 4 | description: Facebook tag description 5 | 6 | hello: 7 | label: Hello 8 | permalink: /hello 9 | description: Hello tag description 10 | 11 | docusaurus: 12 | label: Docusaurus 13 | permalink: /docusaurus 14 | description: Docusaurus tag description 15 | 16 | hola: 17 | label: Hola 18 | permalink: /hola 19 | description: Hola tag description 20 | -------------------------------------------------------------------------------- /docs/docs/api/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "API Documentation", 3 | "position": 6, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Documentation of the codebase of ViBe." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/docs/api/backend/Backend.md: -------------------------------------------------------------------------------- 1 | ## Modules 2 | 3 | - [auth](Other/auth.md) 4 | - [courses](Other/courses.md) 5 | -------------------------------------------------------------------------------- /docs/docs/api/backend/Courses/Transformers/courses.ItemsGroup.md: -------------------------------------------------------------------------------- 1 | Defined in: [backend/src/modules/courses/classes/transformers/Item.ts:84](https://github.com/continuousactivelearning/vibe/blob/2acbe3b478970855555eb5e714d2dc1713e5937b/backend/src/modules/courses/classes/transformers/Item.ts#L84) 2 | 3 | Items Group data transformation. 4 | 5 | ## Constructors 6 | 7 | ### Constructor 8 | 9 | > **new ItemsGroup**(`sectionId?`, `items?`): `ItemsGroup` 10 | 11 | Defined in: [backend/src/modules/courses/classes/transformers/Item.ts:99](https://github.com/continuousactivelearning/vibe/blob/2acbe3b478970855555eb5e714d2dc1713e5937b/backend/src/modules/courses/classes/transformers/Item.ts#L99) 12 | 13 | #### Parameters 14 | 15 | ##### sectionId? 16 | 17 | `ID` 18 | 19 | ##### items? 20 | 21 | [`Item`](Item/courses.Item.md)[] 22 | 23 | #### Returns 24 | 25 | `ItemsGroup` 26 | 27 | ## Properties 28 | 29 | ### \_id? 30 | 31 | > `optional` **\_id**: `ID` 32 | 33 | Defined in: [backend/src/modules/courses/classes/transformers/Item.ts:88](https://github.com/continuousactivelearning/vibe/blob/2acbe3b478970855555eb5e714d2dc1713e5937b/backend/src/modules/courses/classes/transformers/Item.ts#L88) 34 | 35 | *** 36 | 37 | ### items 38 | 39 | > **items**: [`Item`](Item/courses.Item.md)[] 40 | 41 | Defined in: [backend/src/modules/courses/classes/transformers/Item.ts:92](https://github.com/continuousactivelearning/vibe/blob/2acbe3b478970855555eb5e714d2dc1713e5937b/backend/src/modules/courses/classes/transformers/Item.ts#L92) 42 | 43 | *** 44 | 45 | ### sectionId 46 | 47 | > **sectionId**: `ID` 48 | 49 | Defined in: [backend/src/modules/courses/classes/transformers/Item.ts:97](https://github.com/continuousactivelearning/vibe/blob/2acbe3b478970855555eb5e714d2dc1713e5937b/backend/src/modules/courses/classes/transformers/Item.ts#L97) 50 | -------------------------------------------------------------------------------- /docs/docs/api/backend/Courses/Validators/CourseVersionValidators/courses.DeleteCourseVersionParams.md: -------------------------------------------------------------------------------- 1 | Defined in: [backend/src/modules/courses/classes/validators/CourseVersionValidators.ts:67](https://github.com/continuousactivelearning/vibe/blob/2acbe3b478970855555eb5e714d2dc1713e5937b/backend/src/modules/courses/classes/validators/CourseVersionValidators.ts#L67) 2 | 3 | Route parameters for deleting a course version by ID. 4 | 5 | ## Constructors 6 | 7 | ### Constructor 8 | 9 | > **new DeleteCourseVersionParams**(): `DeleteCourseVersionParams` 10 | 11 | #### Returns 12 | 13 | `DeleteCourseVersionParams` 14 | 15 | ## Properties 16 | 17 | ### courseId 18 | 19 | > **courseId**: `string` 20 | 21 | Defined in: [backend/src/modules/courses/classes/validators/CourseVersionValidators.ts:77](https://github.com/continuousactivelearning/vibe/blob/2acbe3b478970855555eb5e714d2dc1713e5937b/backend/src/modules/courses/classes/validators/CourseVersionValidators.ts#L77) 22 | 23 | *** 24 | 25 | ### versionId 26 | 27 | > **versionId**: `string` 28 | 29 | Defined in: [backend/src/modules/courses/classes/validators/CourseVersionValidators.ts:73](https://github.com/continuousactivelearning/vibe/blob/2acbe3b478970855555eb5e714d2dc1713e5937b/backend/src/modules/courses/classes/validators/CourseVersionValidators.ts#L73) 30 | 31 | ID of the course version to delete. 32 | -------------------------------------------------------------------------------- /docs/docs/api/backend/Courses/Validators/CourseVersionValidators/courses.DeleteModuleParams.md: -------------------------------------------------------------------------------- 1 | Defined in: [backend/src/modules/courses/classes/validators/ModuleValidators.ts:189](https://github.com/continuousactivelearning/vibe/blob/2acbe3b478970855555eb5e714d2dc1713e5937b/backend/src/modules/courses/classes/validators/ModuleValidators.ts#L189) 2 | 3 | Route parameters for deleting a module from a course version. 4 | 5 | ## Constructors 6 | 7 | ### Constructor 8 | 9 | > **new DeleteModuleParams**(): `DeleteModuleParams` 10 | 11 | #### Returns 12 | 13 | `DeleteModuleParams` 14 | 15 | ## Properties 16 | 17 | ### moduleId 18 | 19 | > **moduleId**: `string` 20 | 21 | Defined in: [backend/src/modules/courses/classes/validators/ModuleValidators.ts:202](https://github.com/continuousactivelearning/vibe/blob/2acbe3b478970855555eb5e714d2dc1713e5937b/backend/src/modules/courses/classes/validators/ModuleValidators.ts#L202) 22 | 23 | ID of the module to delete. 24 | 25 | *** 26 | 27 | ### versionId 28 | 29 | > **versionId**: `string` 30 | 31 | Defined in: [backend/src/modules/courses/classes/validators/ModuleValidators.ts:195](https://github.com/continuousactivelearning/vibe/blob/2acbe3b478970855555eb5e714d2dc1713e5937b/backend/src/modules/courses/classes/validators/ModuleValidators.ts#L195) 32 | 33 | ID of the course version. 34 | -------------------------------------------------------------------------------- /docs/docs/api/backend/Other/auth.authModuleOptions.md: -------------------------------------------------------------------------------- 1 | > `const` **authModuleOptions**: `RoutingControllersOptions` 2 | 3 | Defined in: [backend/src/modules/auth/index.ts:54](https://github.com/continuousactivelearning/vibe/blob/2acbe3b478970855555eb5e714d2dc1713e5937b/backend/src/modules/auth/index.ts#L54) 4 | -------------------------------------------------------------------------------- /docs/docs/api/backend/Other/auth.md: -------------------------------------------------------------------------------- 1 | ## Classes 2 | 3 | ### Auth/Errors 4 | 5 | - [ChangePasswordError](../Auth/Errors/auth.ChangePasswordError.md) 6 | 7 | ### Auth/Services 8 | 9 | - [FirebaseAuthService](../Auth/Services/auth.FirebaseAuthService.md) 10 | 11 | ### Other 12 | 13 | - [AuthController](auth.AuthController.md) 14 | - [AuthErrorResponse](auth.AuthErrorResponse.md) 15 | - [ChangePasswordBody](auth.ChangePasswordBody.md) 16 | - [ChangePasswordResponse](auth.ChangePasswordResponse.md) 17 | - [SignUpBody](auth.SignUpBody.md) 18 | - [SignUpResponse](auth.SignUpResponse.md) 19 | - [TokenVerificationResponse](auth.TokenVerificationResponse.md) 20 | 21 | ## Interfaces 22 | 23 | ### Auth/Interfaces 24 | 25 | - [IAuthService](../Auth/Interfaces/auth.IAuthService.md) 26 | 27 | ## Variables 28 | 29 | - [authModuleOptions](auth.authModuleOptions.md) 30 | 31 | ## Functions 32 | 33 | - [setupAuthModuleDependencies](auth.setupAuthModuleDependencies.md) 34 | -------------------------------------------------------------------------------- /docs/docs/api/backend/Other/auth.setupAuthModuleDependencies.md: -------------------------------------------------------------------------------- 1 | > **setupAuthModuleDependencies**(): `void` 2 | 3 | Defined in: [backend/src/modules/auth/index.ts:29](https://github.com/continuousactivelearning/vibe/blob/2acbe3b478970855555eb5e714d2dc1713e5937b/backend/src/modules/auth/index.ts#L29) 4 | 5 | ## Returns 6 | 7 | `void` 8 | -------------------------------------------------------------------------------- /docs/docs/api/backend/Other/courses.coursesModuleOptions.md: -------------------------------------------------------------------------------- 1 | > `const` **coursesModuleOptions**: `RoutingControllersOptions` 2 | 3 | Defined in: [backend/src/modules/courses/index.ts:34](https://github.com/continuousactivelearning/vibe/blob/2acbe3b478970855555eb5e714d2dc1713e5937b/backend/src/modules/courses/index.ts#L34) 4 | -------------------------------------------------------------------------------- /docs/docs/api/backend/Other/courses.setupCoursesModuleDependencies.md: -------------------------------------------------------------------------------- 1 | > **setupCoursesModuleDependencies**(): `void` 2 | 3 | Defined in: [backend/src/modules/courses/index.ts:16](https://github.com/continuousactivelearning/vibe/blob/2acbe3b478970855555eb5e714d2dc1713e5937b/backend/src/modules/courses/index.ts#L16) 4 | 5 | ## Returns 6 | 7 | `void` 8 | -------------------------------------------------------------------------------- /docs/docs/cli/intro.md: -------------------------------------------------------------------------------- 1 | # Using the CLI 2 | 3 | This guide provides an overview of how to use the ViBe Command Line Interface (CLI) effectively. 4 | 5 | --- 6 | 7 | ## ⚙️ Installation 8 | 9 | The CLI is installed automatically when you run the initial setup scripts. No additional steps are required. 10 | 11 | --- 12 | 13 | ## 🚀 Available Commands 14 | 15 | Use the following commands to interact with the CLI. For a complete list of available commands, run: 16 | 17 | ``` bash 18 | vibe help 19 | ``` 20 | 21 | ### `vibe setup` 22 | 23 | Initializes the project by configuring Firebase and MongoDB. 24 | 25 | ### `vibe start ...` 26 | 27 | Starts one or more services. For example: `vibe start backend` starts the backend server. You can also pass multiple arguments: `vibe start frontend backend` 28 | 29 | 30 | If no arguments are provided, both the frontend and backend services will be started by default. 31 | 32 | ### `vibe test` 33 | 34 | Runs the test suite for both the frontend and backend services. 35 | 36 | --- -------------------------------------------------------------------------------- /docs/docs/concepts/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Concepts", 3 | "position": 3, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "The Core Concepts of ViBe." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/docs/contributing/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Contributing", 3 | "position": 4, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Guides for Contributing to ViBe" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/docs/contributing/conventions/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Conventions", 3 | "position": 2, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Guides for Conventions in ViBe" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/docs/contributing/conventions/commit-guide.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Commit Convention 3 | --- 4 | 5 | ## Commit Convention 6 | 7 | We follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification for all commits in this project. This ensures that our commit messages are clear and consistent. 8 | 9 | The commit message format is: 10 | 11 | ``` 12 | (): 13 | ``` 14 | 15 | - **``**: Indicates the kind of change (e.g., `feat`, `fix`, `doc`, etc.). 16 | - **``** (optional): Specifies the area of the codebase affected (e.g., `auth`, `courses`, `item`). 17 | - **``**: A brief, imperative description of the change. 18 | 19 | For more details, please refer to the [Conventional Commits documentation](https://www.conventionalcommits.org/en/v1.0.0/). 20 | -------------------------------------------------------------------------------- /docs/docs/contributing/conventions/naming-guide.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: Naming Conventions 4 | 5 | --- 6 | 7 | # Naming Conventions 8 | 9 | This document outlines the naming conventions for our TypeScript codebase, as enforced by [gts](https://github.com/google/gts) and our project guidelines. 10 | 11 | --- 12 | 13 | ## Files 14 | 15 | - **Files Containing a Single Class or Function:** 16 | - **Class File:** If a file contains a single class, the file name should exactly match the class name (using PascalCase). 17 | _Example:_ A file containing the `UserService` class should be named `UserService.ts`. 18 | - **Function File:** If a file contains a single function, the file name should exactly match the function name (using camelCase). 19 | _Example:_ A file containing the function `getUser` should be named `getUser.ts`. 20 | 21 | - **Other Files:** 22 | For files containing multiple classes or functions, use PascalCase with context relavent name. 23 | _Example:_ `UtilsHelper.ts` 24 | 25 | --- 26 | 27 | ## Variables and Functions 28 | 29 | - **Variables & Function Names:** 30 | Use **camelCase**. 31 | _Examples:_ `getUser`, `calculateTotal`, `userName` 32 | 33 | --- 34 | 35 | ## Classes 36 | 37 | - **Class Names:** 38 | Use **PascalCase**. 39 | _Example:_ `UserService` 40 | 41 | --- 42 | 43 | ## Interfaces 44 | 45 | - **Interface Names:** 46 | Prefix interface names with an **I** and use **PascalCase**. 47 | _Examples:_ `IUser`, `IAuthConfig` 48 | 49 | --- 50 | 51 | ## Enums 52 | 53 | - **Enum Names:** 54 | Use **PascalCase** for enum names. 55 | _Examples:_ `UserRole` 56 | - **Enum Values:** 57 | Use **UPPER_SNAKE_CASE** for enum values. 58 | _Examples:_ `ADMIN`, `USER` 59 | 60 | --- 61 | 62 | ## Generics 63 | 64 | - **Generic Type Parameters:** 65 | Use a single uppercase letter or a descriptive name if necessary. 66 | _Examples:_ `T`, `K`, `V` 67 | 68 | --- 69 | 70 | ## Additional Guidelines 71 | 72 | - **Remove Module Name Prefixes:** 73 | Do not include module names in file or type names. 74 | - **Consistency:** 75 | Use these conventions consistently across the codebase for clarity and maintainability. 76 | 77 | For more details, please refer to the [gts documentation](https://github.com/google/gts) and ask in discussions if you have any questions. -------------------------------------------------------------------------------- /docs/docs/development/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Development", 3 | "position": 5, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "The Core Concepts for Development of ViBe" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/docs/development/architecture.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: System Architecture 3 | --- 4 | 5 | ViBe is a full-stack, serverless web application built for continuous active learning, designed to scale efficiently and support modular growth. It features a split frontend for students and admins, and a microservice-style backend using serverless Express functions deployed on Google Cloud. 6 | 7 | ## 🌐 Tech Stack Overview 8 | 9 | | Layer | Tech Used | 10 | |-------------|--------------------------------| 11 | | Frontend | React (Vite) | 12 | | Backend | Express.js | 13 | | Database | MongoDB (Atlas) | 14 | | Auth | Google Firebase Authentication | 15 | | Hosting | Google Cloud Functions | 16 | | Storage | Firebase Storage (or GCP Buckets) | 17 | 18 | --- 19 | 20 | ## ⚙️ Serverless Architecture 21 | 22 | The ViBe backend is composed of several independent Express modules, each deployed as a **Google Cloud Function**. This allows: 23 | - Independent scaling of services 24 | - Faster cold starts per function 25 | - Logical separation of business concerns 26 | 27 | --- 28 | 29 | ## 📦 Backend Modules 30 | 31 | Each backend service is a standalone Express app deployed as a serverless function: 32 | 33 | - `auth` – Authentication & user verification (via Firebase) 34 | - `users` – Student/teacher data 35 | - `courses` – Course structure, access control 36 | - `quizzes` – Quiz content, question rotation 37 | - `grader` – Scoring logic, bonus handling 38 | - `activity` – Monitoring video/screen presence 39 | - `ratings` – Feedback and engagement scoring 40 | - `ai` – Question generation, hinting, proctoring checks 41 | - `messenger` – Internal communication or alerting module 42 | 43 | --- 44 | 45 | ## 🎨 Frontend Layout 46 | 47 | ViBe has **two separate frontend apps**: 48 | 49 | - **Student Frontend**: The main learning interface 50 | - **Admin Frontend**: Tools for teachers to add/edit content, track progress, review contributions 51 | -------------------------------------------------------------------------------- /docs/docs/getting-started/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Getting Started", 3 | "position": 2, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Guides to get started with ViBe." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/docs/getting-started/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | --- 4 | 5 | This guide will help you set up ViBe on your local machine for development. 6 | 7 | --- 8 | You can clone the repository or directly download the setup file and run it to start the setup process. 9 | ## 🚀 Clone the Repository (Optional) 10 | 11 | ```bash 12 | git clone https://github.com/continuousactivelearning/vibe.git 13 | cd vibe 14 | ``` 15 | 16 | --- 17 | 18 | ## ⚙️ Setup Using Installation Scripts 19 | 20 | ViBe uses a custom `setup-unix.sh` and `setup-win.ps1` scripts to help initialize the development environment (both backend and frontend). 21 | 22 | ### 📦 Run the Setup 23 | 24 | ```bash 25 | chmod +x scripts/setup-unix.sh 26 | ./scripts/setup-unix.sh 27 | ``` 28 | 29 | This script will: 30 | - Check required dependencies 31 | - Install backend dependencies 32 | - Install frontend dependencies 33 | - Set up `.env` files 34 | - Installs the CLI 35 | 36 | > 🛠️ The script is interactive and will guide you step-by-step. 37 | 38 | --- 39 | 40 | ## 🧪 Run in Development Mode 41 | 42 | If you want to run services manually: 43 | 44 | ### 🖥 Frontend 45 | 46 | ```bash 47 | vibe start frontend 48 | ``` 49 | 50 | ### ⚙️ Backend 51 | 52 | ```bash 53 | vibe start backend 54 | ``` 55 | 56 | --- 57 | 58 | ## 📦 Build Docusaurus (Docs) 59 | 60 | If you're contributing to the documentation: 61 | 62 | ```bash 63 | vibe start docs 64 | ``` 65 | 66 | Visit: `http://localhost:3000/docs` 67 | 68 | --- 69 | 70 | ## 🐛 Having Issues? 71 | 72 | - Make sure all dependencies are installed correctly 73 | - Open an issue or ask in the [GitHub Discussions](https://github.com/continuousactivelearning/vibe/discussions) 74 | 75 | --- 76 | 77 | ## 📚 What's Next? 78 | 79 | - [Explore the Project Structure](./project-structure.md) 80 | - [Understand the Architecture](../development/architecture.md) 81 | -------------------------------------------------------------------------------- /docs/docs/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: What is ViBe ? 3 | sidebar_position: 1 4 | --- 5 | 6 | ## **ViBe** 7 | ### Reimagining Learning, One Question at a Time 8 | **Inspired by Vikram & Betaal: The Eternal Dialogue** 9 | 10 | In the timeless tale of *Vikram and Betaal*, learning wasn’t about passive listening — it was about questions that challenged the mind and demanded reflection. Each story ended not with answers, but with a choice to think. 11 | 12 | **ViBe carries forward that spirit.** 13 | Instead of lectures to be watched and notes to be memorized, ViBe encourages active engagement, constant reflection, and ethical learning — supported by respectful proctoring and open-source innovation. 14 | 15 | --- 16 | 17 | ### 🔄 Just like Vikram… 18 | Learners walk the path with patience and purpose, returning each time with deeper understanding. 19 | 20 | ### 🧠 Just like Betaal… 21 | The system prompts, challenges, and guides — not with judgment, but with wisdom designed to spark thought. 22 | 23 | --- 24 | 25 | **ViBe is our modern retelling.** 26 | With AI-supported checkpoints, interactive nudges, and a focus on fairness, we’re reimagining education as a conversation — not a one-way stream. 27 | -------------------------------------------------------------------------------- /docs/docs/mcp-server/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Additional Copilot tools", 3 | "position": 8, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Additional tools for GitHub Copilot" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/docs/mcp-server/setting-up.md: -------------------------------------------------------------------------------- 1 | # Setting Up MCP Servers for OpenAPI endpoints 2 | 3 | ## What is MCP? 4 | 5 | **MCP (Model Context Protocol)** is a protocol used by AI agents to access information. Here, it transforms your existing OpenAPI specifications into tools that AI agents like GitHub Copilot can use. This integration allows AI agents to directly interact with your API endpoints, making them more powerful by enabling them to fetch real-time data, perform operations, and work with your specific services. 6 | 7 | By converting your API definitions to the MCP server, you give AI agents the ability to understand and use your API's capabilities programmatically, just like a human developer would. 8 | 9 | ## Setup via automated script (Preferred) 10 | 11 | You can set up mcp server for the openapi specification just by running this in the project root: 12 | ```bash 13 | node scripts/setup-mcp.ts 14 | ``` 15 | And all set! Now, you can call for any tool inside of your AI Agent Mode. If needed in Edit mode, you can add the tool by clicking on `Add Context > Tools` and prompt the tool. 16 | 17 | 18 | ## Instructions for manual installation 19 | ### Install `openapi-mcp-generator` 20 | Install the OpenAPI MCP generator using pnpm: 21 | 22 | ```bash 23 | pnpm install openapi-mcp-generator 24 | ``` 25 | 26 | ### Generating MCP Server 27 | To convert your OpenAPI specification into MCP format, do 28 | 29 | ```bash 30 | openapi-mcp-generator --input path/to/openapi.json --output path/to/output/dir 31 | ``` 32 | In our case, the command could be 33 | ```bash 34 | openapi-mcp-generator --input backend/openapi/openapi.json --output mcp 35 | ``` 36 | 37 | This command: 38 | - Reads your OpenAPI specification file from `backend/openapi/openapi.json` 39 | - Generates MCP definitions in the `mcp` directory 40 | 41 | > Note: if you want, you can also make Streamable HTTP or Web(openAI) type servers as well. For this, add `--transport=[web/streamable-http] --port=[port]` flags 42 | 43 | ### Configuration 44 | Now, to configure the MCP server with github copilot 45 | 1. Press ctrl + shift + P 46 | 2. Search and click on `MCP: Add Server...`. 47 | 3. Select the type of MCP server you want to connect. In our scenario, it will be an stdio server 48 | 4. Type in `pnpm --dir ${workspaceFolder}/mcp start` 49 | 5. Type any name you want for this mcp server.o 50 | 51 | ### Settings Check 52 | Ensure that the setting `chat.mcp.enabled` is checked in vscode settings. To check, go to File > Preferences > Settings and search for the above setting. 53 | 54 | ## Troubleshooting 55 | 56 | If you encounter issues: 57 | 58 | 1. Verify your OpenAPI specification is valid 59 | 2. Check that your MCP server is accessible 60 | 3. Review the settings as mentioned above. 61 | -------------------------------------------------------------------------------- /docs/docs/onboarding/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Intern Onboarding", 3 | "position": 7, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Welcome to the ViBe intern onboarding documentation! This section will help you get up to speed with our tech stack, workflows, and common tasks." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "copy": "mkdir -p static/openapi && cd ../backend && pnpx ts-node -r tsconfig-paths/register scripts/generate-openapi.ts --output ../../docs/static/openapi/openapi.json", 8 | "start": "pnpm copy && docusaurus start", 9 | "build": "pnpm copy && docusaurus build", 10 | "swizzle": "docusaurus swizzle", 11 | "deploy": "docusaurus deploy", 12 | "clear": "docusaurus clear", 13 | "serve": "docusaurus serve", 14 | "write-translations": "docusaurus write-translations", 15 | "write-heading-ids": "docusaurus write-heading-ids", 16 | "typecheck": "tsc" 17 | }, 18 | "dependencies": { 19 | "@docusaurus/core": "3.7.0", 20 | "@docusaurus/preset-classic": "3.7.0", 21 | "@docusaurus/theme-mermaid": "^3.7.0", 22 | "@docusaurus/theme-search-algolia": "^3.7.0", 23 | "@mdx-js/react": "^3.0.0", 24 | "@scalar/docusaurus": "^0.6.7", 25 | "clsx": "^2.0.0", 26 | "docusaurus-plugin-openapi-docs": "^4.4.0", 27 | "docusaurus-theme-openapi-docs": "^4.4.0", 28 | "docs": "file:", 29 | "prism-react-renderer": "^2.3.0", 30 | "react": "^19.0.0", 31 | "react-dom": "^19.0.0", 32 | "yarn": "^1.22.22" 33 | }, 34 | "devDependencies": { 35 | "@docusaurus/module-type-aliases": "3.7.0", 36 | "@docusaurus/tsconfig": "3.7.0", 37 | "@docusaurus/types": "3.7.0", 38 | "docusaurus-plugin-typedoc": "^1.3.0", 39 | "typescript": "~5.6.2" 40 | }, 41 | "browserslist": { 42 | "production": [ 43 | ">0.5%", 44 | "not dead", 45 | "not op_mini all" 46 | ], 47 | "development": [ 48 | "last 3 chrome version", 49 | "last 3 firefox version", 50 | "last 5 safari version" 51 | ] 52 | }, 53 | "engines": { 54 | "node": ">=18.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /docs/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; 2 | 3 | // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) 4 | 5 | /** 6 | * Creating a sidebar enables you to: 7 | - create an ordered group of docs 8 | - render a sidebar for each doc of that group 9 | - provide next/previous navigation 10 | 11 | The sidebars can be generated from the filesystem, or explicitly defined here. 12 | 13 | Create as many sidebars as you want. 14 | */ 15 | const sidebars: SidebarsConfig = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | 'intro', 23 | 'hello', 24 | { 25 | type: 'category', 26 | label: 'Tutorial', 27 | items: ['tutorial-basics/create-a-document'], 28 | }, 29 | ], 30 | */ 31 | }; 32 | 33 | export default sidebars; 34 | -------------------------------------------------------------------------------- /docs/sidebarsNew.ts: -------------------------------------------------------------------------------- 1 | // sidebarsNew.js 2 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 3 | const sidebarsNew = { 4 | newSidebar: [ 5 | { 6 | type: 'autogenerated', 7 | dirName: '.', // scans the `newdocs/` folder by default 8 | }, 9 | ], 10 | }; 11 | 12 | export default sidebarsNew; 13 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import type {ReactNode} from 'react'; 2 | import clsx from 'clsx'; 3 | import Heading from '@theme/Heading'; 4 | import styles from './styles.module.css'; 5 | 6 | type FeatureItem = { 7 | title: string; 8 | Image: string; 9 | description: ReactNode; 10 | }; 11 | 12 | const FeatureList: FeatureItem[] = [ 13 | { 14 | title: 'Active Learning, Not Passive Watching', 15 | Image: require('@site/static/img/home_one.png').default, 16 | description: ( 17 | <> 18 | Online education often becomes a checkbox activity. 19 | ViBe changes that. By weaving in spontaneous comprehension checks, reflections, and interactive nudges, we ensure learners stay engaged and involved — not just present. 20 | 21 | ), 22 | }, 23 | { 24 | title: 'Trust Through Gentle Proctoring', 25 | Image: require('@site/static/img/home_two.png').default, 26 | description: ( 27 | <> 28 | We believe in trust, not surveillance. ViBe includes respectful presence verification — like subtle camera prompts, gesture-based checks, and environment control — to uphold fairness in assessments while keeping students comfortable and in control. 29 | 30 | ), 31 | }, 32 | { 33 | title: 'Guided by AI, Open to All', 34 | Image: require('@site/static/img/home_three.png').default, 35 | description: ( 36 | <> 37 | From content creation to learner feedback, ViBe uses AI to support both educators and learners — helping create better materials, personalized checkpoints, and responsive progress tracking. 38 | And because ViBe is open-source, everyone can contribute, access, and improve it freely. 39 | 40 | ), 41 | }, 42 | ]; 43 | 44 | function Feature({title, Image, description}: FeatureItem): ReactNode { 45 | return ( 46 |
47 |
48 | {title} 49 |
50 |
51 | {title} 52 |

{description}

53 |
54 |
55 | ); 56 | } 57 | 58 | export default function HomepageFeatures(): ReactNode { 59 | return ( 60 |
61 |
62 |
63 | {FeatureList.map((item, idx) => ( 64 | 65 | ))} 66 |
67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Global theme customization for Docusaurus with black/white base 3 | * and warm amber/red tint. 4 | */ 5 | 6 | :root { 7 | --ifm-color-primary: #ff914d; /* soft orange / amber */ 8 | --ifm-color-primary-dark: #e67e36; 9 | --ifm-color-primary-darker: #cc6f2f; 10 | --ifm-color-primary-darkest: #b35f29; 11 | --ifm-color-primary-light: #ffa263; 12 | --ifm-color-primary-lighter: #ffb380; 13 | --ifm-color-primary-lightest: #ffc499; 14 | 15 | --ifm-code-font-size: 95%; 16 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.05); 17 | 18 | /* Optional: Global fonts or overrides */ 19 | /* --ifm-font-family-base: 'Inter', sans-serif; */ 20 | } 21 | 22 | /* Dark mode: invert the base, preserve warm highlight */ 23 | [data-theme='dark'] { 24 | --ifm-color-primary: #ff9e63; /* slightly brighter in dark */ 25 | --ifm-color-primary-dark: #ff8a3d; 26 | --ifm-color-primary-darker: #e6732b; 27 | --ifm-color-primary-darkest: #cc5c1a; 28 | --ifm-color-primary-light: #ffb17d; 29 | --ifm-color-primary-lighter: #ffc69c; 30 | --ifm-color-primary-lightest: #ffd7b8; 31 | 32 | --docusaurus-highlighted-code-line-bg: rgba(255, 255, 255, 0.05); 33 | } 34 | -------------------------------------------------------------------------------- /docs/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /docs/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type {ReactNode} from 'react'; 2 | import clsx from 'clsx'; 3 | import Link from '@docusaurus/Link'; 4 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 5 | import Layout from '@theme/Layout'; 6 | import HomepageFeatures from '@site/src/components/HomepageFeatures'; 7 | import Heading from '@theme/Heading'; 8 | 9 | import styles from './index.module.css'; 10 | 11 | function HomepageHeader() { 12 | const {siteConfig} = useDocusaurusContext(); 13 | return ( 14 |
15 |
16 | 17 | {siteConfig.title} 18 | 19 |

{siteConfig.tagline}

20 |
21 | 24 | Visit Documentation 25 | 26 |
27 |
28 |
29 | ); 30 | } 31 | 32 | export default function Home(): ReactNode { 33 | const {siteConfig} = useDocusaurusContext(); 34 | return ( 35 | 38 | 39 |
40 | 41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /docs/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/continuousactivelearning/vibe/ec151a7a5a3b1b89b3b1564b30bd9db23f10176d/docs/static/.nojekyll -------------------------------------------------------------------------------- /docs/static/img/docusaurus-social-card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/continuousactivelearning/vibe/ec151a7a5a3b1b89b3b1564b30bd9db23f10176d/docs/static/img/docusaurus-social-card.jpg -------------------------------------------------------------------------------- /docs/static/img/docusaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/continuousactivelearning/vibe/ec151a7a5a3b1b89b3b1564b30bd9db23f10176d/docs/static/img/docusaurus.png -------------------------------------------------------------------------------- /docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/continuousactivelearning/vibe/ec151a7a5a3b1b89b3b1564b30bd9db23f10176d/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /docs/static/img/home_one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/continuousactivelearning/vibe/ec151a7a5a3b1b89b3b1564b30bd9db23f10176d/docs/static/img/home_one.png -------------------------------------------------------------------------------- /docs/static/img/home_three.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/continuousactivelearning/vibe/ec151a7a5a3b1b89b3b1564b30bd9db23f10176d/docs/static/img/home_three.png -------------------------------------------------------------------------------- /docs/static/img/home_two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/continuousactivelearning/vibe/ec151a7a5a3b1b89b3b1564b30bd9db23f10176d/docs/static/img/home_two.png -------------------------------------------------------------------------------- /docs/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/continuousactivelearning/vibe/ec151a7a5a3b1b89b3b1564b30bd9db23f10176d/docs/static/img/logo.png -------------------------------------------------------------------------------- /docs/static/img/logo_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/continuousactivelearning/vibe/ec151a7a5a3b1b89b3b1564b30bd9db23f10176d/docs/static/img/logo_old.png -------------------------------------------------------------------------------- /docs/static/img/logo_old2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/continuousactivelearning/vibe/ec151a7a5a3b1b89b3b1564b30bd9db23f10176d/docs/static/img/logo_old2.png -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | }, 7 | "exclude": [".docusaurus", "build"] 8 | } 9 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/", 3 | "rules": { 4 | "@typescript-eslint/no-unused-vars": "off", 5 | "no-unused-vars": "off", 6 | 7 | }, 8 | "overrides": [ 9 | { 10 | "files": ["**/*.test.ts", "**/*.spec.ts", "**/tests/**"], 11 | "rules": { 12 | "n/no-unpublished-import": "off", 13 | "@typescript-eslint/no-unused-vars": "off", 14 | "no-unused-vars": "off" 15 | } 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /frontend/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "vibe-proctoring" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /frontend/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('gts/.prettierrc.json') 3 | } 4 | -------------------------------------------------------------------------------- /frontend/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 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. -------------------------------------------------------------------------------- /frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /frontend/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import globals from "globals"; 3 | import reactHooks from "eslint-plugin-react-hooks"; 4 | import reactRefresh from "eslint-plugin-react-refresh"; 5 | import tseslint from "typescript-eslint"; 6 | 7 | export default tseslint.config( 8 | { ignores: ["dist"] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ["**/*.{ts,tsx}"], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | "react-hooks": reactHooks, 18 | "react-refresh": reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | "react-refresh/only-export-components": [ 23 | "warn", 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | } 28 | ); 29 | -------------------------------------------------------------------------------- /frontend/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "dist", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vite + React + TS 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/store.tsx: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import authReducer from "@/features/auth/auth-slice"; 3 | 4 | export const store = configureStore({ 5 | reducer: { 6 | auth: authReducer, 7 | }, 8 | }); 9 | 10 | export type RootState = ReturnType; 11 | export type AppDispatch = typeof store.dispatch; 12 | -------------------------------------------------------------------------------- /frontend/src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | function Avatar({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 19 | ) 20 | } 21 | 22 | function AvatarImage({ 23 | className, 24 | ...props 25 | }: React.ComponentProps) { 26 | return ( 27 | 32 | ) 33 | } 34 | 35 | function AvatarFallback({ 36 | className, 37 | ...props 38 | }: React.ComponentProps) { 39 | return ( 40 | 48 | ) 49 | } 50 | 51 | export { Avatar, AvatarImage, AvatarFallback } 52 | -------------------------------------------------------------------------------- /frontend/src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ); 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ); 34 | } 35 | 36 | export { Badge, badgeVariants }; 37 | -------------------------------------------------------------------------------- /frontend/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 focus-visible:ring-4 focus-visible:outline-1 aria-invalid:focus-visible:ring-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 25 | sm: "h-8 rounded-md px-3 has-[>svg]:px-2.5", 26 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 27 | icon: "size-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | function Button({ 38 | className, 39 | variant, 40 | size, 41 | asChild = false, 42 | ...props 43 | }: React.ComponentProps<"button"> & 44 | VariantProps & { 45 | asChild?: boolean 46 | }) { 47 | const Comp = asChild ? Slot : "button" 48 | 49 | return ( 50 | 55 | ) 56 | } 57 | 58 | export { Button, buttonVariants } 59 | -------------------------------------------------------------------------------- /frontend/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLDivElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |
64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /frontend/src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 2 | 3 | function Collapsible({ 4 | ...props 5 | }: React.ComponentProps) { 6 | return 7 | } 8 | 9 | function CollapsibleTrigger({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 17 | ) 18 | } 19 | 20 | function CollapsibleContent({ 21 | ...props 22 | }: React.ComponentProps) { 23 | return ( 24 | 28 | ) 29 | } 30 | 31 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 32 | -------------------------------------------------------------------------------- /frontend/src/components/ui/hover-card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const HoverCard = HoverCardPrimitive.Root 7 | 8 | const HoverCardTrigger = HoverCardPrimitive.Trigger 9 | 10 | const HoverCardContent = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 14 | 24 | )) 25 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName 26 | 27 | export { HoverCard, HoverCardTrigger, HoverCardContent } 28 | -------------------------------------------------------------------------------- /frontend/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | 16 | ) 17 | } 18 | 19 | export { Input } 20 | -------------------------------------------------------------------------------- /frontend/src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Separator({ 9 | className, 10 | orientation = "horizontal", 11 | decorative = true, 12 | ...props 13 | }: React.ComponentProps) { 14 | return ( 15 | 25 | ) 26 | } 27 | 28 | export { Separator } 29 | -------------------------------------------------------------------------------- /frontend/src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) { 4 | return ( 5 |
10 | ) 11 | } 12 | 13 | export { Skeleton } 14 | -------------------------------------------------------------------------------- /frontend/src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 | return ( 7 |