├── backend ├── src │ ├── environment-mongo │ │ ├── dto │ │ │ └── create-environment-mongo.dto.ts │ │ ├── environment-mongo.service.spec.ts │ │ ├── environment-mongo.controller.spec.ts │ │ ├── environment-mongo.entity.ts │ │ ├── environment-mongo.module.ts │ │ └── environment-mongo.controller.ts │ ├── auth │ │ ├── enums │ │ │ └── role.enum.ts │ │ ├── decorators │ │ │ ├── isAPIKey.decorator.ts │ │ │ ├── isPublic.decorator.ts │ │ │ └── roles.decorator.ts │ │ ├── serializers │ │ │ └── session.serializer.ts │ │ ├── guards │ │ │ ├── local-auth.guard.ts │ │ │ ├── roles.guard.ts │ │ │ ├── refresh-jwt-auth.guard.ts │ │ │ ├── auth.guard.ts │ │ │ └── jwt-auth.guard.ts │ │ ├── strategies │ │ │ ├── refresh.strategy.ts │ │ │ ├── local.strategy.ts │ │ │ └── jwt.strategy.ts │ │ ├── auth.module.ts │ │ └── auth.controller.spec.ts │ ├── admin │ │ ├── admin.service.ts │ │ ├── admin.service.spec.ts │ │ ├── admin.controller.spec.ts │ │ └── admin.module.ts │ ├── app.service.ts │ ├── logging │ │ ├── logging.module.ts │ │ ├── logging.decorator.ts │ │ └── logging.interceptor.ts │ ├── redis │ │ ├── redis.module.ts │ │ └── redis.service.ts │ ├── users │ │ ├── users.sql │ │ ├── users.service.spec.ts │ │ ├── users.controller.spec.ts │ │ ├── users.module.ts │ │ ├── users.docs.ts │ │ └── user.entity.ts │ ├── docker │ │ ├── docker.module.ts │ │ ├── docker.service.spec.ts │ │ ├── docker.controller.spec.ts │ │ └── docker.controller.ts │ ├── utils │ │ └── encrypt_password.ts │ ├── constants.ts │ ├── mail │ │ ├── mail.service.spec.ts │ │ ├── mail.module.ts │ │ └── templates │ │ │ ├── confirmation.hbs │ │ │ └── reset-password.hbs │ ├── guest │ │ ├── guest.service.spec.ts │ │ ├── guest.controller.spec.ts │ │ ├── guest.module.ts │ │ └── guest.controller.ts │ ├── projects │ │ ├── projects.service.spec.ts │ │ ├── projects.controller.spec.ts │ │ ├── projects.module.ts │ │ ├── project.entity.ts │ │ └── projects.docs.ts │ ├── file-mongo │ │ ├── file-mongo.service.spec.ts │ │ ├── file-mongo.controller.spec.ts │ │ ├── file-mongo.module.ts │ │ ├── file-mongo.entity.ts │ │ └── file-mongo.controller.ts │ ├── project-mongo │ │ ├── project-mongo.service.spec.ts │ │ ├── project-mongo.controller.spec.ts │ │ ├── project-mongo.module.ts │ │ └── project-mongo.entity.ts │ ├── project-manager │ │ ├── project-manager.service.spec.ts │ │ └── project-manager.module.ts │ ├── app.controller.ts │ ├── project-shares │ │ ├── project-shares.service.spec.ts │ │ ├── project-shares.controller.spec.ts │ │ ├── project-shares.module.ts │ │ └── project-shares.entity.ts │ ├── directory-mongo │ │ ├── directory-mongo.service.spec.ts │ │ ├── directory-mongo.controller.spec.ts │ │ ├── directory-mongo.module.ts │ │ ├── directory-mongo.entity.ts │ │ ├── directory-mongo.controller.ts │ │ └── dto │ │ │ └── create-directory-mongo.dto.ts │ ├── app.controller.spec.ts │ ├── config │ │ └── configuration.ts │ ├── main.ts │ └── filters │ │ └── http-exception.filter.ts ├── docker │ ├── python_container │ │ ├── requirements.txt │ │ ├── install.sh │ │ └── Dockerfile │ ├── javascript_container │ │ ├── install.sh │ │ └── Dockerfile │ └── install.sh ├── .prettierrc ├── tsconfig.build.json ├── babel.config.js ├── test │ ├── jest-e2e.json │ └── app.e2e-spec.ts ├── nest-cli.json ├── create_user.sql ├── .gitignore ├── drop_tables.sql ├── .eslintrc.js ├── .eslintrc.cjs ├── ecosystem.config.js ├── tsconfig.json ├── deploy.sh ├── create_mongodb_db.sh └── migrate.sh ├── dump.rdb ├── frontend ├── netlify.toml ├── public │ ├── aa.png │ ├── mab.png │ ├── mea.png │ ├── ro.png │ ├── banner.png │ ├── dots.png │ ├── giphy.gif │ ├── icon-b.png │ ├── icon-w.png │ ├── icon.png │ ├── logo-b.png │ ├── logo-w.png │ ├── loop.png │ ├── avatar-1.png │ ├── avatar-2.png │ ├── avatar-3.png │ ├── banner2.png │ ├── banner3.png │ ├── gradient.png │ ├── logo-bb.png │ ├── converted.gif │ ├── lang-logo │ │ ├── c.png │ │ ├── html.png │ │ ├── python.png │ │ ├── markdown.png │ │ ├── unknown.png │ │ ├── javascript.png │ │ └── typescript.png │ ├── pattern-b.gif │ ├── pattern-w.gif │ ├── editor-action.gif │ └── vite.svg ├── src │ ├── assets │ │ └── ahjHe3h.jpg │ ├── components │ │ ├── Dashboard │ │ │ ├── Dashboard.tsx │ │ │ ├── Dashboard.css │ │ │ └── DBMenu.tsx │ │ ├── CodeEditor │ │ │ ├── index.ts │ │ │ ├── codemirrorSetup.ts │ │ │ ├── names.ts │ │ │ ├── VoiceDrawer.tsx │ │ │ └── LineNumber.tsx │ │ ├── Bars │ │ │ ├── ThemeSelector.tsx │ │ │ ├── LanguageSelector.tsx │ │ │ └── Shares.tsx │ │ ├── Slogan.tsx │ │ └── Buttons │ │ │ └── CallToAction.tsx │ ├── common │ │ └── icons │ │ │ ├── index.ts │ │ │ ├── Twitter.tsx │ │ │ ├── LinkedIn.tsx │ │ │ └── Github.tsx │ ├── hooks │ │ ├── useTitle.ts │ │ ├── useApp.ts │ │ ├── useAuth.ts │ │ ├── useTypingEffect.tsx │ │ ├── useLogOut.ts │ │ └── useAuthRefresh.ts │ ├── globals.d.ts │ ├── store │ │ ├── selectors │ │ │ ├── index.ts │ │ │ ├── fileSelectors.ts │ │ │ ├── projectShareSelectors.ts │ │ │ ├── cookieConsentSelectors.ts │ │ │ ├── authSelectors.ts │ │ │ ├── userSelectors.ts │ │ │ └── projectSelectors.ts │ │ ├── middleware │ │ │ └── loggerMiddleware.ts │ │ ├── services │ │ │ ├── environment.ts │ │ │ ├── directory.ts │ │ │ ├── file.ts │ │ │ ├── admin.ts │ │ │ ├── user.ts │ │ │ └── api.ts │ │ ├── slices │ │ │ ├── projectSharesSlice.ts │ │ │ ├── fileSlice.ts │ │ │ └── cookieConsentSlice.ts │ │ └── store.ts │ ├── pages │ │ ├── index.ts │ │ └── Verify.tsx │ ├── config │ │ └── apiConfig.ts │ ├── vite-env.d.ts │ ├── utils │ │ ├── filetreeinit.ts │ │ ├── dashboard.utils.ts │ │ ├── Spinner.tsx │ │ ├── addleaf.ts │ │ ├── createfiledir.ts │ │ └── codeExamples.ts │ ├── context │ │ ├── AuthContext.tsx │ │ └── EditorContext.tsx │ ├── theme │ │ └── MenuTheme.tsx │ ├── index.css │ ├── App.tsx │ └── main.tsx ├── .prettierrc ├── tsconfig.json ├── tsconfig.node.json ├── tailwind.config.js ├── eslint.config.js ├── tsconfig.app.json ├── vite.config.ts ├── README.md └── package.json ├── rm_user.sql ├── socket_deploy.sh ├── .gitignore ├── socket_server ├── package.json └── transform.js ├── LICENSE ├── deploy.sh └── runon.sh /backend/src/environment-mongo/dto/create-environment-mongo.dto.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dump.rdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/dump.rdb -------------------------------------------------------------------------------- /backend/docker/python_container/requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | pandas 3 | requests 4 | -------------------------------------------------------------------------------- /backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /frontend/netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/*" 3 | to = "/" 4 | status = 200 5 | -------------------------------------------------------------------------------- /frontend/public/aa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/aa.png -------------------------------------------------------------------------------- /frontend/public/mab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/mab.png -------------------------------------------------------------------------------- /frontend/public/mea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/mea.png -------------------------------------------------------------------------------- /frontend/public/ro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/ro.png -------------------------------------------------------------------------------- /frontend/public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/banner.png -------------------------------------------------------------------------------- /frontend/public/dots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/dots.png -------------------------------------------------------------------------------- /frontend/public/giphy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/giphy.gif -------------------------------------------------------------------------------- /frontend/public/icon-b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/icon-b.png -------------------------------------------------------------------------------- /frontend/public/icon-w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/icon-w.png -------------------------------------------------------------------------------- /frontend/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/icon.png -------------------------------------------------------------------------------- /frontend/public/logo-b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/logo-b.png -------------------------------------------------------------------------------- /frontend/public/logo-w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/logo-w.png -------------------------------------------------------------------------------- /frontend/public/loop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/loop.png -------------------------------------------------------------------------------- /backend/docker/javascript_container/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | docker build -t node-leetcode:latest . 3 | -------------------------------------------------------------------------------- /backend/docker/python_container/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | docker build -t python-leetcode:latest . 3 | -------------------------------------------------------------------------------- /frontend/public/avatar-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/avatar-1.png -------------------------------------------------------------------------------- /frontend/public/avatar-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/avatar-2.png -------------------------------------------------------------------------------- /frontend/public/avatar-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/avatar-3.png -------------------------------------------------------------------------------- /frontend/public/banner2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/banner2.png -------------------------------------------------------------------------------- /frontend/public/banner3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/banner3.png -------------------------------------------------------------------------------- /frontend/public/gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/gradient.png -------------------------------------------------------------------------------- /frontend/public/logo-bb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/logo-bb.png -------------------------------------------------------------------------------- /frontend/public/converted.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/converted.gif -------------------------------------------------------------------------------- /frontend/public/lang-logo/c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/lang-logo/c.png -------------------------------------------------------------------------------- /frontend/public/pattern-b.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/pattern-b.gif -------------------------------------------------------------------------------- /frontend/public/pattern-w.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/pattern-w.gif -------------------------------------------------------------------------------- /frontend/src/assets/ahjHe3h.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/src/assets/ahjHe3h.jpg -------------------------------------------------------------------------------- /frontend/public/editor-action.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/editor-action.gif -------------------------------------------------------------------------------- /frontend/public/lang-logo/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/lang-logo/html.png -------------------------------------------------------------------------------- /frontend/public/lang-logo/python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/lang-logo/python.png -------------------------------------------------------------------------------- /backend/src/auth/enums/role.enum.ts: -------------------------------------------------------------------------------- 1 | export enum Role { 2 | User = 'user', 3 | Admin = 'admin', 4 | Guest = 'guest', 5 | } 6 | -------------------------------------------------------------------------------- /frontend/public/lang-logo/markdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/lang-logo/markdown.png -------------------------------------------------------------------------------- /frontend/public/lang-logo/unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/lang-logo/unknown.png -------------------------------------------------------------------------------- /frontend/src/components/Dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | export default function Dashboard() { 2 | return
Dashboard
; 3 | } 4 | -------------------------------------------------------------------------------- /frontend/public/lang-logo/javascript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/lang-logo/javascript.png -------------------------------------------------------------------------------- /frontend/public/lang-logo/typescript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reunicorn1/collabor8/HEAD/frontend/public/lang-logo/typescript.png -------------------------------------------------------------------------------- /backend/src/admin/admin.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AdminService {} 5 | -------------------------------------------------------------------------------- /backend/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "semi": true, 6 | "trailingComma": "all" 7 | } 8 | -------------------------------------------------------------------------------- /rm_user.sql: -------------------------------------------------------------------------------- 1 | use collabor8; 2 | select username,email,is_verified from Users; 3 | delete from Users where is_verified=0; 4 | select username,email,is_verified from Users; 5 | -------------------------------------------------------------------------------- /frontend/src/common/icons/index.ts: -------------------------------------------------------------------------------- 1 | export { default as LinkedIn } from './LinkedIn'; 2 | export { default as Twitter } from './Twitter'; 3 | export { default as Github } from './Github'; 4 | -------------------------------------------------------------------------------- /frontend/src/components/CodeEditor/index.ts: -------------------------------------------------------------------------------- 1 | export { default as LanguageSelector } from '../Bars/LanguageSelector'; 2 | export { default as ThemeSelector } from '../Bars/ThemeSelector'; 3 | -------------------------------------------------------------------------------- /backend/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /socket_deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # script that will run on server by another util script called `runon.sh` 3 | cd /home/ubuntu/collabor8/socket_server && \ 4 | git pull origin main && \ 5 | npm install 6 | -------------------------------------------------------------------------------- /backend/src/auth/decorators/isAPIKey.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | 3 | export const IS_API_KEY = 'isApiKey'; 4 | export const AllowApiKey = () => SetMetadata(IS_API_KEY, true); 5 | -------------------------------------------------------------------------------- /backend/src/auth/decorators/isPublic.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | 3 | export const IS_PUBLIC_KEY = 'isPublic'; 4 | export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); 5 | -------------------------------------------------------------------------------- /backend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /backend/src/logging/logging.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { LoggingInterceptor } from './logging.interceptor'; 3 | 4 | @Module({ 5 | providers: [LoggingInterceptor], 6 | }) 7 | export class LoggingModule {} 8 | -------------------------------------------------------------------------------- /frontend/src/hooks/useTitle.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | const usePageTitle = (title: string) => { 4 | useEffect(() => { 5 | document.title = title; 6 | }, [title]); 7 | }; 8 | 9 | export default usePageTitle; 10 | -------------------------------------------------------------------------------- /backend/src/redis/redis.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { RedisService } from '@redis/redis.service'; 3 | 4 | @Module({ 5 | providers: [RedisService], 6 | exports: [RedisService], 7 | }) 8 | export class RedisModule {} 9 | -------------------------------------------------------------------------------- /backend/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare var process: { 2 | env: { 3 | NODE_ENV: string; 4 | [key: string]: string | undefined; 5 | }; 6 | }; 7 | 8 | interface Window { 9 | dataLayer: any[]; 10 | gtag: (...args: any[]) => void; 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/auth/decorators/roles.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | import { Role } from '../enums/role.enum'; 3 | 4 | export const ROLES_KEY = 'roles'; 5 | export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles); 6 | -------------------------------------------------------------------------------- /backend/src/users/users.sql: -------------------------------------------------------------------------------- 1 | -- create the main database, user, and password 2 | CREATE DATABASE collabor8; 3 | 4 | CREATE USER 'collabor'@'localhost' IDENTIFIED BY 'collabor8'; 5 | 6 | GRANT ALL PRIVILEGES ON collabor8.* TO 'collabor'@'localhost'; 7 | 8 | FLUSH PRIVILEGES; 9 | -------------------------------------------------------------------------------- /frontend/src/store/selectors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './authSelectors'; 2 | export * from './userSelectors'; 3 | export * from './projectSelectors'; 4 | export * from './projectShareSelectors'; 5 | export * from './fileSelectors'; 6 | export * from './cookieConsentSelectors'; 7 | -------------------------------------------------------------------------------- /backend/src/logging/logging.decorator.ts: -------------------------------------------------------------------------------- 1 | import { applyDecorators, UseInterceptors } from '@nestjs/common'; 2 | import { LoggingInterceptor } from './logging.interceptor'; 3 | 4 | export function LogEndpoint() { 5 | return applyDecorators(UseInterceptors(LoggingInterceptor)); 6 | } 7 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.app.json" 6 | } 7 | ], 8 | "compilerOptions": { 9 | "typeRoots": [ 10 | "./node_modules/@types", 11 | "./src/types.ts" 12 | ], 13 | "allowJs": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /backend/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true, 7 | "assets": ["mail/templates/**/*"], 8 | "watchAssets": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /backend/create_user.sql: -------------------------------------------------------------------------------- 1 | -- CREATE the main DATABASE, USER, AND password 2 | DROP DATABASE IF EXISTS collabor8; 3 | 4 | CREATE DATABASE IF NOT EXISTS collabor8; 5 | 6 | CREATE USER IF NOT EXISTS 'collabor'@'localhost' IDENTIFIED BY 'collabor8'; 7 | 8 | GRANT ALL PRIVILEGES ON collabor8.* TO 'collabor'@'localhost'; 9 | 10 | FLUSH PRIVILEGES; 11 | -------------------------------------------------------------------------------- /frontend/src/pages/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Verify } from './Verify'; 2 | export { default as Home } from './Home'; 3 | export { default as About } from './About'; 4 | export { default as Editor } from './Editor'; 5 | export { default as Profile } from './Profile'; 6 | export { default as NotFound } from './404_page'; 7 | export { default as Dashboard } from './DashboardPage'; 8 | -------------------------------------------------------------------------------- /backend/src/docker/docker.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { DockerService } from './docker.service'; 3 | import { DockerController } from './docker.controller'; 4 | 5 | @Module({ 6 | imports: [], 7 | providers: [DockerService], 8 | controllers: [DockerController], 9 | exports: [DockerService], 10 | }) 11 | export class DockerModule {} 12 | -------------------------------------------------------------------------------- /backend/src/auth/serializers/session.serializer.ts: -------------------------------------------------------------------------------- 1 | import { PassportSerializer } from '@nestjs/passport'; 2 | 3 | export class SessionSerializer extends PassportSerializer { 4 | serializeUser(user: any, done: (err: Error, user: any) => void): any { 5 | done(null, user); 6 | } 7 | 8 | deserializeUser(payload: any, done: Function) { 9 | done(null, payload); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/hooks/useApp.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; 2 | import type { RootState, AppDispatch } from '@store/store'; 3 | 4 | // Use throughout the app instead of plain `useDispatch` and `useSelector` 5 | export const useAppDispatch = () => useDispatch(); 6 | export const useAppSelector: TypedUseSelectorHook = useSelector; 7 | -------------------------------------------------------------------------------- /backend/src/utils/encrypt_password.ts: -------------------------------------------------------------------------------- 1 | import * as bcrypt from 'bcrypt'; 2 | 3 | function encryptPwd(password: string): string { 4 | const salt = bcrypt.genSaltSync(10); 5 | return bcrypt.hashSync(password, salt); 6 | } 7 | 8 | function comparePwd(password: string, hash: string): Promise { 9 | return bcrypt.compare(password, hash); 10 | } 11 | 12 | export { encryptPwd, comparePwd }; 13 | -------------------------------------------------------------------------------- /frontend/src/hooks/useAuth.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import AuthContext from '@context/AuthContext'; 3 | 4 | // custom hook to use AuthContext 5 | export const useAuth = () => { 6 | const context = useContext(AuthContext); 7 | if (!context) { 8 | throw new Error('useAuth must be used within an AuthProvider'); 9 | } 10 | return context; 11 | }; 12 | 13 | export default useAuth; 14 | -------------------------------------------------------------------------------- /backend/src/constants.ts: -------------------------------------------------------------------------------- 1 | const MYSQL_CONN = 'mysqlConnection'; 2 | const MONGO_CONN = 'mongoConnection'; 3 | const GUEST_USER = `guest`; 4 | const GUEST_EMAIL = 'guest.co11abor8@gmail.com'; 5 | 6 | const jwtConstants = { 7 | // jehahahahahah 8 | secret: 9 | 't(-_-)tjsdhgahgsjgnerjhhgiaijg9a0hgawftj3e45890tyyu348twrjbyuia34hgn784', 10 | }; 11 | export { MYSQL_CONN, MONGO_CONN, jwtConstants, GUEST_USER, GUEST_EMAIL }; 12 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | pnpm-debug.log* 9 | lerna-debug.log* 10 | 11 | node_modules 12 | dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | *constants* 28 | 29 | *pk.pk* 30 | -------------------------------------------------------------------------------- /frontend/src/store/selectors/fileSelectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from '@reduxjs/toolkit'; 2 | import { RootState } from '@store/store'; 3 | 4 | export const selectFile = createSelector( 5 | (state: RootState) => state.file.file, 6 | (file) => file, 7 | ); 8 | 9 | export const selectPanelVisibility = createSelector( 10 | (state: RootState) => state.file.panelVisibility, 11 | (panelVisibility) => panelVisibility, 12 | ); 13 | -------------------------------------------------------------------------------- /backend/drop_tables.sql: -------------------------------------------------------------------------------- 1 | -- Use this script to drop all tables in the database if needed 2 | 3 | USE collabor8; 4 | 5 | -- Drop all tables if they exist 6 | SET @tables = NULL; 7 | SELECT GROUP_CONCAT(table_name) INTO @tables 8 | FROM information_schema.tables 9 | WHERE table_schema = 'collabor8'; 10 | 11 | SET @tables = CONCAT('DROP TABLE IF EXISTS ', @tables); 12 | PREPARE stmt FROM @tables; 13 | EXECUTE stmt; 14 | DEALLOCATE PREPARE stmt; 15 | -------------------------------------------------------------------------------- /backend/docker/javascript_container/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile 2 | # Use the official Node.js image 3 | FROM node:20.15 4 | 5 | # Set the working directory inside the container 6 | WORKDIR /app 7 | 8 | # Copy the application files into the container (if needed) 9 | COPY . . 10 | 11 | # Install any dependencies (if you have a package.json) 12 | # RUN npm install 13 | 14 | # Default command (this will be overridden by Dockerode) 15 | CMD ["node"] 16 | 17 | -------------------------------------------------------------------------------- /frontend/src/config/apiConfig.ts: -------------------------------------------------------------------------------- 1 | const env: { [key: string]: string } = { 2 | development: 3 | import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api/v1', 4 | test: 'http://localhost:3000/api/v1', 5 | production: 'https://collabor8.eduresource.tech/api/v1', 6 | appID: import.meta.env.VITE_AGORA_APPID || '', 7 | }; 8 | 9 | const apiConfig = { 10 | baseUrl: `${env[import.meta.env.MODE]}`, 11 | appID: `${env.appID}`, 12 | }; 13 | 14 | export default apiConfig; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | **/package-lock.json 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | pnpm-debug.log* 9 | lerna-debug.log* 10 | *_log.txt 11 | **/package-lock.json 12 | node_modules 13 | dist 14 | dist-ssr 15 | *.local 16 | *.txt 17 | .env 18 | 19 | *migration* 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | .DS_Store 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | 31 | backend/.env 32 | -------------------------------------------------------------------------------- /backend/src/auth/guards/local-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, ExecutionContext } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class LocalAuthGuard extends AuthGuard('local') { 6 | async canActivate(context: ExecutionContext): Promise { 7 | const result = (await super.canActivate(context)) as boolean; 8 | const request = context.switchToHttp().getRequest(); 9 | 10 | await super.logIn(request); 11 | return result; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /backend/src/mail/mail.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { MailService } from '@mail/mail.service'; 3 | 4 | describe('MailService', () => { 5 | let service: MailService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [MailService], 10 | }).compile(); 11 | 12 | service = module.get(MailService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /backend/src/guest/guest.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { GuestService } from './guest.service'; 3 | 4 | describe('GuestService', () => { 5 | let service: GuestService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [GuestService], 10 | }).compile(); 11 | 12 | service = module.get(GuestService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /backend/src/admin/admin.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AdminService } from '@admin/admin.service'; 3 | 4 | describe('AdminService', () => { 5 | let service: AdminService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [AdminService], 10 | }).compile(); 11 | 12 | service = module.get(AdminService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /backend/src/users/users.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UsersService } from '@users/users.service'; 3 | 4 | describe('UsersService', () => { 5 | let service: UsersService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [UsersService], 10 | }).compile(); 11 | 12 | service = module.get(UsersService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /backend/src/docker/docker.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { DockerService } from './docker.service'; 3 | 4 | describe('DockerService', () => { 5 | let service: DockerService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [DockerService], 10 | }).compile(); 11 | 12 | service = module.get(DockerService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /backend/src/guest/guest.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { GuestController } from './guest.controller'; 3 | 4 | describe('GuestController', () => { 5 | let controller: GuestController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [GuestController], 10 | }).compile(); 11 | 12 | controller = module.get(GuestController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /backend/src/projects/projects.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ProjectsService } from '@projects/projects.service'; 3 | 4 | describe('ProjectsService', () => { 5 | let service: ProjectsService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [ProjectsService], 10 | }).compile(); 11 | 12 | service = module.get(ProjectsService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2023"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "isolatedModules": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["vite.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/admin/admin.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AdminController } from '@admin/admin.controller'; 3 | 4 | describe('AdminController', () => { 5 | let controller: AdminController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [AdminController], 10 | }).compile(); 11 | 12 | controller = module.get(AdminController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /backend/src/docker/docker.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { DockerController } from './docker.controller'; 3 | 4 | describe('DockerController', () => { 5 | let controller: DockerController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [DockerController], 10 | }).compile(); 11 | 12 | controller = module.get(DockerController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /frontend/src/store/middleware/loggerMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from '@reduxjs/toolkit'; 2 | 3 | // Define the logger middleware 4 | const loggerMiddleware: Middleware = (store) => (next) => (action: any) => { 5 | console.log('Dispatching action:', action); 6 | 7 | const endpointName = action.meta?.arg?.endpointName as string | undefined; 8 | if (endpointName) { 9 | console.log('API Endpoint:', endpointName); 10 | } 11 | 12 | const result = next(action); 13 | console.log('Next state:', store.getState()); 14 | return result; 15 | }; 16 | 17 | export default loggerMiddleware; 18 | -------------------------------------------------------------------------------- /backend/src/file-mongo/file-mongo.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { FileMongoService } from '@file-mongo/file-mongo.service'; 3 | 4 | describe('FileMongoService', () => { 5 | let service: FileMongoService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [FileMongoService], 10 | }).compile(); 11 | 12 | service = module.get(FileMongoService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /backend/src/docker/docker.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Controller, Post } from '@nestjs/common'; 2 | import { DockerService } from './docker.service'; 3 | import { Public } from '@auth/decorators/isPublic.decorator'; 4 | 5 | @Controller('docker') 6 | export class DockerController { 7 | constructor(private readonly dockerService: DockerService) {} 8 | 9 | @Public() 10 | @Post('execute') 11 | async executeCode(@Request() req): Promise<{ stdout: string, stderr: string }> { 12 | return this.dockerService.executeLanguageCode(req.body.code, req.body?.filename, req.body?.language); 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /backend/src/projects/projects.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ProjectsController } from '@projects/projects.controller'; 3 | 4 | describe('ProjectsController', () => { 5 | let controller: ProjectsController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [ProjectsController], 10 | }).compile(); 11 | 12 | controller = module.get(ProjectsController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /backend/src/users/users.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UsersController } from '@users/users.controller'; 3 | 4 | // TODO: complete test 5 | describe('UsersController', () => { 6 | let controller: UsersController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [UsersController], 11 | }).compile(); 12 | 13 | controller = module.get(UsersController); 14 | }); 15 | 16 | it('should be defined', () => { 17 | expect(controller).toBeDefined(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /backend/src/project-mongo/project-mongo.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ProjectMongoService } from '@project-mongo/project-mongo.service'; 3 | 4 | describe('ProjectMongoService', () => { 5 | let service: ProjectMongoService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [ProjectMongoService], 10 | }).compile(); 11 | 12 | service = module.get(ProjectMongoService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /backend/src/file-mongo/file-mongo.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { FileMongoController } from '@file-mongo/file-mongo.controller'; 3 | 4 | describe('FileMongoController', () => { 5 | let controller: FileMongoController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [FileMongoController], 10 | }).compile(); 11 | 12 | controller = module.get(FileMongoController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /backend/src/project-manager/project-manager.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ProjectManagerService } from './project-manager.service'; 3 | 4 | describe('ProjectManagerService', () => { 5 | let service: ProjectManagerService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [ProjectManagerService], 10 | }).compile(); 11 | 12 | service = module.get(ProjectManagerService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /backend/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | import { ApiOperation, ApiTags } from '@nestjs/swagger'; 4 | import { Public } from '@auth/decorators/isPublic.decorator'; 5 | 6 | @ApiTags('AppController') 7 | @Controller() 8 | export class AppController { 9 | constructor(private readonly appService: AppService) {} 10 | 11 | @ApiOperation({ 12 | summary: 'Get Hello Message', 13 | description: 'Returns a simple greeting message.', 14 | }) 15 | @Public() 16 | @Get() 17 | getHello(): string { 18 | return this.appService.getHello(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/project-shares/project-shares.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ProjectSharesService } from '@project-shares/project-shares.service'; 3 | 4 | describe('ProjectSharesService', () => { 5 | let service: ProjectSharesService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [ProjectSharesService], 10 | }).compile(); 11 | 12 | service = module.get(ProjectSharesService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /socket_server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "collabor8", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "main": "HocuspocusServer.js", 6 | "dependencies": { 7 | "@hocuspocus/extension-logger": "^1.0.0", 8 | "@hocuspocus/server": "^1.0.0", 9 | "axios": "^1.7.4", 10 | "chalk": "^5.3.0", 11 | "dotenv": "^16.4.5", 12 | "nodemon": "^3.1.4", 13 | "quill-delta": "^5.1.0", 14 | "yjs": "^13.6.18" 15 | }, 16 | "scripts": { 17 | "start": "node HocuspocusServer.js", 18 | "dev": "NODE_ENV=development nodemon HocuspocusServer.js", 19 | "pro": "NODE_ENV=production node HocuspocusServer.js" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/directory-mongo/directory-mongo.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { DirectoryMongoService } from '@directory-mongo/directory-mongo.service'; 3 | 4 | describe('DirectoryMongoService', () => { 5 | let service: DirectoryMongoService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [DirectoryMongoService], 10 | }).compile(); 11 | 12 | service = module.get(DirectoryMongoService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /backend/src/project-mongo/project-mongo.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ProjectMongoController } from '@project-mongo/project-mongo.controller'; 3 | 4 | describe('ProjectMongoController', () => { 5 | let controller: ProjectMongoController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [ProjectMongoController], 10 | }).compile(); 11 | 12 | controller = module.get(ProjectMongoController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /backend/src/environment-mongo/environment-mongo.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { EnvironmentMongoService } from '@environment-mongo/environment-mongo.service'; 3 | 4 | describe('EnvironmentMongoService', () => { 5 | let service: EnvironmentMongoService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [EnvironmentMongoService], 10 | }).compile(); 11 | 12 | service = module.get(EnvironmentMongoService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /backend/src/project-shares/project-shares.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ProjectSharesController } from '@project-shares/project-shares.controller'; 3 | 4 | describe('ProjectSharesController', () => { 5 | let controller: ProjectSharesController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [ProjectSharesController], 10 | }).compile(); 11 | 12 | controller = module.get(ProjectSharesController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module 'y-codemirror' { 4 | import { Text } from 'yjs'; 5 | import { Editor } from 'codemirror'; 6 | import { Awareness } from 'y-websocket'; 7 | 8 | export class CodemirrorBinding { 9 | constructor( 10 | yText: Text, 11 | editor: Editor, 12 | awareness: Awareness, 13 | options?: { 14 | yUndoManager?: any; 15 | }, 16 | ); 17 | 18 | destroy(): void; 19 | } 20 | } 21 | 22 | interface ImportMetaEnv { 23 | readonly VITE_APP_TITLE: string; 24 | readonly MODE: string; 25 | } 26 | 27 | interface ImportMeta { 28 | readonly env: ImportMetaEnv; 29 | } 30 | -------------------------------------------------------------------------------- /backend/src/directory-mongo/directory-mongo.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { DirectoryMongoController } from '@directory-mongo/directory-mongo.controller'; 3 | 4 | describe('DirectoryMongoController', () => { 5 | let controller: DirectoryMongoController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [DirectoryMongoController], 10 | }).compile(); 11 | 12 | controller = module.get(DirectoryMongoController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /frontend/src/components/CodeEditor/codemirrorSetup.ts: -------------------------------------------------------------------------------- 1 | // CodeMirror base CSS 2 | import 'codemirror/lib/codemirror.css'; 3 | 4 | // CodeMirror modes 5 | import 'codemirror/mode/javascript/javascript'; 6 | import 'codemirror/mode/python/python'; 7 | import 'codemirror/mode/clike/clike'; 8 | import 'codemirror/mode/markdown/markdown'; 9 | import 'codemirror/mode/xml/xml'; 10 | 11 | // CodeMirror themes 12 | import 'codemirror/theme/dracula.css'; 13 | import 'codemirror/theme/eclipse.css'; 14 | import 'codemirror/theme/material.css'; 15 | import 'codemirror/theme/monokai.css'; 16 | import 'codemirror/theme/solarized.css'; 17 | import 'codemirror/theme/twilight.css'; 18 | import 'codemirror/theme/zenburn.css'; 19 | -------------------------------------------------------------------------------- /backend/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /backend/src/environment-mongo/environment-mongo.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { EnvironmentMongoController } from '@environment-mongo/environment-mongo.controller'; 3 | 4 | describe('EnvironmentMongoController', () => { 5 | let controller: EnvironmentMongoController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [EnvironmentMongoController], 10 | }).compile(); 11 | 12 | controller = module.get( 13 | EnvironmentMongoController, 14 | ); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(controller).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /backend/src/project-manager/project-manager.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ProjectManagerService } from './project-manager.service'; 3 | import { BullModule } from '@nestjs/bullmq'; 4 | import { RedisModule } from '@redis/redis.module'; 5 | import { ProjectsModule } from '@projects/projects.module'; 6 | import { ProjectSharesModule } from '@project-shares/project-shares.module'; 7 | 8 | @Module({ 9 | imports: [ 10 | RedisModule, 11 | ProjectsModule, 12 | ProjectSharesModule, 13 | BullModule.registerQueue({ 14 | name: 'project-manager', 15 | }), 16 | ], 17 | exports: [ProjectManagerService], 18 | providers: [ProjectManagerService] 19 | }) 20 | export class ProjectManagerModule {} 21 | -------------------------------------------------------------------------------- /backend/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /frontend/src/common/icons/Twitter.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from '@chakra-ui/react'; 2 | 3 | const Twitter = (props) => ( 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | 20 | export default Twitter; 21 | -------------------------------------------------------------------------------- /backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /frontend/src/hooks/useTypingEffect.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | function useTypingEffect(text: string, speed = 100, start = true) { 4 | const [displayedText, setDisplayedText] = useState(''); 5 | 6 | useEffect(() => { 7 | if (!start) { 8 | return; 9 | } 10 | 11 | let currentIndex = 0; 12 | const intervalId = setInterval(() => { 13 | if (currentIndex < text.length - 1) { 14 | setDisplayedText((prev) => prev + text[currentIndex]); 15 | currentIndex++; 16 | } else { 17 | clearInterval(intervalId); 18 | } 19 | }, speed); 20 | 21 | return () => clearInterval(intervalId); 22 | }, [text, speed, start]); 23 | 24 | return displayedText; 25 | } 26 | 27 | export default useTypingEffect; 28 | -------------------------------------------------------------------------------- /backend/src/auth/strategies/refresh.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { jwtConstants } from '../../constants'; 5 | 6 | @Injectable() 7 | export class RefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh') { 8 | constructor() { 9 | super({ 10 | jwtFromRequest: ExtractJwt.fromBodyField('refreshToken'), 11 | ignoreExpiration: false, 12 | secretOrKey: jwtConstants.secret, 13 | passReqToCallback: true, 14 | }); 15 | } 16 | 17 | async validate(payload: any) { 18 | // check if user is still active 19 | return { userId: payload.sub, username: payload.username, roles: payload.roles }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/store/selectors/projectShareSelectors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: This file contains the selectors for the sharedProjects slice of the store 3 | * Responsibilities: 4 | * - Retrieve the room from the state. 5 | * - Retrieve the invitation count from the state. 6 | */ 7 | 8 | import { createSelector } from '@reduxjs/toolkit'; 9 | import { RootState } from '@store/store'; 10 | 11 | // Selector to get the room from the state. 12 | export const selectRoom = createSelector( 13 | (state: RootState) => state.sharedProjects.room, 14 | (room) => room, 15 | ); 16 | 17 | // Selector to get the invitation count from the state. 18 | export const selectInvitationCount = createSelector( 19 | (state: RootState) => state.sharedProjects.invitationCount, 20 | (invitationCount) => invitationCount, 21 | ); 22 | -------------------------------------------------------------------------------- /backend/src/auth/guards/roles.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; 2 | import { Reflector } from '@nestjs/core'; 3 | import { Role } from '../enums/role.enum'; 4 | import { ROLES_KEY } from '@auth/decorators/roles.decorator'; 5 | 6 | @Injectable() 7 | export class RolesGuard implements CanActivate { 8 | constructor(private reflector: Reflector) {} 9 | 10 | canActivate(context: ExecutionContext): boolean { 11 | const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [ 12 | context.getHandler(), 13 | context.getClass(), 14 | ]); 15 | if (!requiredRoles) { 16 | return true; 17 | } 18 | const { user } = context.switchToHttp().getRequest(); 19 | return requiredRoles.some((role) => user.roles?.includes(role)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/store/selectors/cookieConsentSelectors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains selectors for accessing cookie consent-related data from the 3 | * Redux store. 4 | */ 5 | 6 | import { createSelector } from '@reduxjs/toolkit'; 7 | import { RootState } from '@store/store'; 8 | 9 | export const selectShowCookieConsent = createSelector( 10 | (state: RootState) => state.cookieConsent.showCookieConsent, 11 | (showCookieConsent) => showCookieConsent, 12 | ); 13 | 14 | export const selectConsentOptions = createSelector( 15 | (state: RootState) => state.cookieConsent.consentOptions, 16 | (consentOptions) => consentOptions, 17 | ); 18 | 19 | export const selectUserChangedConsent = createSelector( 20 | (state: RootState) => state.cookieConsent.userChangedConsent, 21 | (userChangedConsent) => userChangedConsent, 22 | ); 23 | -------------------------------------------------------------------------------- /backend/src/guest/guest.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { GuestService } from './guest.service'; 3 | import { GuestController } from './guest.controller'; 4 | import { ProjectsModule } from '@projects/projects.module'; 5 | import { RedisModule } from '@redis/redis.module'; 6 | import { AuthModule } from '@auth/auth.module'; 7 | import { UsersModule } from '@users/users.module'; 8 | import { JwtModule } from '@nestjs/jwt'; 9 | import { ProjectManagerModule } from '@project-manager/project-manager.module'; 10 | 11 | @Module({ 12 | imports: [ 13 | ProjectsModule, 14 | RedisModule, 15 | AuthModule, 16 | UsersModule, 17 | JwtModule, 18 | ProjectManagerModule, 19 | ], 20 | exports: [GuestService], 21 | providers: [GuestService], 22 | controllers: [GuestController], 23 | }) 24 | export class GuestModule { } 25 | -------------------------------------------------------------------------------- /frontend/src/utils/filetreeinit.ts: -------------------------------------------------------------------------------- 1 | import * as Y from 'yjs'; 2 | import { YMapValueType } from '@context/EditorContext'; 3 | 4 | // This function creates a filetree metadata, if the database has already saved a session 5 | // related to this room before, the filetree will be overwritten with whatever is in the server 6 | 7 | export default function createfiletree(root: Y.Map) { 8 | if (!root.get('filetree')) { 9 | const rootnode: any = { 10 | type: 'directory', 11 | id: '0', 12 | name: 'root', 13 | children: [], 14 | }; 15 | root.set('filetree', rootnode); // hopeless type error 16 | console.log('A file tree stucture as metadata is created'); 17 | } else { 18 | console.log('This is a loaded project, file tree is already provided'); 19 | console.log(JSON.stringify(root.get('filetree'))); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/admin/admin.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | import { AdminService } from './admin.service'; 3 | import { AdminController } from './admin.controller'; 4 | import { UsersModule } from '@users/users.module'; 5 | import { ProjectsModule } from '@projects/projects.module'; 6 | import { FileMongoModule } from '@file-mongo/file-mongo.module'; 7 | import { APP_GUARD } from '@nestjs/core'; 8 | import { RolesGuard } from '@auth/guards/roles.guard'; 9 | 10 | @Module({ 11 | imports: [ 12 | UsersModule, 13 | forwardRef(() => ProjectsModule), 14 | FileMongoModule, 15 | ], 16 | providers: [ 17 | { 18 | provide: APP_GUARD, 19 | useClass: RolesGuard 20 | }, 21 | AdminService, 22 | ], 23 | controllers: [AdminController], 24 | exports: [AdminService], 25 | 26 | }) 27 | export class AdminModule {} 28 | -------------------------------------------------------------------------------- /backend/src/directory-mongo/directory-mongo.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { DirectoryMongoService } from './directory-mongo.service'; 4 | import { DirectoryMongoController } from './directory-mongo.controller'; 5 | import { DirectoryMongo } from './directory-mongo.entity'; 6 | import { FileMongoModule } from '../file-mongo/file-mongo.module'; 7 | import { ProjectsModule } from '@projects/projects.module'; 8 | 9 | @Module({ 10 | imports: [ 11 | TypeOrmModule.forFeature([DirectoryMongo], 'mongoConnection'), 12 | forwardRef(() => ProjectsModule), 13 | forwardRef(() => FileMongoModule), 14 | ], 15 | providers: [DirectoryMongoService], 16 | controllers: [DirectoryMongoController], 17 | exports: [DirectoryMongoService], 18 | }) 19 | export class DirectoryMongoModule {} 20 | -------------------------------------------------------------------------------- /backend/src/environment-mongo/environment-mongo.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | Column, 4 | OneToMany, 5 | ObjectIdColumn, 6 | ObjectId, 7 | // OneToOne, 8 | } from 'typeorm'; 9 | import { ProjectMongo } from '@project-mongo/project-mongo.entity'; 10 | // import { Users } from '@users/user.entity'; 11 | /** 12 | * Environments entity - a mongodb collection managing the environments 13 | * of all users. Each user has one environment. 14 | * Each environment has multiple projects. 15 | */ 16 | 17 | @Entity({ name: 'environment' }) 18 | export class EnvironmentMongo { 19 | @ObjectIdColumn() 20 | _id: ObjectId; 21 | 22 | @Column() 23 | username: string; 24 | 25 | @OneToMany(() => ProjectMongo, (project) => project.environment) 26 | projects: ProjectMongo[]; 27 | 28 | // @OneToOne(() => Users, (user) => user.environment) 29 | // user: Users; 30 | } 31 | -------------------------------------------------------------------------------- /backend/src/environment-mongo/environment-mongo.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { EnvironmentMongoService } from './environment-mongo.service'; 4 | import { EnvironmentMongoController } from './environment-mongo.controller'; 5 | import { EnvironmentMongo } from './environment-mongo.entity'; 6 | import { UsersModule } from '@users/users.module'; 7 | import { MONGO_CONN } from '@constants'; 8 | import { ProjectsModule } from '@projects/projects.module'; 9 | 10 | @Module({ 11 | imports: [ 12 | TypeOrmModule.forFeature([EnvironmentMongo], MONGO_CONN), 13 | forwardRef(() => UsersModule), 14 | forwardRef(() => ProjectsModule), 15 | ], 16 | providers: [EnvironmentMongoService], 17 | controllers: [EnvironmentMongoController], 18 | exports: [EnvironmentMongoService], 19 | }) 20 | export class EnvironmentMongoModule {} 21 | -------------------------------------------------------------------------------- /frontend/src/components/CodeEditor/names.ts: -------------------------------------------------------------------------------- 1 | // usernames.ts 2 | const usernames: string[] = [ 3 | 'SilentShade', 4 | 'GhostlyEcho', 5 | 'VeilSpecter', 6 | 'HiddenVoyage', 7 | 'ShadowCipher', 8 | 'IncogNebula', 9 | 'PhantomLoom', 10 | 'MaskedWander', 11 | 'ObscureMist', 12 | 'CloakedRogue', 13 | 'NullEnigma', 14 | 'QuietWhisper', 15 | 'MysterVeil', 16 | 'ShroudEcho', 17 | 'HiddenPhantom', 18 | 'VeiledMystic', 19 | 'NamelessVibe', 20 | 'SilentRealm', 21 | 'GhostedPath', 22 | 'ShadowedX', 23 | 'IncognitoLoom', 24 | 'EnigmaHush', 25 | 'ObscuraShade', 26 | 'HushedSpecter', 27 | 'PhantomEcho', 28 | 'VeilEnigma', 29 | 'MaskedMystic', 30 | 'CloakWander', 31 | 'VagueWhisper', 32 | 'SilentGhost', 33 | ]; 34 | 35 | export function getRandomUsername(): string { 36 | const randomIndex = Math.floor(Math.random() * usernames.length); 37 | return usernames[randomIndex]; 38 | } 39 | -------------------------------------------------------------------------------- /backend/src/file-mongo/file-mongo.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | import { FileMongoService } from './file-mongo.service'; 3 | import { FileMongoController } from './file-mongo.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { FileMongo } from './file-mongo.entity'; 6 | import { ProjectsModule } from '@projects/projects.module'; 7 | import { DirectoryMongoModule } from '@directory-mongo/directory-mongo.module'; 8 | import { DockerModule } from '@docker/docker.module'; 9 | 10 | @Module({ 11 | imports: [ 12 | TypeOrmModule.forFeature([FileMongo], 'mongoConnection'), 13 | forwardRef(() => DirectoryMongoModule), 14 | forwardRef(() => ProjectsModule), 15 | forwardRef(() => DockerModule), 16 | ], 17 | providers: [FileMongoService], 18 | controllers: [FileMongoController], 19 | exports: [FileMongoService], 20 | }) 21 | export class FileMongoModule {} 22 | -------------------------------------------------------------------------------- /backend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: false, 5 | es6: true, 6 | jest: true, 7 | }, 8 | extends: [ 9 | 'airbnb-base', 10 | 'plugin:jest/all', 11 | ], 12 | globals: { 13 | Atomics: 'readonly', 14 | SharedArrayBuffer: 'readonly', 15 | }, 16 | parserOptions: { 17 | ecmaVersion: 2018, 18 | sourceType: 'module', 19 | }, 20 | plugins: ['jest'], 21 | rules: { 22 | 'max-classes-per-file': 'off', 23 | 'no-underscore-dangle': 'off', 24 | 'no-console': 'off', 25 | 'no-shadow': 'off', 26 | 'no-restricted-syntax': [ 27 | 'error', 28 | 'LabeledStatement', 29 | 'WithStatement', 30 | ], 31 | }, 32 | settings: { 33 | jest: { 34 | version: '29.7.0', 35 | }, 36 | }, 37 | overrides: [ 38 | { 39 | files: ['*.js'], 40 | excludedFiles: 'babel.config.js', 41 | }, 42 | ], 43 | }; 44 | -------------------------------------------------------------------------------- /backend/src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { Users } from './user.entity'; 4 | import { UsersService } from './users.service'; 5 | import { UsersController } from './users.controller'; 6 | import { EnvironmentMongoModule } from '@environment-mongo/environment-mongo.module'; 7 | import { APP_GUARD } from '@nestjs/core'; 8 | import { RolesGuard } from '@auth/guards/roles.guard'; 9 | import { MONGO_CONN, MYSQL_CONN } from '@constants'; 10 | 11 | @Module({ 12 | imports: [ 13 | TypeOrmModule.forFeature([Users], MYSQL_CONN), 14 | forwardRef(() => EnvironmentMongoModule), 15 | ], 16 | providers: [ 17 | { 18 | provide: APP_GUARD, 19 | useClass: RolesGuard, 20 | }, 21 | UsersService, 22 | ], 23 | controllers: [UsersController], 24 | exports: [UsersService], 25 | }) 26 | export class UsersModule {} 27 | -------------------------------------------------------------------------------- /backend/src/redis/redis.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import Redis from 'ioredis'; 3 | @Injectable() 4 | export class RedisService { 5 | private readonly redis: Redis; 6 | 7 | constructor() { 8 | this.redis = new Redis({ 9 | host: 'localhost', // Replace with your Redis host 10 | port: 6379, // Replace with your Redis port 11 | }); 12 | } 13 | 14 | async set(key: string, value: string, ttl?: number) { 15 | if (ttl) { 16 | return this.redis.set(key, value, 'EX', ttl); 17 | } 18 | return this.redis.set(key, value); 19 | } 20 | 21 | async get(key: string) { 22 | return this.redis.get(key); 23 | } 24 | 25 | async getRevoked(key: string) { 26 | return this.redis.get(`revoked:${key}`); 27 | } 28 | 29 | async del(key: string) { 30 | return this.redis.del(key); 31 | } 32 | 33 | async exists(key: string) { 34 | return this.redis.exists(key); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/src/project-mongo/project-mongo.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | import { ProjectMongo } from './project-mongo.entity'; 3 | import { ProjectMongoService } from './project-mongo.service'; 4 | // import { ProjectMongoController } from './project-mongo.controller'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | import { UsersModule } from '@users/users.module'; 7 | import { DirectoryMongoModule } from '@directory-mongo/directory-mongo.module'; 8 | import { FileMongoModule } from '@file-mongo/file-mongo.module'; 9 | import { MONGO_CONN } from '@constants'; 10 | 11 | @Module({ 12 | imports: [ 13 | TypeOrmModule.forFeature([ProjectMongo], MONGO_CONN), 14 | forwardRef(() => UsersModule), 15 | forwardRef(() => DirectoryMongoModule), 16 | forwardRef(() => FileMongoModule), 17 | ], 18 | providers: [ProjectMongoService], 19 | // controllers: [ProjectMongoController], 20 | exports: [ProjectMongoService], 21 | }) 22 | export class ProjectMongoModule {} 23 | -------------------------------------------------------------------------------- /backend/ecosystem.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module [TODO:RUN WITH: `pm2 start ecosystem.config.js --env production` 3 | */ 4 | 5 | module.exports = { 6 | apps: [ 7 | { 8 | name: 'collabor8', 9 | script: 'dist/src/main.js', 10 | instances: 'max', 11 | exec_mode: 'cluster', 12 | watch: false, 13 | env: { 14 | NODE_ENV: 'development', 15 | PORT: 3000, 16 | DB_HOST: 'localhost', 17 | DB_PORT: 5432, 18 | ...process.env, 19 | }, 20 | env_production: { 21 | NODE_ENV: 'production', 22 | PORT: 3000, 23 | DB_HOST: 'localhost', 24 | DB_PORT: 5432, 25 | ...process.env, 26 | }, 27 | increment_var: 'PORT', 28 | error_file: './logs/collabor8-error.log', 29 | out_file: './logs/collabor8-out.log', 30 | log_date_format: 'YYYY-MM-DD HH:mm Z', 31 | merge_logs: true, 32 | instances: 4, 33 | max_memory_restart: '300M', 34 | time: true, 35 | }, 36 | ], 37 | }; 38 | -------------------------------------------------------------------------------- /frontend/src/common/icons/LinkedIn.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from '@chakra-ui/react'; 2 | 3 | const LinkedIn = (props) => ( 4 | 5 | 10 | 14 | 15 | ); 16 | 17 | export default LinkedIn; 18 | -------------------------------------------------------------------------------- /backend/src/file-mongo/file-mongo.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, ObjectIdColumn, ObjectId, Column, ManyToOne } from 'typeorm'; 2 | import { ProjectMongo } from '@project-mongo/project-mongo.entity'; 3 | import { DirectoryMongo } from '@directory-mongo/directory-mongo.entity'; 4 | 5 | /** 6 | * File entity - a mongodb collection 7 | */ 8 | @Entity('files') 9 | export class FileMongo { 10 | @ObjectIdColumn() 11 | _id: ObjectId; 12 | 13 | @Column() 14 | name: string; 15 | 16 | // deafaault is file 17 | @Column({ default: 'file' }) 18 | type: string; 19 | 20 | @Column() 21 | file_content: any; 22 | 23 | @Column({ type: 'string', nullable: true }) 24 | parent_id?: string; 25 | 26 | @Column() 27 | project_id: string; 28 | 29 | @Column() 30 | created_at: Date; 31 | 32 | @Column() 33 | updated_at: Date; 34 | 35 | @ManyToOne(() => ProjectMongo, (project) => project.files) 36 | project: ProjectMongo; 37 | 38 | @ManyToOne(() => DirectoryMongo, (directory) => directory.files) 39 | directory: DirectoryMongo; 40 | } 41 | -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], 3 | theme: { 4 | extend: { 5 | backgroundImage: { 6 | brand: "linear-gradient(rgb(0, 24, 69), rgb(82, 65, 117))", 7 | }, 8 | colors: { 9 | brand: { 10 | 900: '#001845', 11 | 800: '#524175', 12 | 850: '#553C9A', 13 | 700: '#333333', 14 | 600: '#E6E85C', 15 | 500: '#F3F3F3', 16 | 400: '#FFD700', 17 | 300: '#FF4C4C', 18 | 200: '#6BE3E1', 19 | 150: '#D1D1D1', 20 | 100: '#524175', 21 | }, 22 | }, 23 | animation: { 24 | bounce_r: 'bounce_r 1s infinite', 25 | }, 26 | keyframes: { 27 | bounce_r: { 28 | '0%, 100%': { 29 | transform: 'translateX(0)', 30 | }, 31 | '50%': { 32 | transform: 'translateX(10px)', 33 | }, 34 | }, 35 | }, 36 | }, 37 | }, 38 | plugins: [], 39 | }; 40 | -------------------------------------------------------------------------------- /backend/src/auth/strategies/local.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Strategy } from 'passport-local'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Request } from 'express'; 4 | import { ContextIdFactory, ModuleRef } from '@nestjs/core'; 5 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 6 | import { AuthService } from '@auth/auth.service'; 7 | 8 | @Injectable() 9 | export class LocalStrategy extends PassportStrategy(Strategy) { 10 | constructor(private moduleRef: ModuleRef) { 11 | super({ 12 | passReqToCallback: true, 13 | }); 14 | } 15 | 16 | async validate( 17 | request: Request, 18 | username: string, 19 | password: string, 20 | ): Promise { 21 | const contextId = ContextIdFactory.getByRequest(request); 22 | const authService = await this.moduleRef.resolve(AuthService, contextId); 23 | const user = await authService.validateUser(username, password); 24 | 25 | if (!user) { 26 | throw new UnauthorizedException(); 27 | } 28 | return user; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/src/auth/strategies/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 4 | import { jwtConstants } from '../../constants'; 5 | import { RedisService } from '@redis/redis.service'; 6 | 7 | @Injectable() 8 | export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { 9 | constructor(private redisService: RedisService) { 10 | super({ 11 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 12 | ignoreExpiration: false, 13 | secretOrKey: jwtConstants.secret, 14 | passReqToCallback: true, 15 | }); 16 | } 17 | 18 | async validate(req: Request, payload: any) { 19 | const isRevoked = await this.redisService.getRevoked(payload.jti); 20 | if (isRevoked) { 21 | throw new UnauthorizedException('Token has been revoked'); 22 | } 23 | return { userId: payload.sub, username: payload.username, roles: payload.roles, jti: payload.jti }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/store/services/environment.ts: -------------------------------------------------------------------------------- 1 | import { api } from './api'; 2 | import { Environment } from '@types'; 3 | 4 | export const environmentApi = api.injectEndpoints({ 5 | endpoints: (builder) => ({ 6 | // Get all environments 7 | getAllEnvironments: builder.query({ 8 | query: () => '/environments', 9 | }), 10 | // Get the environment for the current user 11 | getEnvironmentForCurrentUser: builder.query({ 12 | query: () => '/environments/me', 13 | providesTags: ['Environment'], 14 | }), 15 | // Delete the environment for the current user 16 | deleteEnvironmentForCurrentUser: builder.mutation({ 17 | query: () => ({ 18 | url: `/environments/me`, 19 | method: 'DELETE', 20 | }), 21 | invalidatesTags: ['Environment'], 22 | }), 23 | }), 24 | overrideExisting: false, 25 | }); 26 | 27 | export const { 28 | useGetAllEnvironmentsQuery, 29 | useGetEnvironmentForCurrentUserQuery, 30 | useDeleteEnvironmentForCurrentUserMutation, 31 | } = environmentApi; 32 | -------------------------------------------------------------------------------- /backend/src/project-shares/project-shares.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { ProjectShares } from './project-shares.entity'; 4 | import { ProjectSharesController } from './project-shares.controller'; 5 | import { ProjectSharesService } from './project-shares.service'; 6 | import { MYSQL_CONN } from '@constants'; 7 | import { ProjectsModule } from '@projects/projects.module'; 8 | import { UsersModule } from '@users/users.module'; 9 | import { BullModule } from '@nestjs/bullmq'; 10 | import { RedisModule } from '@redis/redis.module'; 11 | 12 | @Module({ 13 | imports: [ 14 | TypeOrmModule.forFeature([ProjectShares], MYSQL_CONN), 15 | forwardRef(() => ProjectsModule), 16 | forwardRef(() => UsersModule), 17 | forwardRef(() => RedisModule), 18 | BullModule.registerQueue({ 19 | name: 'mailer', 20 | }), 21 | ], 22 | providers: [ProjectSharesService], 23 | controllers: [ProjectSharesController], 24 | exports: [ProjectSharesService], 25 | }) 26 | export class ProjectSharesModule {} 27 | -------------------------------------------------------------------------------- /frontend/src/store/selectors/authSelectors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains selectors for accessing authentication-related data 3 | * from the Redux store. Selectors are used to derive and access specific 4 | * pieces of state related to user authentication. 5 | * 6 | * Responsibilities: 7 | * - Access the current access token from the authentication state. 8 | * - Determine if the user is authenticated based on the presence of the access token. 9 | */ 10 | 11 | import { createSelector } from '@reduxjs/toolkit'; 12 | import { RootState } from '@store/store'; 13 | 14 | // Access token from the authentication state. 15 | export const selectAccessToken = createSelector( 16 | (state: RootState) => 17 | state.auth.accessToken || localStorage.getItem('accessToken'), 18 | (accessToken) => accessToken, 19 | ); 20 | 21 | // Checks if the user is authenticated by verifying the presence of the accessToken. 22 | export const selectIsAuthenticated = createSelector( 23 | (state: RootState) => 24 | state.auth.accessToken || localStorage.getItem('accessToken'), 25 | (accessToken) => !!accessToken, 26 | ); 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Reem Osama 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 | -------------------------------------------------------------------------------- /frontend/src/components/Dashboard/Dashboard.css: -------------------------------------------------------------------------------- 1 | .dashboard-container { 2 | display: flex; 3 | /**height: 100vh;*/ 4 | } 5 | 6 | .projects-sidebar { 7 | width: 250px; 8 | padding: 20px; 9 | border-right: 1px solid #ddd; 10 | background-color: #f4f4f4; 11 | } 12 | 13 | .projects-list { 14 | list-style-type: none; 15 | padding: 0; 16 | } 17 | 18 | .project-item { 19 | cursor: pointer; 20 | padding: 10px; 21 | margin-bottom: 5px; 22 | border-radius: 4px; 23 | transition: background-color 0.3s ease; 24 | } 25 | 26 | .project-item:hover { 27 | background-color: #e0e0e0; 28 | } 29 | 30 | .project-item.selected { 31 | background-color: #dcdcdc; 32 | } 33 | 34 | .file-tree-container { 35 | flex: 1; 36 | display: flex; 37 | flex-direction: column; 38 | height: 100%; 39 | padding: 20px; 40 | } 41 | 42 | .placeholder { 43 | display: flex; 44 | align-items: center; 45 | justify-content: center; 46 | height: 100%; 47 | font-size: 18px; 48 | color: #888; 49 | } 50 | 51 | @keyframes spin { 52 | 0% { 53 | transform: rotate(0deg); 54 | } 55 | 100% { 56 | transform: rotate(360deg); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /backend/docker/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | function installDocker(){ 3 | # Add Docker's official GPG key: 4 | sudo apt-get update 5 | sudo apt-get install ca-certificates curl 6 | sudo install -m 0755 -d /etc/apt/keyrings 7 | sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc 8 | sudo chmod a+r /etc/apt/keyrings/docker.asc 9 | 10 | # Add the repository to Apt sources: 11 | echo \ 12 | "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ 13 | $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ 14 | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null 15 | sudo apt-get update 16 | sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 17 | sudo docker run hello-world 18 | } 19 | 20 | 21 | # check if docker is installed 22 | if ! [ -x "$(command -v docker)" ]; then 23 | echo 'Error: docker is not installed.' >&2 24 | installDocker 25 | ./javascript_container/install.sh 26 | ./python_container/install.sh 27 | else 28 | echo 'docker is installed.' 29 | fi 30 | 31 | 32 | -------------------------------------------------------------------------------- /frontend/src/components/CodeEditor/VoiceDrawer.tsx: -------------------------------------------------------------------------------- 1 | import { VStack, Box, Flex, CloseButton } from '@chakra-ui/react'; 2 | import Draggable from 'react-draggable'; 3 | import RoomComponent from '@components/Audio/RoomComponent'; 4 | 5 | interface ModalProps { 6 | isOpen: boolean; 7 | onClose: () => void; 8 | } 9 | 10 | export default function VoiceDrawer({ isOpen, onClose }: ModalProps) { 11 | return ( 12 | 13 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /backend/src/mail/mail.module.ts: -------------------------------------------------------------------------------- 1 | import { MailerModule } from '@nestjs-modules/mailer'; 2 | import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter'; 3 | import { Module } from '@nestjs/common'; 4 | import { MailService } from './mail.service'; 5 | import { join } from 'path'; 6 | import { ConfigModule } from '@nestjs/config'; 7 | 8 | @Module({ 9 | imports: [ 10 | ConfigModule.forRoot(), // load .env 11 | MailerModule.forRoot({ 12 | transport: { 13 | host: 'smtp.gmail.com', 14 | port: 465, 15 | secure: true, 16 | auth: { 17 | user: process.env.APP_EMAIL, 18 | pass: process.env.MAIL_PSW, 19 | }, 20 | }, 21 | defaults: { 22 | from: '"Collabor8 Team" ', 23 | }, 24 | template: { 25 | dir: join(process.cwd(), 'dist', 'mail', 'templates'), 26 | adapter: new HandlebarsAdapter(), 27 | options: { 28 | strict: true, 29 | }, 30 | }, 31 | }), 32 | ], 33 | providers: [MailService], 34 | exports: [MailService], // 👈 export for DI 35 | }) 36 | export class MailModule {} 37 | -------------------------------------------------------------------------------- /frontend/src/common/icons/Github.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from '@chakra-ui/react'; 2 | 3 | const Github = (props) => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export default Github; 10 | -------------------------------------------------------------------------------- /frontend/src/context/AuthContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, ReactNode } from 'react'; 2 | import { useAppSelector, useAppDispatch } from '@hooks/useApp'; 3 | import { selectIsAuthenticated } from '@store/selectors/authSelectors'; 4 | import { setCredentials, performLogout } from '@store/slices/authSlice'; 5 | 6 | interface AuthContextType { 7 | isAuthenticated: boolean; 8 | login: (token: string) => void; 9 | logout: () => void; 10 | } 11 | 12 | const AuthContext = createContext(undefined); 13 | 14 | export const AuthProvider: React.FC<{ children: ReactNode }> = ({ 15 | children, 16 | }) => { 17 | const isAuthenticated = useAppSelector(selectIsAuthenticated); 18 | const dispatch = useAppDispatch(); 19 | 20 | const login = (token: string) => { 21 | dispatch(setCredentials({ accessToken: token })); 22 | }; 23 | 24 | const logout = () => { 25 | dispatch(performLogout()); 26 | // dispatch(unsetCredentials()); 27 | }; 28 | 29 | return ( 30 | 31 | {children} 32 | 33 | ); 34 | }; 35 | 36 | export default AuthContext; 37 | -------------------------------------------------------------------------------- /frontend/src/store/selectors/userSelectors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains selectors for accessing user-related data from the 3 | * Redux store. Selectors are used to derive and access specific pieces of 4 | * state related to the user profile and loading states. 5 | * 6 | * Responsibilities: 7 | * - Retrieve user details from the user state. 8 | * - Retrieve the loading state for user data. 9 | * - Determine if user details have been loaded. 10 | */ 11 | 12 | import { createSelector } from '@reduxjs/toolkit'; 13 | import { RootState } from '@store/store'; 14 | 15 | // Selector to get the user details from the state. 16 | export const selectUserDetails = createSelector( 17 | (state: RootState) => state.user.userDetails, 18 | (userDetails) => userDetails, 19 | ); 20 | 21 | // Selector to get the loading state of the user data. 22 | export const selectUserLoading = createSelector( 23 | (state: RootState) => state.user.loading, 24 | (loading) => loading, 25 | ); 26 | 27 | // Selector to check if user details are loaded 28 | export const selectIsUserDetailsLoaded = createSelector( 29 | (state: RootState) => state.user.userDetails, 30 | (userDetails) => !!userDetails, 31 | ); 32 | -------------------------------------------------------------------------------- /frontend/src/utils/dashboard.utils.ts: -------------------------------------------------------------------------------- 1 | import { formatDistanceToNow, parseISO } from 'date-fns'; 2 | 3 | // This function should compute the last edited time based on the updated_at field 4 | export function computeTimeDiff(time: string) { 5 | const lastEdited = formatDistanceToNow(parseISO(time), { 6 | addSuffix: true, 7 | }); 8 | return lastEdited; 9 | } 10 | 11 | // This function should mutate the projects state 12 | export function mutateProjects(projs) { 13 | if (!projs) { 14 | console.log('no projects'); 15 | return []; 16 | } 17 | return projs?.map((proj) => ({ 18 | ...proj, 19 | lastEdited: computeTimeDiff(proj.updated_at), 20 | })); 21 | } 22 | 23 | //export function SharedProjects() { 24 | // // This function should fetch the projects shared with the user from the backend 25 | // } 26 | 27 | export function setRecentProjectsFromAllProjects(projects) { 28 | console.log('Setting recent projects', projects); 29 | const recentProjects = [...projects] 30 | ?.sort( 31 | (a, b) => 32 | (parseISO(b.updated_at) as any) - (parseISO(a.updated_at) as any), 33 | ) 34 | .slice(0, 5); 35 | 36 | return mutateProjects(recentProjects); // Directly set the recent projects 37 | } 38 | -------------------------------------------------------------------------------- /socket_server/transform.js: -------------------------------------------------------------------------------- 1 | function transformData(input) { 2 | // transform each node 3 | function transformNode(node, type) { 4 | const transformed = { 5 | id: node._id, 6 | name: node.name || node.project_name, // use project_name for root project node 7 | type: type, 8 | parent: node.parent_id 9 | }; 10 | 11 | if (type !== 'file') transformed.children = []; 12 | // Initialize children array if there are directories or files 13 | if ( 14 | node.children && 15 | (node.children.directories.length > 0 || node.children.files.length > 0) 16 | 17 | ) { 18 | 19 | // recursively transform directories 20 | node.children.directories.forEach((directory) => { 21 | transformed.children.push(transformNode(directory, "directory")); 22 | }); 23 | 24 | // recursively transform files 25 | node.children.files.forEach((file) => { 26 | transformed.children.push(transformNode(file, "file")); 27 | }); 28 | } 29 | 30 | return transformed; 31 | } 32 | 33 | // start from the root object 34 | return transformNode(input, "project"); 35 | } 36 | 37 | export default transformData; 38 | //console.log(JSON.stringify(transformedData, null, 2)); 39 | -------------------------------------------------------------------------------- /backend/docker/python_container/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python image from the Docker library 2 | FROM python:3.12-slim 3 | 4 | # Set environment variables for Python 5 | ENV PYTHONDONTWRITEBYTECODE=1 6 | ENV PYTHONUNBUFFERED=1 7 | 8 | # Set the working directory in the container 9 | WORKDIR /app 10 | 11 | # Copy requirements file 12 | COPY requirements.txt /app/ 13 | 14 | # Install any necessary libraries 15 | # RUN python -m pip cache purge && \ 16 | RUN pip install --upgrade pip && \ 17 | pip install --no-cache-dir --no-deps -r requirements.txt 18 | 19 | # Copy the application files into the container (optional step if you want to load initial scripts) 20 | COPY . /app/ 21 | 22 | # Command to run the Python code (this will be replaced or customized in practice) 23 | CMD ["python3"] 24 | # FROM python:3.9-slim 25 | 26 | # # Set the working directory 27 | # WORKDIR /app 28 | 29 | # # Copy the requirements file 30 | # COPY requirements.txt . 31 | 32 | # # Upgrade pip and install requirements 33 | # RUN pip install --upgrade pip && \ 34 | # pip install --no-cache-dir -r requirements.txt 35 | 36 | # # Copy the rest of the application code 37 | # COPY . . 38 | 39 | # # Define the command to run the application 40 | # CMD ["python", "app.py"] 41 | 42 | -------------------------------------------------------------------------------- /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 tsPlugin from '@typescript-eslint/eslint-plugin'; 6 | import tsParser from '@typescript-eslint/parser'; 7 | import prettierPlugin from 'eslint-plugin-prettier'; 8 | import prettierConfig from 'eslint-config-prettier'; 9 | 10 | export default [ 11 | { 12 | files: ['**/*.{ts,tsx}'], 13 | ignores: ['dist'], 14 | languageOptions: { 15 | ecmaVersion: 2020, 16 | globals: globals.browser, 17 | parser: tsParser, 18 | }, 19 | plugins: { 20 | 'react-hooks': reactHooks, 21 | 'react-refresh': reactRefresh, 22 | '@typescript-eslint': tsPlugin, 23 | prettier: prettierPlugin, 24 | }, 25 | rules: { 26 | '@typescript-eslint/no-explicit-any': 'off', 27 | ...reactHooks.configs.recommended.rules, 28 | ...tsPlugin.configs.recommended.rules, 29 | 'react-refresh/only-export-components': [ 30 | 'warn', 31 | { allowConstantExport: true }, 32 | ], 33 | 'prettier/prettier': 'error', 34 | }, 35 | }, 36 | js.configs.recommended, 37 | prettierConfig, 38 | ]; 39 | -------------------------------------------------------------------------------- /frontend/src/store/slices/projectSharesSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | 3 | interface roomInterface { 4 | token: string; 5 | uid: string; 6 | channel: string; 7 | project_id: string; 8 | } 9 | 10 | interface ROOM { 11 | room: roomInterface; 12 | invitationCount: number; 13 | } 14 | 15 | const initialState: ROOM = { 16 | room: { 17 | token: '', 18 | uid: '', 19 | channel: '', 20 | project_id: '', 21 | }, 22 | invitationCount: 0, 23 | }; 24 | 25 | const projectSharesSlice = createSlice({ 26 | name: 'ROOM', 27 | initialState, 28 | reducers: { 29 | setRoom(state, action: PayloadAction) { 30 | state.room.token = action.payload.token; 31 | state.room.uid = action.payload.uid; 32 | state.room.channel = action.payload.channel; 33 | state.room.project_id = action.payload.project_id; 34 | }, 35 | clearRoom(state) { 36 | state.room = initialState.room; 37 | }, 38 | setInvitationCount(state, action: PayloadAction) { 39 | state.invitationCount = action.payload; 40 | }, 41 | }, 42 | }); 43 | 44 | export const { setRoom, clearRoom, setInvitationCount } = 45 | projectSharesSlice.actions; 46 | export default projectSharesSlice.reducer; 47 | -------------------------------------------------------------------------------- /frontend/src/store/slices/fileSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | 3 | interface fileInterface { 4 | file_id: string; 5 | language: string; 6 | } 7 | 8 | interface IFileSlice { 9 | file: fileInterface; 10 | panelVisibility: boolean; 11 | } 12 | 13 | const initialState: IFileSlice = { 14 | file: { 15 | file_id: '', 16 | language: '', 17 | }, 18 | panelVisibility: false, 19 | }; 20 | 21 | const fileSlice = createSlice({ 22 | name: 'file', 23 | initialState, 24 | reducers: { 25 | setFile(state, action: PayloadAction) { 26 | state.file.file_id = action.payload.file_id; 27 | state.file.language = action.payload.language; 28 | }, 29 | clearFile(state) { 30 | state.file = initialState.file; 31 | }, 32 | togglePanelVisibility(state) { 33 | state.panelVisibility = !state.panelVisibility; 34 | }, 35 | displayPanel(state) { 36 | state.panelVisibility = true; 37 | }, 38 | removePanel(state) { 39 | state.panelVisibility = false; 40 | }, 41 | }, 42 | }); 43 | 44 | export const { 45 | setFile, 46 | clearFile, 47 | togglePanelVisibility, 48 | displayPanel, 49 | removePanel, 50 | } = fileSlice.actions; 51 | export default fileSlice.reducer; 52 | -------------------------------------------------------------------------------- /backend/src/directory-mongo/directory-mongo.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | ObjectIdColumn, 4 | Column, 5 | ManyToOne, 6 | OneToMany, 7 | ObjectId, 8 | } from 'typeorm'; 9 | import { ProjectMongo } from '@project-mongo/project-mongo.entity'; 10 | import { FileMongo } from '@file-mongo/file-mongo.entity'; 11 | 12 | /** 13 | * Directory entity - a mongodb collection 14 | */ 15 | @Entity('directories') 16 | export class DirectoryMongo { 17 | @ObjectIdColumn() 18 | _id: ObjectId; 19 | 20 | @Column() 21 | name: string; 22 | 23 | @Column() 24 | created_at: Date; 25 | 26 | @Column() 27 | updated_at: Date; 28 | 29 | @Column({default: 'directory'}) 30 | type: string; 31 | 32 | @Column({ type: 'string', nullable: true }) 33 | parent_id?: string; // Use string for ObjectId 34 | 35 | @Column() 36 | project_id: string; 37 | 38 | @ManyToOne(() => ProjectMongo, (project) => project.directories) 39 | project: ProjectMongo; 40 | 41 | @OneToMany(() => FileMongo, (file) => file.directory) 42 | files: FileMongo[]; 43 | 44 | @OneToMany(() => DirectoryMongo, (directory) => directory.parent) 45 | children: DirectoryMongo[]; 46 | 47 | @ManyToOne(() => DirectoryMongo, (directory) => directory.children, { 48 | nullable: true, 49 | }) 50 | parent: DirectoryMongo; 51 | } 52 | -------------------------------------------------------------------------------- /backend/src/auth/guards/refresh-jwt-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { AuthGuard } from '@nestjs/passport'; 2 | import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; 3 | import { Reflector } from '@nestjs/core'; 4 | import { IS_PUBLIC_KEY } from '@auth/decorators/isPublic.decorator'; 5 | import { JwtService } from '@nestjs/jwt'; 6 | 7 | @Injectable() 8 | export class RefreshAuthGuard extends AuthGuard('jwt-refresh') { 9 | constructor(private reflector: Reflector, private jwtService: JwtService) { 10 | super(); 11 | } 12 | //checks if refresh token is valid 13 | async checkRefresh(context: ExecutionContext) { 14 | const request = context.switchToHttp().getRequest(); 15 | const token = request.cookies['refreshToken']; 16 | if (token) { 17 | try { 18 | const checkToken = await this.jwtService.verifyAsync(token); 19 | if (checkToken) { 20 | return true; 21 | } 22 | } catch (e) { 23 | return false; 24 | } 25 | } 26 | return false; 27 | } 28 | 29 | canActivate(context: ExecutionContext): boolean { 30 | 31 | const isRefresh = this.checkRefresh(context); 32 | if (isRefresh) { 33 | return true; 34 | } 35 | return super.canActivate(context) as boolean; 36 | 37 | } 38 | 39 | 40 | } 41 | -------------------------------------------------------------------------------- /backend/src/projects/projects.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { Projects } from './project.entity'; 4 | import { ProjectsService } from './projects.service'; 5 | import { ProjectsController } from './projects.controller'; 6 | import { ProjectMongoModule } from '@project-mongo/project-mongo.module'; 7 | import { UsersModule } from '@users/users.module'; 8 | import { EnvironmentMongoModule } from '@environment-mongo/environment-mongo.module'; 9 | import { DirectoryMongoModule } from '@directory-mongo/directory-mongo.module'; 10 | import { FileMongoModule } from '@file-mongo/file-mongo.module'; 11 | import { MYSQL_CONN } from '@constants'; 12 | import { ProjectSharesModule } from '@project-shares/project-shares.module'; 13 | 14 | @Module({ 15 | imports: [ 16 | TypeOrmModule.forFeature([Projects], MYSQL_CONN), 17 | forwardRef(() => ProjectMongoModule), 18 | forwardRef(() => UsersModule), 19 | forwardRef(() => EnvironmentMongoModule), 20 | forwardRef(() => DirectoryMongoModule), 21 | forwardRef(() => FileMongoModule), 22 | forwardRef(() => ProjectSharesModule), 23 | ], 24 | providers: [ProjectsService], 25 | controllers: [ProjectsController], 26 | exports: [ProjectsService], 27 | }) 28 | export class ProjectsModule {} 29 | -------------------------------------------------------------------------------- /frontend/src/theme/MenuTheme.tsx: -------------------------------------------------------------------------------- 1 | import { menuAnatomy } from '@chakra-ui/anatomy'; 2 | import { createMultiStyleConfigHelpers } from '@chakra-ui/react'; 3 | 4 | const { definePartsStyle, defineMultiStyleConfig } = 5 | createMultiStyleConfigHelpers(menuAnatomy.keys); 6 | 7 | const baseStyle = definePartsStyle({ 8 | // define the part you're going to style 9 | list: { 10 | // this will style the MenuList component 11 | py: '4', 12 | borderRadius: 'xl', 13 | border: 'none', 14 | bg: 'orange.500', 15 | }, 16 | item: { 17 | // this will style the MenuItem and MenuItemOption components 18 | fontFamily: 'mono', 19 | opacity: '0.8', 20 | color: 'black', 21 | _hover: { 22 | color: 'white', 23 | bg: 'purple.900', 24 | }, 25 | }, 26 | groupTitle: { 27 | // this will style the text defined by the title prop 28 | // in the MenuGroup and MenuOptionGroup components 29 | textTransform: 'uppercase', 30 | color: 'white', 31 | textAlign: 'center', 32 | letterSpacing: 'wider', 33 | opacity: '0.7', 34 | }, 35 | divider: { 36 | // this will style the MenuDivider component 37 | my: '4', 38 | borderColor: 'white', 39 | borderBottom: '2px dotted', 40 | }, 41 | }); 42 | 43 | export const menuTheme = defineMultiStyleConfig({ 44 | baseStyle, 45 | }); 46 | -------------------------------------------------------------------------------- /frontend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "strict": false, 6 | "noImplicitAny": false, 7 | "noImplicitReturns": false, 8 | "noImplicitThis": false, 9 | "strictNullChecks": false, 10 | "noUnusedLocals": false, 11 | "noUnusedParameters": false, 12 | "lib": [ 13 | "ES2020", 14 | "DOM", 15 | "DOM.Iterable" 16 | ], 17 | "module": "ESNext", 18 | "skipLibCheck": true, 19 | "moduleResolution": "bundler", 20 | "allowImportingTsExtensions": true, 21 | "isolatedModules": true, 22 | "moduleDetection": "force", 23 | "noEmit": true, 24 | "jsx": "react-jsx", 25 | "noFallthroughCasesInSwitch": true, 26 | "baseUrl": "./src", 27 | "paths": { 28 | "@store/*": [ 29 | "store/*" 30 | ], 31 | "@config/*": [ 32 | "config/*" 33 | ], 34 | "@types": [ 35 | "types.ts" 36 | ], 37 | "@components/*": [ 38 | "components/*" 39 | ], 40 | "@utils/*": [ 41 | "utils/*" 42 | ], 43 | "@pages/*": [ 44 | "pages/*" 45 | ], 46 | "@hooks/*": [ 47 | "hooks/*" 48 | ], 49 | "@public/*": [ 50 | "../public/*" 51 | ], 52 | "@context/*": [ 53 | "context/*" 54 | ], 55 | "@constants": [ 56 | "constants.ts" 57 | ] 58 | } 59 | }, 60 | "include": [ 61 | "src" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /frontend/src/components/CodeEditor/LineNumber.tsx: -------------------------------------------------------------------------------- 1 | const LineNumberedText = ({ color, text }) => { 2 | const generateLineNumberedText = (text) => { 3 | const lines = text.split('\n'); 4 | 5 | const hasTrailingEmptyLine = lines[lines.length - 1] === ''; 6 | 7 | if ( 8 | hasTrailingEmptyLine && 9 | lines[lines.length - 2]?.endsWith('\n') === false 10 | ) { 11 | lines.pop(); 12 | } 13 | 14 | return lines.map((line, index) => ({ 15 | lineNumber: index + 1, 16 | content: line, 17 | })); 18 | }; 19 | 20 | const lines = generateLineNumberedText(text); 21 | 22 | return ( 23 |
24 | {lines.map((line) => ( 25 |
26 | {/* Line Numbers */} 27 |
28 |
{line.lineNumber}
29 |
30 | 31 |
32 |
38 | {line.content} 39 |
40 |
41 |
42 | ))} 43 |
44 | ); 45 | }; 46 | 47 | export default LineNumberedText; 48 | -------------------------------------------------------------------------------- /backend/src/project-mongo/project-mongo.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | ObjectIdColumn, 4 | Column, 5 | OneToMany, 6 | ManyToOne, 7 | } from 'typeorm'; 8 | import { EnvironmentMongo } from '@environment-mongo/environment-mongo.entity'; 9 | import { FileMongo } from '@file-mongo/file-mongo.entity'; 10 | import { DirectoryMongo } from '@directory-mongo/directory-mongo.entity'; 11 | import { ObjectId } from 'mongodb'; 12 | 13 | @Entity('projects') 14 | export class ProjectMongo { 15 | @ObjectIdColumn() 16 | _id: ObjectId; 17 | 18 | @Column() 19 | project_name: string; 20 | 21 | @Column({ nullable: true, default: null }) // passed from mysql 22 | project_id: string; 23 | 24 | @Column() 25 | owner_id: string; 26 | 27 | @Column() 28 | username: string; 29 | 30 | @Column() 31 | created_at: Date; 32 | 33 | @Column() 34 | updated_at: Date; 35 | 36 | @Column({ nullable: true }) 37 | environment_id: string; 38 | 39 | @ManyToOne(() => EnvironmentMongo, (environment) => environment.projects) 40 | environment: EnvironmentMongo; 41 | 42 | @OneToMany(() => DirectoryMongo, (directory) => directory.project) 43 | directories: DirectoryMongo[]; 44 | 45 | @OneToMany(() => FileMongo, (file) => file.project) 46 | files: FileMongo[]; 47 | 48 | @Column('json', { default: [] }) 49 | shared_with: Array<{ user_id: string; access_level: 'read' | 'write' }>; 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/components/Bars/ThemeSelector.tsx: -------------------------------------------------------------------------------- 1 | import { Button, MenuButton, Menu, MenuList, MenuItem } from '@chakra-ui/react'; 2 | import { useSettings } from '@context/EditorContext'; 3 | 4 | const themes: Record = { 5 | dracula: 'Dracula', 6 | eclipse: 'Eclipse', 7 | material: 'Material', 8 | monokai: 'Monokai', 9 | solarized: 'Solarized', 10 | twilight: 'Twilight', 11 | zenburn: 'Zenburn', 12 | }; 13 | 14 | const ThemeSelector = () => { 15 | const { setTheme, theme } = useSettings()!; 16 | 17 | return ( 18 | <> 19 | 20 | 28 | Theme 29 | 30 | 31 | {Object.keys(themes).map((themeKey, index) => ( 32 | setTheme(themeKey)} 38 | > 39 | {themes[themeKey]} 40 | 41 | ))} 42 | 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default ThemeSelector; 49 | -------------------------------------------------------------------------------- /frontend/src/hooks/useLogOut.ts: -------------------------------------------------------------------------------- 1 | import { selectIsAuthenticated } from '@store/selectors/authSelectors'; 2 | import { useNavigate, useLocation } from 'react-router-dom'; 3 | import { useSelector } from 'react-redux'; 4 | import { useEffect } from 'react'; 5 | import { useAppSelector } from './useApp'; 6 | import { selectUserDetails } from '@store/selectors'; 7 | 8 | const useLogOut = () => { 9 | const userAuthenticated = useSelector(selectIsAuthenticated); 10 | const userDetails = useAppSelector(selectUserDetails); 11 | const navigate = useNavigate(); 12 | const location = useLocation(); 13 | 14 | useEffect(() => { 15 | const protectedRoutes = ['/dashboard', '/profile']; 16 | console.log('User is authenticated', userAuthenticated); 17 | if ( 18 | !userAuthenticated && 19 | protectedRoutes.some((route) => location.pathname.startsWith(route)) 20 | ) { 21 | navigate('/'); 22 | } 23 | if (userAuthenticated && location.pathname === '/') { 24 | if (userDetails?.roles === 'guest') { 25 | navigate(`/editor/${localStorage.getItem('project_id')}`); 26 | } 27 | navigate('/dashboard'); 28 | } 29 | }, [location.pathname, navigate, userAuthenticated, userDetails?.roles]); 30 | 31 | // This useEffect handles taking the user into the home page if he's not authenticated anymore 32 | // It also locks access to any endpoint if user is not authenticated 33 | }; 34 | 35 | export default useLogOut; 36 | -------------------------------------------------------------------------------- /frontend/src/hooks/useAuthRefresh.ts: -------------------------------------------------------------------------------- 1 | import { useRefreshTokenMutation } from '@store/services/auth'; 2 | import { useEffect } from 'react'; 3 | import { useDispatch } from 'react-redux'; 4 | 5 | /** 6 | * Custom React hook for handling authentication token refresh. 7 | * 8 | * This hook automatically attempts to refresh the authentication token 9 | * when the hook is first used, provided that an access token is present 10 | * in local storage. It ensures that the application maintains a valid 11 | * authentication state by refreshing the token as needed. 12 | */ 13 | const useAuthRefresh = () => { 14 | const dispatch = useDispatch(); 15 | const [refreshToken, { isLoading, isSuccess, isError }] = 16 | useRefreshTokenMutation(); 17 | 18 | useEffect(() => { 19 | if (localStorage.getItem('accessToken')) { 20 | console.log('No need to refresh token'); 21 | return; 22 | } 23 | const fetchToken = async () => { 24 | try { 25 | await refreshToken().unwrap(); 26 | } catch (error) { 27 | console.error('Failed to refresh token:', error); 28 | } 29 | }; 30 | const storedAccessToken = localStorage.getItem('accessToken'); 31 | if (storedAccessToken) { 32 | fetchToken(); 33 | } 34 | }, [dispatch, refreshToken]); 35 | // The hook does not return any values; its primary purpose is to 36 | // handle the side effect of refreshing the token. 37 | }; 38 | 39 | export default useAuthRefresh; 40 | -------------------------------------------------------------------------------- /backend/src/environment-mongo/environment-mongo.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Delete, Get, Param, Request } from '@nestjs/common'; 2 | import { EnvironmentMongoService } from './environment-mongo.service'; 3 | import { ApiOperation, ApiTags } from '@nestjs/swagger'; 4 | 5 | @ApiTags('EnvironmentMongo') 6 | @Controller('environments') 7 | export class EnvironmentMongoController { 8 | constructor(private readonly environService: EnvironmentMongoService) {} 9 | 10 | @ApiOperation({ 11 | summary: 'Retrieve all environments', 12 | description: 13 | 'Retrieve a list of all environment documents stored in MongoDB.', 14 | }) 15 | @Get() 16 | async findAll() { 17 | return await this.environService.findAll(); 18 | } 19 | 20 | @ApiOperation({ 21 | summary: 'Retrieve an environment by username', 22 | description: 23 | 'Retrieve a specific environment document from MongoDB using its unique username.', 24 | }) 25 | @Get('me') 26 | async getMyEnvironment(@Request() req) { 27 | return await this.environService.getEnvironmentUsername(req.user.username); 28 | } 29 | 30 | @ApiOperation({ 31 | summary: 'Delete an environment by ID', 32 | description: 33 | 'Delete a specific environment document from MongoDB using its unique ID. Creates a new environment for the user.', 34 | }) 35 | @Delete('me') 36 | async remove(@Request() req) { 37 | return await this.environService.remove(req.user.username); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/src/projects/project.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | Column, 4 | CreateDateColumn, 5 | UpdateDateColumn, 6 | OneToMany, 7 | ManyToOne, 8 | JoinColumn, 9 | PrimaryGeneratedColumn, 10 | } from 'typeorm'; 11 | import { Users } from '@users/user.entity'; 12 | import { ProjectShares } from '@project-shares/project-shares.entity'; 13 | 14 | @Entity({ name: 'Projects' }) 15 | export class Projects { 16 | @PrimaryGeneratedColumn('uuid') 17 | project_id: string; 18 | 19 | @Column({ type: 'varchar', length: 100 }) 20 | project_name: string; 21 | 22 | @Column('uuid') 23 | owner_id: string; 24 | 25 | @Column({ type: 'varchar' }) 26 | username: string; 27 | 28 | @Column({ type: 'varchar', nullable: true, default: null}) 29 | _id: string | null; 30 | 31 | // TODO: Add favorite boolean 32 | @Column({ type: 'boolean', default: false }) 33 | favorite: boolean; 34 | 35 | @Column({ type: 'varchar', nullable: true }) 36 | environment_id: string | null; 37 | 38 | @Column({ type: 'varchar', nullable: true }) 39 | description: string | null; 40 | 41 | @CreateDateColumn() 42 | created_at: Date; 43 | 44 | @UpdateDateColumn() 45 | updated_at: Date; 46 | 47 | @ManyToOne(() => Users, (user) => user.ownedProjects) 48 | @JoinColumn({ name: 'owner_id' }) // References owner_id from the Users table 49 | owner: Users; 50 | 51 | @OneToMany(() => ProjectShares, (projectShare) => projectShare.project) 52 | projectShares: ProjectShares[]; 53 | } 54 | -------------------------------------------------------------------------------- /backend/src/directory-mongo/directory-mongo.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Delete, 4 | Get, 5 | Param, 6 | Post, 7 | Body, 8 | Request, 9 | Patch, 10 | } from '@nestjs/common'; 11 | import { DirectoryMongoService } from './directory-mongo.service'; 12 | import { ApiTags } from '@nestjs/swagger'; 13 | import { 14 | CreateDirectoryOutDto, 15 | UpdateDirectoryOutDto, 16 | } from './dto/create-directory-mongo.dto'; 17 | import DirDocs from './directory-mongo.docs'; 18 | 19 | @ApiTags('DirectoryMongo') 20 | @Controller('directory') 21 | export class DirectoryMongoController { 22 | constructor(private readonly dirService: DirectoryMongoService) {} 23 | 24 | @DirDocs.findAll() 25 | @Get() 26 | async findAll() { 27 | return await this.dirService.findAll(); 28 | } 29 | 30 | @DirDocs.create() 31 | @Post() 32 | async create(@Body() createDirectoryDto: CreateDirectoryOutDto) { 33 | return await this.dirService.create(createDirectoryDto); 34 | } 35 | 36 | @DirDocs.findOne() 37 | @Get(':id') 38 | async findOne(@Param('id') id: string) { 39 | return await this.dirService.findOne(id); 40 | } 41 | 42 | @DirDocs.update() 43 | @Patch(':id') 44 | async update( 45 | @Param('id') id: string, 46 | @Body() updateDirectoryDto: UpdateDirectoryOutDto, 47 | ) { 48 | return await this.dirService.update(id, updateDirectoryDto); 49 | } 50 | 51 | @DirDocs.remove() 52 | @Delete(':id') 53 | async remove(@Param('id') id: string) { 54 | return await this.dirService.remove(id); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /frontend/src/components/Bars/LanguageSelector.tsx: -------------------------------------------------------------------------------- 1 | import { Button, MenuButton, Menu, MenuList, MenuItem } from '@chakra-ui/react'; 2 | import { LanguageCode } from '@utils/codeExamples'; 3 | import { useSettings } from '@context/EditorContext'; 4 | 5 | const languageModes: Record = { 6 | javascript: 'javascript', 7 | python: 'python', 8 | c: 'text/x-csrc', 9 | typescript: 'javascript', 10 | markdown: 'markdown', 11 | html: 'xml', 12 | unknown: 'javascript', 13 | }; 14 | 15 | // animation variants 16 | 17 | const LanguageSelector = () => { 18 | const { setLanguage, language } = useSettings()!; 19 | 20 | return ( 21 | <> 22 | 23 | 31 | Language 32 | 33 | 34 | {Object.keys(languageModes).map((lang) => ( 35 | setLanguage(lang as LanguageCode)} 41 | > 42 | {lang.charAt(0).toUpperCase() + lang.slice(1)} 43 | 44 | ))} 45 | 46 | 47 | 48 | ); 49 | }; 50 | 51 | export default LanguageSelector; 52 | -------------------------------------------------------------------------------- /backend/src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import { AuthController } from './auth.controller'; 4 | import { UsersModule } from '../users/users.module'; 5 | import { EnvironmentMongoModule } from '@environment-mongo/environment-mongo.module'; 6 | import { JwtModule } from '@nestjs/jwt'; 7 | import { jwtConstants } from '../constants'; 8 | import { PassportModule } from '@nestjs/passport'; 9 | import { LocalStrategy } from '@auth/strategies/local.strategy'; 10 | import { JwtStrategy } from '@auth/strategies/jwt.strategy'; 11 | import { SessionSerializer } from '@auth/serializers/session.serializer'; 12 | import { RefreshStrategy } from '@auth/strategies/refresh.strategy'; 13 | import { MailModule } from '@mail/mail.module'; 14 | import { RedisModule } from '@redis/redis.module'; 15 | import { BullModule } from '@nestjs/bullmq'; 16 | 17 | @Module({ 18 | imports: [ 19 | UsersModule, 20 | EnvironmentMongoModule, 21 | MailModule, 22 | RedisModule, 23 | PassportModule.register({ session: true }), 24 | JwtModule.register({ 25 | secret: jwtConstants.secret, 26 | signOptions: { expiresIn: '1d' }, 27 | }), 28 | BullModule.registerQueue({ 29 | name: 'mailer', 30 | }), 31 | ], 32 | providers: [ 33 | AuthService, 34 | LocalStrategy, 35 | JwtStrategy, 36 | SessionSerializer, 37 | RefreshStrategy, 38 | ], 39 | controllers: [AuthController], 40 | exports: [AuthService, JwtModule], 41 | }) 42 | export class AuthModule { } 43 | -------------------------------------------------------------------------------- /frontend/src/store/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import authReducer from './slices/authSlice'; 3 | import userReducer from './slices/userSlice'; 4 | import projectReducer from './slices/projectSlice'; 5 | import projectSharesReducer from './slices/projectSharesSlice'; 6 | import fileReducer from './slices/fileSlice'; 7 | import loggerMiddleware from './middleware/loggerMiddleware'; 8 | import cookieConsentReducer from './slices/cookieConsentSlice'; 9 | import { api } from './services/api'; 10 | 11 | const isDevelopment = process.env.NODE_ENV !== 'production'; 12 | 13 | // Base array of middlewares 14 | const baseMiddlewares = [api.middleware]; 15 | 16 | // Conditionally add the logger middleware 17 | const middleware = isDevelopment 18 | ? [...baseMiddlewares, loggerMiddleware] 19 | : baseMiddlewares; 20 | 21 | // Configures the Redux store with reducers and middleware. 22 | const store = configureStore({ 23 | reducer: { 24 | auth: authReducer, 25 | user: userReducer, 26 | project: projectReducer, 27 | sharedProjects: projectSharesReducer, 28 | file: fileReducer, 29 | cookieConsent: cookieConsentReducer, 30 | [api.reducerPath]: api.reducer, 31 | }, 32 | middleware: (getDefaultMiddleware) => 33 | getDefaultMiddleware().concat(middleware), 34 | }); 35 | 36 | // Type representing the root state of the Redux store. 37 | export type RootState = ReturnType; 38 | // Type representing the app dispatch type 39 | export type AppDispatch = typeof store.dispatch; 40 | export default store; 41 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "resolveJsonModule": true, 5 | "declaration": true, 6 | "removeComments": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "allowSyntheticDefaultImports": true, 10 | "target": "ES2021", 11 | "sourceMap": true, 12 | "outDir": "./dist", 13 | "baseUrl": "./", 14 | "incremental": true, 15 | "skipLibCheck": true, 16 | "strictNullChecks": false, 17 | "noImplicitAny": false, 18 | "strictBindCallApply": false, 19 | "forceConsistentCasingInFileNames": false, 20 | "noFallthroughCasesInSwitch": false, 21 | "paths": { 22 | "@admin/*": ["src/admin/*"], 23 | "@auth/*": ["src/auth/*"], 24 | "@users/*": ["src/users/*"], 25 | "@guest/*": ["src/guest/*"], 26 | "@project-manager/*": ["src/project-manager/*"], 27 | "@projects/*": ["src/projects/*"], 28 | "@project-shares/*": ["src/project-shares/*"], 29 | "@environment-mongo/*": ["src/environment-mongo/*"], 30 | "@project-mongo/*": ["src/project-mongo/*"], 31 | "@project-shares-mongo/*": ["src/project-shares-mongo/*"], 32 | "@directory-mongo/*": ["src/directory-mongo/*"], 33 | "@file-mongo/*": ["src/file-mongo/*"], 34 | "@utils/*": ["src/utils/*"], 35 | "@filters/*": ["src/filters/*"], 36 | "@constants": ["src/constants"], 37 | "@config/*": ["src/config/*"], 38 | "@hocuspocus/*": ["src/hocuspocus/*"], 39 | "@events/*": ["src/events/*"], 40 | "@mail/*": ["src/mail/*"], 41 | "@redis/*": ["src/redis/*"], 42 | "@logging/*": ["src/logging/*"], 43 | "@docker/*": ["src/docker/*"] 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /frontend/src/store/services/directory.ts: -------------------------------------------------------------------------------- 1 | import { api } from './api'; 2 | import { CreateDirectoryDto, Directory } from '@types'; 3 | 4 | export const directoryApi = api.injectEndpoints({ 5 | endpoints: (builder) => ({ 6 | // Fetch all directories 7 | getAllDirectories: builder.query({ 8 | query: () => '/directory', 9 | }), 10 | // Create a new directory 11 | createDirectory: builder.mutation({ 12 | query: (newDirectory) => ({ 13 | url: '/directory', 14 | method: 'POST', 15 | body: newDirectory, 16 | }), 17 | }), 18 | // Fetch a specific directory by its ID 19 | getDirectoryById: builder.query({ 20 | query: (id) => `/directory/${id}`, 21 | }), 22 | // Update a directory by its ID 23 | updateDirectory: builder.mutation< 24 | Directory, 25 | { id: string; data: Partial } 26 | >({ 27 | query: ({ id, data }) => ({ 28 | url: `/directory/${id}`, 29 | method: 'PATCH', 30 | body: data, 31 | }), 32 | }), 33 | // Delete a directory by its ID 34 | deleteDirectory: builder.mutation({ 35 | query: (id) => ({ 36 | url: `/directory/${id}`, 37 | method: 'DELETE', 38 | }), 39 | }), 40 | }), 41 | overrideExisting: false, 42 | }); 43 | 44 | export const { 45 | useGetAllDirectoriesQuery, 46 | useCreateDirectoryMutation, 47 | useGetDirectoryByIdQuery, 48 | useUpdateDirectoryMutation, 49 | useDeleteDirectoryMutation, 50 | } = directoryApi; 51 | -------------------------------------------------------------------------------- /backend/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Set the project directory 4 | PROJECT_DIR="/home/ubuntu/collabor8/backend" 5 | 6 | # Define colors 7 | RED='\e[31m' 8 | GREEN='\e[32m' 9 | YELLOW='\e[33m' 10 | BLUE='\e[34m' 11 | NC='\e[0m' # No Color 12 | 13 | # Navigate to the project directory 14 | cd $PROJECT_DIR 15 | 16 | # Pull the latest changes from the GitHub repository 17 | echo -e "${BLUE}Pulling latest changes from GitHub...${NC}" 18 | git pull origin main 19 | if [ $? -ne 0 ]; then 20 | echo -e "${RED}Error encountered during git pull. Exiting deployment.${NC}" 21 | exit 1 22 | fi 23 | 24 | # Install any new dependencies 25 | echo -e "${BLUE}Installing dependencies...${NC}" 26 | npm install 27 | if [ $? -ne 0 ]; then 28 | echo -e "${RED}Error encountered during npm install. Exiting deployment.${NC}" 29 | exit 1 30 | fi 31 | 32 | # Fix vulnerabilities 33 | echo -e "${YELLOW}Fixing vulnerabilities...${NC}" 34 | npm audit fix 35 | 36 | # Build the NestJS project 37 | echo -e "${BLUE}Building the project...${NC}" 38 | npm run build 39 | if [ $? -ne 0 ]; then 40 | echo -e "${RED}Error encountered during npm build. Exiting deployment.${NC}" 41 | exit 1 42 | fi 43 | 44 | # Start or restart the application using PM2 with updated configuration 45 | echo -e "${BLUE}Restarting the application with PM2...${NC}" 46 | pm2 delete collabor8 || true 47 | pm2 start dist/src/main.js -i 4 --name collabor8 --update-env --env production 48 | if [ $? -ne 0 ]; then 49 | echo -e "${RED}Error encountered during PM2 restart. Exiting deployment.${NC}" 50 | exit 1 51 | fi 52 | 53 | echo -e "${GREEN}Deployment completed successfully.${NC}" 54 | -------------------------------------------------------------------------------- /frontend/src/store/services/file.ts: -------------------------------------------------------------------------------- 1 | import { api } from './api'; 2 | import { File } from '@types'; 3 | 4 | export const fileApi = api.injectEndpoints({ 5 | endpoints: (builder) => ({ 6 | // Fetch a file by its ID 7 | getFileById: builder.query({ 8 | query: (id) => `/files/${id}`, 9 | }), 10 | // Create a new file 11 | createFile: builder.mutation>({ 12 | query: (newFile) => ({ 13 | url: '/files', 14 | method: 'POST', 15 | body: newFile, 16 | }), 17 | }), 18 | // Update a file by its ID 19 | updateFile: builder.mutation }>({ 20 | query: ({ id, data }) => ({ 21 | url: `/files/${id}`, 22 | method: 'PATCH', 23 | body: data, 24 | }), 25 | }), 26 | // Execute a file by its ID 27 | executeFile: builder.mutation< 28 | { output: { stdout: string; stderr: string } }, 29 | { id: string; language: string } 30 | >({ 31 | query: ({ id, language }) => ({ 32 | url: `/files/execute/${id}`, 33 | method: 'POST', 34 | body: { language }, 35 | }), 36 | }), 37 | // Delete a file by its ID 38 | deleteFile: builder.mutation({ 39 | query: (id) => ({ 40 | url: `/files/${id}`, 41 | method: 'DELETE', 42 | }), 43 | }), 44 | }), 45 | overrideExisting: false, 46 | }); 47 | 48 | export const { 49 | useGetFileByIdQuery, 50 | useCreateFileMutation, 51 | useUpdateFileMutation, 52 | useDeleteFileMutation, 53 | useExecuteFileMutation, 54 | } = fileApi; 55 | -------------------------------------------------------------------------------- /backend/src/project-shares/project-shares.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | PrimaryGeneratedColumn, 4 | Column, 5 | CreateDateColumn, 6 | UpdateDateColumn, 7 | ManyToOne, 8 | JoinColumn, 9 | Index, 10 | } from 'typeorm'; 11 | import { Projects } from '@projects/project.entity'; 12 | import { Users } from '@users/user.entity'; 13 | 14 | @Entity('project_shares') 15 | @Index('IDX_PROJECT_USER', ['project_id', 'user_id'], { unique: true }) // Composite unique index on project_id and user_id 16 | export class ProjectShares { 17 | @PrimaryGeneratedColumn('uuid') 18 | share_id: string; 19 | 20 | @Column('uuid') 21 | project_id: string; 22 | 23 | 24 | @Column('uuid') 25 | user_id: string; // id of the guest 26 | 27 | @Column({ type: 'varchar', nullable: true, default: null}) 28 | _id: string | null; 29 | 30 | @Column() 31 | username: string; // Username of guest 32 | 33 | @Column({ default: 'pending' }) 34 | status: string; // Status of the share 35 | 36 | // TODO: Add favorite boolean 37 | @Column({ type: 'boolean', default: false }) 38 | favorite: boolean; 39 | 40 | @Column({ type: 'enum', enum: ['read', 'write'] }) 41 | access_level: 'read' | 'write'; // Access level granted 42 | 43 | @CreateDateColumn() 44 | created_at: Date; 45 | 46 | @UpdateDateColumn() 47 | updated_at: Date; 48 | 49 | @ManyToOne(() => Projects, (project) => project.projectShares) 50 | @JoinColumn({ name: 'project_id' }) // References project_id from the Projects table 51 | project: Projects; 52 | 53 | @ManyToOne(() => Users, (user) => user.projectShares) 54 | @JoinColumn({ name: 'user_id' }) // References user_id from the Users table 55 | user: Users; 56 | } 57 | -------------------------------------------------------------------------------- /backend/src/logging/logging.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Injectable, 5 | NestInterceptor, 6 | } from '@nestjs/common'; 7 | import { Observable } from 'rxjs'; 8 | import { tap } from 'rxjs/operators'; 9 | import { Logger } from '@nestjs/common'; 10 | 11 | @Injectable() 12 | export class LoggingInterceptor implements NestInterceptor { 13 | private readonly logger = new Logger(LoggingInterceptor.name); 14 | 15 | intercept(context: ExecutionContext, next: CallHandler): Observable { 16 | const request = context.switchToHttp().getRequest(); 17 | const response = context.switchToHttp().getResponse(); 18 | let respFlag = false; 19 | 20 | const { method, url, params, query, body } = request; 21 | 22 | this.logger.verbose(`Request - ${method} ${url}`); 23 | this.logger.verbose(`Params: ${JSON.stringify(params)}`); 24 | this.logger.verbose(`Query: ${JSON.stringify(query)}`); 25 | this.logger.verbose(`Body: ${JSON.stringify(body)}`); 26 | 27 | // Intercept the response 28 | const originalSend = response.send.bind(response); 29 | response.send = (body: any) => { 30 | if (body && !respFlag) { 31 | respFlag = true; 32 | this.logger.log(`Response: ${JSON.stringify(body)}`); 33 | 34 | } 35 | return originalSend(body); 36 | }; 37 | 38 | const now = Date.now(); 39 | return next.handle().pipe( 40 | tap((response) => { 41 | if (response && !respFlag) { 42 | respFlag = true; 43 | this.logger.log(`Response: ${JSON.stringify(response)}`); 44 | } 45 | const responseTime = Date.now() - now; 46 | this.logger.verbose(`Response Time: ${responseTime}ms`); 47 | }), 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Set the project directory 4 | PROJECT_DIR="/home/ubuntu/collabor8/backend" 5 | 6 | # Define colors for output messages 7 | RED='\e[31m' 8 | GREEN='\e[32m' 9 | YELLOW='\e[33m' 10 | BLUE='\e[34m' 11 | NC='\e[0m' # No Color 12 | 13 | # Navigate to the project directory 14 | cd "$PROJECT_DIR" || exit 15 | 16 | # Pull the latest changes from the GitHub repository 17 | echo -e "${BLUE}Pulling latest changes from GitHub...${NC}" 18 | if ! git pull origin main; then 19 | echo -e "${RED}Error encountered during git pull. Exiting deployment.${NC}" 20 | exit 1 21 | fi 22 | 23 | # Install any new dependencies 24 | echo -e "${BLUE}Installing dependencies...${NC}" 25 | if ! npm install; then 26 | echo -e "${RED}Error encountered during npm install. Exiting deployment.${NC}" 27 | exit 1 28 | fi 29 | 30 | # Fix vulnerabilities 31 | echo -e "${YELLOW}Fixing vulnerabilities...${NC}" 32 | npm audit fix 33 | 34 | # Build the NestJS project 35 | echo -e "${BLUE}Building the project...${NC}" 36 | if ! npm run build; then 37 | echo -e "${RED}Error encountered during npm build. Exiting deployment.${NC}" 38 | exit 1 39 | fi 40 | 41 | # Use PM2 to start or restart the application using the ecosystem configuration 42 | echo -e "${BLUE}Starting or restarting the application with PM2 using ecosystem.config.js...${NC}" 43 | if ! pm2 startOrRestart ecosystem.config.js --env production; then 44 | echo -e "${RED}Error encountered during PM2 start/restart. Exiting deployment.${NC}" 45 | exit 1 46 | fi 47 | 48 | # Install docker dependencies if Docker is not installed 49 | if ! command -v docker >/dev/null 2>&1; then 50 | echo -e "${BLUE}Installing docker dependencies...${NC}" 51 | ./docker/install.sh 52 | fi 53 | 54 | echo -e "${GREEN}Deployment completed successfully.${NC}" 55 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import path from 'path'; 4 | import tailwindcss from 'tailwindcss'; 5 | import terser from '@rollup/plugin-terser'; 6 | 7 | const __dirname = path.resolve(); 8 | 9 | export default defineConfig(({ mode }) => ({ 10 | css: { 11 | postcss: { 12 | plugins: [tailwindcss()], 13 | }, 14 | }, 15 | plugins: [react()], 16 | server: { 17 | host: 'localhost', 18 | port: 3001, 19 | }, 20 | resolve: { 21 | alias: { 22 | '@store': path.resolve(__dirname, 'src/store'), 23 | '@config': path.resolve(__dirname, 'src/config'), 24 | '@types': path.resolve(__dirname, 'src/types.ts'), 25 | '@components': path.resolve(__dirname, 'src/components'), 26 | '@utils': path.resolve(__dirname, 'src/utils'), 27 | '@pages': path.resolve(__dirname, 'src/pages'), 28 | '@hooks': path.resolve(__dirname, 'src/hooks'), 29 | '@context': path.resolve(__dirname, 'src/context'), 30 | '@constants': path.resolve(__dirname, 'src/constants.ts'), 31 | '@public': path.resolve(__dirname, 'public'), 32 | }, 33 | }, 34 | build: { 35 | assetsInlineLimit: 4096, 36 | sourcemap: mode !== 'production', 37 | rollupOptions: { 38 | plugins: 39 | mode === 'production' 40 | ? [ 41 | terser({ 42 | compress: { 43 | drop_console: true, 44 | drop_debugger: true, 45 | }, 46 | output: { 47 | comments: false, 48 | }, 49 | mangle: { 50 | toplevel: true, 51 | }, 52 | }), 53 | ] 54 | : [], 55 | }, 56 | }, 57 | })); 58 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default tseslint.config({ 18 | languageOptions: { 19 | // other options... 20 | parserOptions: { 21 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 22 | tsconfigRootDir: import.meta.dirname, 23 | }, 24 | }, 25 | }) 26 | ``` 27 | 28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` 29 | - Optionally add `...tseslint.configs.stylisticTypeChecked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: 31 | 32 | ```js 33 | // eslint.config.js 34 | import react from 'eslint-plugin-react' 35 | 36 | export default tseslint.config({ 37 | // Set the react version 38 | settings: { react: { version: '18.3' } }, 39 | plugins: { 40 | // Add the react plugin 41 | react, 42 | }, 43 | rules: { 44 | // other rules... 45 | // Enable its recommended rules 46 | ...react.configs.recommended.rules, 47 | ...react.configs['jsx-runtime'].rules, 48 | }, 49 | }) 50 | ``` 51 | -------------------------------------------------------------------------------- /backend/src/file-mongo/file-mongo.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Delete, 4 | Get, 5 | Param, 6 | Post, 7 | Patch, 8 | Body, 9 | Request, 10 | UseGuards, 11 | } from '@nestjs/common'; 12 | import { FileMongoService } from './file-mongo.service'; 13 | import { FileMongo } from './file-mongo.entity'; 14 | import { ApiOperation, ApiTags } from '@nestjs/swagger'; 15 | import { 16 | CreateFileOutDto, 17 | UpdateFileOutDto, 18 | } from './dto/create-file-mongo.dto'; 19 | import FileDocs from './file-mongo.docs'; 20 | import { AllowApiKey } from '@auth/decorators/isAPIKey.decorator'; 21 | 22 | @ApiTags('FileMongo') 23 | @Controller('files') 24 | export class FileMongoController { 25 | constructor(private readonly fileService: FileMongoService) {} 26 | 27 | @FileDocs.create() 28 | @Post() 29 | async create(@Body() createFileDto: CreateFileOutDto): Promise { 30 | return await this.fileService.create(createFileDto); 31 | } 32 | 33 | @FileDocs.findOne() 34 | @AllowApiKey() 35 | @Get(':id') 36 | async findOne(@Param('id') id: string) { 37 | return await this.fileService.findOne(id); 38 | } 39 | 40 | // TODO: After testing deployement, integrate yjs updates with this method 41 | 42 | @FileDocs.update() 43 | @AllowApiKey() 44 | @Patch(':id') 45 | async update( 46 | @Param('id') id: string, 47 | @Body() updateFileDto: UpdateFileOutDto, 48 | ) { 49 | return await this.fileService.update(id, updateFileDto); 50 | } 51 | 52 | // @FileDocs.execute() 53 | @Post('execute/:id') 54 | async execute(@Param('id') id: string, @Request() req) { 55 | return await this.fileService.execute(id, req); 56 | } 57 | 58 | @FileDocs.remove() 59 | @Delete(':id') 60 | async remove(@Param('id') id: string) { 61 | return await this.fileService.remove(id); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /backend/src/users/users.docs.ts: -------------------------------------------------------------------------------- 1 | import { applyDecorators } from '@nestjs/common'; 2 | import { 3 | ApiOperation, 4 | ApiBody, 5 | ApiOkResponse, 6 | ApiResponse, 7 | getSchemaPath, 8 | ApiHeader, 9 | ApiBearerAuth 10 | } from '@nestjs/swagger'; 11 | import { CreateUserDto, LoginUserDto } from '@users/dto/create-user.dto'; 12 | 13 | class UserDocs { 14 | static init({ summary, description }, ...args) { 15 | return applyDecorators( 16 | ApiOperation({ 17 | summary, 18 | description, 19 | }), 20 | ...args, 21 | ); 22 | } 23 | 24 | @ApiBearerAuth() 25 | findUser() { 26 | return UserDocs.init({ 27 | summary: 'Get current user profile', 28 | description: 'Retrieve the profile of the currently authenticated user.', 29 | }); 30 | } 31 | 32 | updateUser() { 33 | return UserDocs.init( 34 | { 35 | summary: 'Update current user profile', 36 | description: 'Update the profile of the currently authenticated user.', 37 | }, 38 | ApiBody({ 39 | type: CreateUserDto, 40 | }), 41 | ApiOkResponse({ 42 | type: CreateUserDto, 43 | }), 44 | ); 45 | } 46 | removeUser() { 47 | return UserDocs.init({ 48 | summary: 'Delete current user profile', 49 | description: 'Delete the profile of the currently authenticated user.', 50 | }); 51 | } 52 | 53 | findOneById() { 54 | return UserDocs.init({ 55 | summary: 'Get user by ID', 56 | description: 'Retrieve a specific user by their unique ID.', 57 | }); 58 | } 59 | 60 | updateById() { 61 | return UserDocs.init({ 62 | summary: '', 63 | description: '', 64 | }); 65 | } 66 | 67 | remove() { 68 | return UserDocs.init({ 69 | summary: '', 70 | description: '', 71 | }); 72 | } 73 | } 74 | 75 | export default new UserDocs(); 76 | -------------------------------------------------------------------------------- /backend/src/auth/guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | // import { 2 | // CanActivate, 3 | // ExecutionContext, 4 | // Injectable, 5 | // UnauthorizedException, 6 | // } from '@nestjs/common'; 7 | // import { JwtService } from '@nestjs/jwt'; 8 | // import { Reflector } from '@nestjs/core'; 9 | // import { jwtConstants } from '../../constants'; 10 | // import { IS_PUBLIC_KEY } from '@auth/decorators/isPublic.decorator'; 11 | // import { Request } from 'express'; 12 | // // TODO: Blocklist for JWT tokens 13 | // @Injectable() 14 | // export class AuthGuard implements CanActivate { 15 | // constructor( 16 | // private jwtService: JwtService, 17 | // private reflector: Reflector, 18 | // ) {} 19 | 20 | // async canActivate(context: ExecutionContext): Promise { 21 | // const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ 22 | // context.getHandler(), 23 | // context.getClass(), 24 | // ]); 25 | // if (isPublic) { 26 | // return true; 27 | // } 28 | 29 | // const request = context.switchToHttp().getRequest(); 30 | // const token = this.extractTokenFromHeader(request); 31 | // if (!token) { 32 | // throw new UnauthorizedException(); 33 | // } 34 | // try { 35 | // const payload = await this.jwtService.verifyAsync(token, { 36 | // secret: jwtConstants.secret, 37 | // }); 38 | // // 💡 We're assigning the payload to the request object here 39 | // // so that we can access it in our route handlers 40 | // request['user'] = payload; 41 | // } catch { 42 | // throw new UnauthorizedException(); 43 | // } 44 | // return true; 45 | // } 46 | 47 | // private extractTokenFromHeader(request: Request): string | undefined { 48 | // const [type, token] = request.headers.authorization?.split(' ') ?? []; 49 | // return type === 'Bearer' ? token : undefined; 50 | // } 51 | // } 52 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer utilities { 6 | .text-last-center { 7 | text-align-last: center; 8 | } 9 | .inset-shadow { 10 | box-shadow: 11 | inset -50px -60px 25px 20px theme('colors.brand.100'), 12 | inset 50px 60px 25px 20px theme('colors.brand.100'); 13 | } 14 | .shadow-cookie { 15 | box-shadow: inset 0px 0px 15px 20px theme('colors.brand.100'); 16 | @apply lg:inset-shadow; 17 | } 18 | } 19 | 20 | html, 21 | body, 22 | div#root { 23 | width: 100%; 24 | /*height: 100%;*/ 25 | min-height: 100%; 26 | } 27 | 28 | .remote-caret { 29 | position: absolute; 30 | border-left: black; 31 | border-left-style: solid; 32 | border-left-width: 2px; 33 | height: 1em; 34 | } 35 | .remote-caret > div { 36 | position: relative; 37 | top: -1.05em; 38 | font-size: 13px; 39 | background-color: rgb(250, 129, 0); 40 | font-family: serif; 41 | font-style: normal; 42 | font-weight: normal; 43 | line-height: normal; 44 | user-select: none; 45 | color: white; 46 | padding-left: 2px; 47 | padding-right: 2px; 48 | z-index: 3; 49 | } 50 | 51 | .menu-item { 52 | z-index: 50; 53 | } 54 | 55 | .inline-menu-item { 56 | display: inline-block; 57 | margin-right: 0.5rem; 58 | padding: 0.5rem 1rem; 59 | border-radius: 0.375rem; 60 | background-color: #1a1a1a; 61 | color: white; 62 | cursor: pointer; 63 | transition: background-color 0.25s; 64 | } 65 | .inline-menu-item:hover { 66 | background-color: #646cff; 67 | } 68 | 69 | @media (prefers-color-scheme: light) { 70 | :root { 71 | color: #213547; 72 | background-color: #ffffff; 73 | } 74 | a:hover { 75 | color: #747bff; 76 | } 77 | button { 78 | background-color: #f9f9f9; 79 | } 80 | .inline-menu-item { 81 | background-color: #f9f9f9; 82 | color: #213547; 83 | } 84 | .inline-menu-item:hover { 85 | background-color: #747bff; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /runon.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Run script on remote server 3 | 4 | # process command and its flags 5 | # leading colon to suppresses error message for invalid flag 6 | # trailing colon indicates flag requires argument 7 | while getopts ":s:f:h" opt; do 8 | case "$opt" in 9 | s) HOST="$OPTARG";; 10 | f) FILE="$OPTARG";; 11 | h) cat <<- _EOF_ 12 | Usage: runon [-s server] [-f filename] 13 | Options: 14 | -h help : this help 15 | -s server : Host name that you configured in your ~/.ssh/config file (see down below) 16 | -f file : executable file 17 | OR: 18 | u can use the script w/out flags, then u'll be prompted for host name and file 19 | **NOTE** : 20 | before you run the script make sure u have the following in '~/.ssh/config' file: 21 | Host web-01 22 | Hostname 23 | IdentityFile 24 | User ubuntu 25 | Host web-02 26 | Hostname 27 | IdentityFile 28 | User ubuntu 29 | Host balancer 30 | Hostname 31 | IdentityFile 32 | User ubuntu 33 | _EOF_ 34 | exit;; 35 | \?) echo "Invalid option -$OPTARG";; 36 | esac 37 | done 38 | 39 | if [[ -n $HOST && $FILE ]]; then 40 | [ ! -x "$FILE" ] && echo -e "${COLOR_RED}file does not have execute permissions${RESET}" && exit 1 41 | echo -e "${COLOR_CYAN}Processing...${RESET}" 42 | ssh "$HOST" 'sudo bash -s' < "$FILE" 43 | [ "$?" -eq 0 ] && echo -e "${COLOR_GREEN}Done!${RESET}" 44 | exit "$?" 45 | fi 46 | 47 | [ -z "$HOST" ] && read -rp "host name -> " HOST 48 | [ -z "$FILE" ] && read -rp "script file to execute -> " FILE 49 | 50 | [ ! -x "$FILE" ] && echo -e "${COLOR_RED}file does not have execute permissions${RESET}" && exit 1 51 | 52 | echo -e "${COLOR_CYAN}Processing...${RESET}" 53 | ssh "$HOST" 'sudo bash -s' < "$FILE" 54 | [ "$?" -eq 0 ] && echo -e "${COLOR_GREEN}Done!${RESET}" 55 | -------------------------------------------------------------------------------- /frontend/src/components/Slogan.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable prettier/prettier */ 2 | import { Box } from '@chakra-ui/react'; 3 | import CallToAction from './Buttons/CallToAction'; 4 | 5 | type SloganProps = { 6 | className?: string; 7 | }; 8 | 9 | export default function Slogan({ className = '' }: SloganProps) { 10 | return ( 11 | 12 | 13 | 14 |

15 | code 16 |

17 |
18 | 19 |

20 | together 21 |

22 |
23 | 24 | 29 |

30 | 31 | Collaborative coding made simple. Join developers worldwide and 32 | create together. 33 | 34 |

35 |
36 | 37 |

faster

38 | ^ 39 | * 40 |
41 | 42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /backend/src/config/configuration.ts: -------------------------------------------------------------------------------- 1 | // TODO: incomplete and might be removed 2 | import * as dotenv from 'dotenv'; 3 | dotenv.config(); 4 | export default () => ({ 5 | port: parseInt(process.env.PORT, 10) || 3000, 6 | database: { 7 | host: process.env.DATABASE_HOST, 8 | port: parseInt(process.env.DATABASE_PORT, 10) || 5432, 9 | }, 10 | }); 11 | 12 | export const corsConfig = { 13 | origin: (origin, callback) => { 14 | const allowedOrigins = [ 15 | 'http://localhost:3001', 16 | 'http://localhost:3001/', 17 | 'http://localhost:1234', 18 | 'https://collabor8-socket.abdallah.tech', 19 | 'https://co11abor8.netlify.app/', 20 | 'https://co11abor8.netlify.app', 21 | ]; 22 | 23 | if (!origin || allowedOrigins.includes(origin)) { 24 | callback(null, true); 25 | } else { 26 | callback(new Error('Not allowed by CORS')); 27 | } 28 | }, 29 | methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS', 30 | preflightContinue: false, 31 | optionsSuccessStatus: 204, 32 | credentials: true, 33 | }; 34 | 35 | export const adminEmails = [ 36 | 'abdofola67@gmail.com', 37 | 'ireosama1@gmail.com', 38 | 'mohannadabdo21@gmail.com', 39 | 'mo7amedelfadil@gmail.com', 40 | ]; 41 | 42 | export const appConfig = { 43 | appName: 'Collabor8', 44 | appUrl: 'https://co11abor8.netlify.app', 45 | appID: process.env.AGORA_APPID, 46 | appCertificate: process.env.AGORA_CERTIFICATE, 47 | }; 48 | 49 | export const cookieConfig = { 50 | secure: process.env.NODE_ENV === 'production', 51 | sameSite: process.env.NODE_ENV === 'production' ? 'None' : 'Lax', 52 | // maxAge: 1000 * 60 * 60 * 24 * 7, 53 | httpOnly: true, 54 | // domain: process.env.NODE_ENV === 'production' ? '.co11abor8.netlify.app' : 'localhost', 55 | // path: '/', 56 | }; 57 | 58 | export const accessTokenCookieConfig = { 59 | secure: process.env.NODE_ENV === 'production', 60 | sameSite: process.env.NODE_ENV === 'production' ? 'None' : 'Lax', 61 | httpOnly: false, 62 | }; 63 | -------------------------------------------------------------------------------- /backend/src/main.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { AppModule } from './app.module'; 4 | import { HttpExceptionFilter } from './filters/http-exception.filter'; 5 | import { corsConfig, cookieConfig } from '@config/configuration'; 6 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; 7 | import { ValidationPipe } from '@nestjs/common'; 8 | import * as session from 'express-session'; 9 | import * as passport from 'passport'; 10 | import { jwtConstants } from './constants'; 11 | import * as cookieParser from 'cookie-parser'; 12 | import { LoggingInterceptor } from '@logging/logging.interceptor'; 13 | 14 | async function bootstrap() { 15 | const app = await NestFactory.create(AppModule, { 16 | logger: ['error', 'warn', 'log', 'debug', 'verbose'], 17 | }); 18 | const options = new DocumentBuilder() 19 | .addBearerAuth() 20 | .setTitle('Collabor8') 21 | .setDescription('Collabor8 API, the padlock means endpoint requires authentication') 22 | .setVersion('1.0') 23 | .addServer('http://localhost:3000/api/v1', 'Local environment') 24 | .addServer('https://staging.yourapi.com/api/v1', 'Staging') 25 | .addServer('https://production.yourapi.com/api/v1', 'Production') 26 | .addTag('Your API Tag') 27 | .build(); 28 | 29 | const document = SwaggerModule.createDocument(app, options); 30 | SwaggerModule.setup('api-docs', app, document); 31 | app.use( 32 | session({ 33 | secret: jwtConstants.secret, 34 | resave: true, 35 | saveUninitialized: false, 36 | cookie: cookieConfig, 37 | }), 38 | ); 39 | app.use(cookieParser()); 40 | app.use(passport.initialize()); 41 | app.use(passport.session()); 42 | app.useGlobalFilters(new HttpExceptionFilter()); 43 | app.setGlobalPrefix('api/v1'); 44 | app.useGlobalPipes(new ValidationPipe()); 45 | app.useGlobalInterceptors(new LoggingInterceptor()); 46 | app.enableCors(corsConfig); 47 | 48 | await app.listen(3000); 49 | } 50 | bootstrap(); 51 | -------------------------------------------------------------------------------- /backend/create_mongodb_db.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Function to display help message 4 | display_help() { 5 | echo "Usage: $0 [-d ] [-c ] [-h]" 6 | echo 7 | echo "Options:" 8 | echo " -d Specify the database name to create." 9 | echo " -c (Optional) Specify the collection name to create in the database." 10 | echo " -h Display this help message." 11 | exit 1 12 | } 13 | 14 | # Default values 15 | DB_NAME="" 16 | COL_NAME="" 17 | 18 | # Parse command-line options 19 | while getopts ":d:c:h" opt; do 20 | case ${opt} in 21 | d) 22 | DB_NAME=$OPTARG 23 | ;; 24 | c) 25 | COL_NAME=$OPTARG 26 | ;; 27 | h) 28 | display_help 29 | ;; 30 | \?) 31 | echo "Invalid option: -$OPTARG" >&2 32 | display_help 33 | ;; 34 | :) 35 | echo "Option -$OPTARG requires an argument." >&2 36 | display_help 37 | ;; 38 | esac 39 | done 40 | 41 | # Check if database name is provided 42 | if [ -z "$DB_NAME" ]; then 43 | echo "Database name is required." 44 | display_help 45 | fi 46 | 47 | # Create the database 48 | # Use `mongosh` to run the commands 49 | if [ -n "$COL_NAME" ]; then 50 | # Create the database and collection 51 | mongosh --eval " 52 | use ${DB_NAME}; 53 | db.${COL_NAME}.insertOne({ name: 'initialData' }); 54 | " > /dev/null 55 | if [ $? -eq 0 ]; then 56 | echo "Database '${DB_NAME}' and collection '${COL_NAME}' created successfully." 57 | else 58 | echo "Failed to create database or collection." 59 | exit 1 60 | fi 61 | else 62 | # Create the database without collection 63 | mongosh --eval "use ${DB_NAME};" > /dev/null 64 | if [ $? -eq 0 ]; then 65 | echo "Database '${DB_NAME}' created successfully." 66 | else 67 | echo "Failed to create database." 68 | exit 1 69 | fi 70 | fi 71 | -------------------------------------------------------------------------------- /frontend/src/pages/Verify.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { useVerifyEmailMutation } from '@store/services/auth'; 4 | import { useAppDispatch } from '@hooks/useApp'; 5 | import { setCredentials } from '@store/slices/authSlice'; 6 | import { Skeleton, useToast } from '@chakra-ui/react'; 7 | import { setUserDetails } from '@store/slices/userSlice'; 8 | import usePageTitle from '@hooks/useTitle'; 9 | 10 | function Verify() { 11 | // extarct the query from url 12 | const navigate = useNavigate(); 13 | const toast = useToast(); 14 | const [verify, { isLoading, isUninitialized }] = useVerifyEmailMutation(); 15 | const dispatch = useAppDispatch(); 16 | usePageTitle('Verify Email - Collabor8'); 17 | 18 | useEffect(() => { 19 | const parmas = new URLSearchParams(window.location.search); 20 | const token = parmas.get('token')!; 21 | // console.log('-----token------>', { token }); 22 | verify({ token }) 23 | .unwrap() 24 | .then((data: any) => { 25 | console.log('------verify--------->', { data }); 26 | toast({ 27 | title: 'Verification', 28 | description: 'your account has been verified', 29 | status: 'success', 30 | variant: 'subtle', 31 | position: 'bottom', 32 | }); 33 | dispatch(setCredentials({ accessToken: data.accessToken })); 34 | dispatch(setUserDetails(data.user)); 35 | navigate('/dashboard'); 36 | }) 37 | .catch((err) => { 38 | console.log('--------------->', 'background: red', { err: err.data }); 39 | toast({ 40 | title: 'Verification', 41 | description: 'your account has not been veified, please try again', 42 | status: 'error', 43 | variant: 'subtle', 44 | position: 'bottom', 45 | }); 46 | }) 47 | .finally(() => { 48 | console.log('%c------------resolved------------->', 'background:green'); 49 | }); 50 | }, []); 51 | 52 | return ; 53 | } 54 | 55 | export default Verify; 56 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense, lazy } from 'react'; 2 | import { Route, Routes } from 'react-router-dom'; 3 | import useAuthRefresh from '@hooks/useAuthRefresh'; 4 | import useLogOut from '@hooks/useLogOut'; 5 | import ResetPasswordModal from '@components/Modals/ResetPassword'; 6 | import { AuthProvider } from '@context/AuthContext'; 7 | import ProtectedRoute from '@components/ProtectedRoute'; 8 | import ThemedLoader from '@utils/Spinner'; 9 | import './App.css'; 10 | 11 | const Verify = lazy(() => import('@pages/Verify')); 12 | const NotFound = lazy(() => import('@pages/404_page')); 13 | const About = lazy(() => import('@pages/About')); 14 | const Profile = lazy(() => import('@pages/Profile')); 15 | const Editor = lazy(() => import('@pages/Editor')); 16 | const Home = lazy(() => import('@pages/Home')); 17 | const Dashboard = lazy(() => import('@pages/DashboardPage')); 18 | const InviteGuest = lazy(() => import('@components/Invite')); 19 | const CookiePolicy = lazy(() => import('@pages/CookiePolicy')); 20 | 21 | const App: React.FC = () => { 22 | useAuthRefresh(); 23 | useLogOut(); 24 | 25 | return ( 26 | 27 | }> 28 | 29 | } /> 30 | } /> 31 | } /> 32 | } />} 35 | /> 36 | } />} 39 | /> 40 | } />} 43 | /> 44 | } /> 45 | } /> 46 | } /> 47 | } /> 48 | 49 | 50 | 51 | ); 52 | }; 53 | 54 | export default App; 55 | -------------------------------------------------------------------------------- /frontend/src/store/services/admin.ts: -------------------------------------------------------------------------------- 1 | import { api } from './api'; 2 | import { User, Project, File } from '@types'; 3 | 4 | export const adminApi = api.injectEndpoints({ 5 | endpoints: (builder) => ({ 6 | // Retrieve all users (admin only) 7 | findAllUsers: builder.query({ 8 | query: () => ({ 9 | url: '/admin/all', 10 | method: 'GET', 11 | }), 12 | }), 13 | // Delete all users (admin only) 14 | removeAllUsers: builder.mutation<{ message: string }, void>({ 15 | query: () => ({ 16 | url: '/admin/all', 17 | method: 'DELETE', 18 | }), 19 | }), 20 | // Retrieve a specific user profile by username (admin only) 21 | findUserAdmin: builder.query({ 22 | query: (username) => ({ 23 | url: `/admin/profile/${username}`, 24 | method: 'GET', 25 | }), 26 | }), 27 | // Update a specific user profile by username (admin only) 28 | updateUserAdmin: builder.mutation< 29 | User, 30 | { username: string; data: Partial } 31 | >({ 32 | query: ({ username, data }) => ({ 33 | url: `/admin/profile/${username}`, 34 | method: 'PATCH', 35 | body: data, 36 | }), 37 | }), 38 | 39 | // Delete a specific user profile by username (admin only) 40 | removeUserAdmin: builder.mutation<{ message: string }, string>({ 41 | query: (username) => ({ 42 | url: `/admin/profile/${username}`, 43 | method: 'DELETE', 44 | }), 45 | }), 46 | // Retrieve all projects (admin only) 47 | findAllProjects: builder.query({ 48 | query: () => ({ 49 | url: '/admin', 50 | method: 'GET', 51 | }), 52 | }), 53 | // Retrieve all files (admin only) 54 | findAllFiles: builder.query({ 55 | query: () => ({ 56 | url: '/admin', 57 | method: 'GET', 58 | }), 59 | }), 60 | }), 61 | overrideExisting: false, 62 | }); 63 | 64 | export const { 65 | useFindAllUsersQuery, 66 | useRemoveAllUsersMutation, 67 | useFindUserAdminQuery, 68 | useUpdateUserAdminMutation, 69 | useRemoveUserAdminMutation, 70 | useFindAllProjectsQuery, 71 | useFindAllFilesQuery, 72 | } = adminApi; 73 | -------------------------------------------------------------------------------- /backend/src/auth/guards/jwt-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; 2 | 3 | import { AuthGuard } from '@nestjs/passport'; 4 | import { Reflector } from '@nestjs/core'; 5 | import { IS_PUBLIC_KEY } from '@auth/decorators/isPublic.decorator'; 6 | import { IS_API_KEY } from '@auth/decorators/isAPIKey.decorator'; 7 | import { JwtService } from '@nestjs/jwt'; 8 | import { ConfigService } from '@nestjs/config'; 9 | 10 | @Injectable() 11 | export class JwtAuthGuard extends AuthGuard('jwt') implements CanActivate { 12 | constructor( 13 | private reflector: Reflector, 14 | private configService: ConfigService 15 | ) { 16 | super(); 17 | } 18 | 19 | /** 20 | * checkApiKey - Check if the API Key is valid for linking with the socket 21 | * connection. 22 | * @param request - The request object 23 | * @returns true if the API Key is valid, false otherwise 24 | */ 25 | checkApiKey(request: Request): boolean { 26 | const apiKey = request.headers['x-api-key']; 27 | const validApiKey = this.configService.get('API_KEY'); 28 | return apiKey === validApiKey; 29 | } 30 | 31 | /** 32 | * canActivate - Check if the request is allowed to proceed. 33 | * @param context - The execution context 34 | * @returns true if the request is allowed to proceed, false otherwise 35 | */ 36 | async canActivate(context: ExecutionContext): Promise { 37 | const isApiKey = this.reflector.getAllAndOverride(IS_API_KEY, [ 38 | context.getHandler(), 39 | context.getClass(), 40 | ]); 41 | if ( 42 | isApiKey && 43 | this.checkApiKey( 44 | context.switchToHttp().getRequest() 45 | )) { 46 | return true; 47 | } 48 | 49 | const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ 50 | context.getHandler(), 51 | context.getClass(), 52 | ]); 53 | if (isPublic) { 54 | return true; 55 | } 56 | 57 | return await super.canActivate(context) as boolean; 58 | } 59 | 60 | handleRequest(err, user, info) { 61 | if (err || !user) { 62 | throw err || new UnauthorizedException(); 63 | } 64 | return user; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /backend/src/guest/guest.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Request, 7 | Query, 8 | Response, 9 | } from '@nestjs/common'; 10 | import { GuestService } from '@guest/guest.service'; 11 | import { Public } from '@auth/decorators/isPublic.decorator'; 12 | import { accessTokenCookieConfig, cookieConfig } from '@config/configuration'; 13 | 14 | @Controller('guest') 15 | export class GuestController { 16 | constructor( 17 | private readonly guestService: GuestService, 18 | ) { } 19 | /** 20 | * triggered when user clicks on generate invite link button 21 | * for guest, we only allow to generate invite link (no username or email) 22 | * for user, we allow to generate invite link as well as the normal functionality 23 | * of inviting a user by username or email 24 | * this handles the following cases: 25 | * guest to user invite (requires username to be sent from frontend when user 26 | * redirects to the invite link) 27 | * we check if user is registered then we create a projectShare 28 | * user to guest invite ( 29 | * guest to guest invite 30 | * returns projectInfo and accessToken (if guest) 31 | * consider separating the guest and user invite functionality to separate endpoints 32 | */ 33 | @Post('invite') 34 | async invite( 35 | @Body() body: any, 36 | @Request() req: any, 37 | ): Promise { 38 | } 39 | 40 | @Public() 41 | @Post('login') 42 | async tryout( 43 | @Request() req: any, 44 | @Response() res: any, 45 | ): Promise { 46 | const { user, userData, accessToken, refreshToken } = await this.guestService.login(); 47 | req.user = userData; // set user on the request to satisfy the local strategy 48 | 49 | res 50 | .cookie('refreshToken', refreshToken, cookieConfig) 51 | .cookie('accessToken', accessToken, accessTokenCookieConfig) 52 | .status(200).send({ user, accessToken }); 53 | } 54 | 55 | @Public() 56 | @Get('project') 57 | async getGuestProject( 58 | @Query('IP') IP: string, 59 | @Response() res: any, 60 | ): Promise { 61 | const project = await this.guestService.createOrGetProject(IP); 62 | 63 | res.status(200).send({ redirect: project._id }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /backend/migrate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function showHelp() { 4 | echo "Usage: migrateDB [migrationName]" 5 | echo "Create a new migration file or run migrations" 6 | echo "Options:" 7 | echo " -h Display help" 8 | echo " -r Run the migration after creating it" 9 | echo " -u Revert the last migration" 10 | echo " -c Clear all migrations" 11 | echo " -g Generate a new migration file" 12 | echo " -s Show all migrations" 13 | } 14 | 15 | function migrateDB() { 16 | local migration_name="" 17 | local runFlag=false 18 | local generateFlag=false 19 | 20 | while getopts ":cghrsu" opt; do 21 | case ${opt} in 22 | c ) 23 | echo "Clearing all migrations..." 24 | rm -rf ./db/migrations/*.ts 25 | exit 0 26 | ;; 27 | h ) 28 | showHelp 29 | exit 0 30 | ;; 31 | g ) 32 | generateFlag=true 33 | ;; 34 | r ) 35 | runFlag=true 36 | ;; 37 | s ) 38 | echo "Showing all migrations..." 39 | npm run migration:show 40 | exit 0 41 | ;; 42 | u ) 43 | echo "Reverting the last migration..." 44 | npm run migration:revert 45 | exit 0 46 | ;; 47 | \? ) 48 | echo "Invalid Option: -$OPTARG" 1>&2 49 | exit 1 50 | ;; 51 | esac 52 | done 53 | shift $((OPTIND -1)) 54 | 55 | migration_name="$1" 56 | 57 | if [ -z "$migration_name" ]; then 58 | echo "Error: Missing migration name" 59 | showHelp 60 | exit 1 61 | fi 62 | 63 | if [ "$generateFlag" = true ]; then 64 | echo "Generating a new migration file... $migration_name" 65 | npm run migration:generate --name="$migration_name" 66 | else 67 | echo "Creating a new migration file... $migration_name" 68 | npm run migration:create --name="$migration_name" 69 | 70 | if [ "$runFlag" = true ]; then 71 | echo "Running the migration..." 72 | npm run migration:run 73 | fi 74 | fi 75 | } 76 | 77 | migrateDB "$@" 78 | 79 | -------------------------------------------------------------------------------- /frontend/src/store/selectors/projectSelectors.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file contains selectors for accessing project-related data from the 3 | * Redux store. Selectors are used to derive and access specific pieces of 4 | * state related to the project list, loading states, and error states. 5 | * 6 | * Responsibilities: 7 | * - Retrieve the project list from the state. 8 | * - Filter the list based on search criteria. 9 | * - Sort the list based on a specified field. 10 | * - Paginate the list based on the current page and page size. 11 | * - Retrieve a specific project by ID. 12 | */ 13 | 14 | import { createSelector } from '@reduxjs/toolkit'; 15 | import { RootState } from '@store/store'; 16 | 17 | // Selector to get the recent projects from the state. 18 | export const selectRecentProjects = createSelector( 19 | (state: RootState) => state.project.recentProjects, 20 | (recentProjects) => recentProjects, 21 | ); 22 | 23 | // Selector to get the user project list from the state. 24 | export const selectUserProjects = createSelector( 25 | (state: RootState) => state.project.userProjects, 26 | (userProjects) => userProjects, 27 | ); 28 | 29 | // Selector to get the share project list from the state. 30 | export const selectSharedProjects = createSelector( 31 | (state: RootState) => state.project.sharedProjects, 32 | (sharedProjects) => sharedProjects, 33 | ); 34 | 35 | export const selectAllProjects = createSelector( 36 | (state: RootState) => state.project.allProjects, 37 | (allProjects) => allProjects, 38 | ); 39 | 40 | export const selectRecentProjectsPagination = createSelector( 41 | (state: RootState) => state.project.pagination.recentProjects, 42 | (recentProjectsPagination) => recentProjectsPagination, 43 | ); 44 | 45 | export const selectUserProjectsPagination = createSelector( 46 | (state: RootState) => state.project.pagination.userProjects, 47 | 48 | (userProjectsPagination) => userProjectsPagination, 49 | ); 50 | 51 | export const selectSharedProjectsPagination = createSelector( 52 | (state: RootState) => state.project.pagination.sharedProjects, 53 | (sharedProjectsPagination) => sharedProjectsPagination, 54 | ); 55 | 56 | export const selectAllProjectsPagination = createSelector( 57 | (state: RootState) => state.project.pagination.allProjects, 58 | (allProjectsPagination) => allProjectsPagination, 59 | ); 60 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint . --ext .ts,.tsx", 10 | "format": "prettier --write \"src/**/*.ts\" \"src/**/*.tsx\" \"src/**/*.css\"", 11 | "preview": "vite preview", 12 | "start:dev": "cross-env NODE_ENV=development vite", 13 | "start:prod": "cross-env NODE_ENV=production vite" 14 | }, 15 | "dependencies": { 16 | "@chakra-ui/anatomy": "^2.2.2", 17 | "@chakra-ui/icons": "^2.1.1", 18 | "@chakra-ui/react": "^2.8.2", 19 | "@emotion/react": "^11.13.3", 20 | "@emotion/styled": "^11.13.0", 21 | "@reduxjs/toolkit": "^2.2.7", 22 | "@uploadcare/upload-client": "^6.14.1", 23 | "agora-rtc-sdk-ng": "^4.22.0", 24 | "codemirror": "^5.65.17", 25 | "date-fns": "^3.6.0", 26 | "framer-motion": "^11.3.30", 27 | "mongodb": "^6.8.0", 28 | "randomcolor": "^0.6.2", 29 | "react": "^18.3.1", 30 | "react-codemirror2": "^8.0.0", 31 | "react-dom": "^18.3.1", 32 | "react-draggable": "^4.4.6", 33 | "react-icons": "^5.3.0", 34 | "react-redux": "^9.1.2", 35 | "react-router-dom": "^6.26.0", 36 | "tailwindcss": "^3.4.9", 37 | "y-codemirror": "^3.0.1", 38 | "y-websocket": "^2.0.4", 39 | "yjs": "^13.6.18", 40 | "zustand": "^4.5.5", 41 | "zustand-yjs": "^0.0.14" 42 | }, 43 | "devDependencies": { 44 | "@eslint/js": "^9.8.0", 45 | "@rollup/plugin-terser": "^0.4.4", 46 | "@types/codemirror": "^5.60.15", 47 | "@types/node": "^22.3.0", 48 | "@types/randomcolor": "^0.5.9", 49 | "@types/react": "^18.3.3", 50 | "@types/react-dom": "^18.3.0", 51 | "@typescript-eslint/eslint-plugin": "^8.0.1", 52 | "@typescript-eslint/parser": "^8.0.1", 53 | "@vitejs/plugin-react": "^4.3.1", 54 | "cross-env": "^7.0.3", 55 | "eslint": "^9.8.0", 56 | "eslint-config-prettier": "^9.1.0", 57 | "eslint-plugin-prettier": "^5.2.1", 58 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 59 | "eslint-plugin-react-refresh": "^0.4.9", 60 | "globals": "^15.9.0", 61 | "prettier": "^3.3.3", 62 | "typescript": "^5.5.3", 63 | "vite": "^5.4.0" 64 | }, 65 | "overrides": { 66 | "react-lorem-component": { 67 | "react": "^18.3.1" 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /frontend/src/utils/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from 'react'; 2 | import { Box, Center, Text } from '@chakra-ui/react'; 3 | import { keyframes } from '@emotion/react'; 4 | 5 | // set of code snippets 6 | const codeSnippets = [ 7 | 'const a = 1;', 8 | 'let b = "hello";', 9 | 'function foo() {}', 10 | 'if (x > 10) { console.log("x is large"); }', 11 | 'for (let i = 0; i < 10; i++) { console.log(i); }', 12 | 'class MyClass {}', 13 | 'return Math.random();', 14 | 'import React from "react";', 15 | ]; 16 | 17 | // Keyframes for fade animation 18 | const fadeAnimation = keyframes` 19 | 0% { opacity: 0; transform: scale(0.95); } 20 | 50% { opacity: 1; transform: scale(1); } 21 | 100% { opacity: 0; transform: scale(0.95); } 22 | `; 23 | 24 | // random code snippet excluding the current snippet 25 | const getRandomSnippet = (excludeSnippet: string) => { 26 | const filteredSnippets = codeSnippets.filter( 27 | (snippet) => snippet !== excludeSnippet, 28 | ); 29 | const randomIndex = Math.floor(Math.random() * filteredSnippets.length); 30 | return filteredSnippets[randomIndex]; 31 | }; 32 | 33 | const CodeSnippetLoader: React.FC = () => { 34 | const [snippet, setSnippet] = useState(getRandomSnippet('')); 35 | 36 | const updateSnippet = useCallback(() => { 37 | setSnippet((prevSnippet) => getRandomSnippet(prevSnippet)); 38 | }, []); 39 | 40 | // Update the snippet every 1.5 seconds 41 | useEffect(() => { 42 | const interval = setInterval(updateSnippet, 1500); 43 | 44 | return () => clearInterval(interval); 45 | }, [updateSnippet]); 46 | 47 | return ( 48 |
54 | 55 | 56 | 65 | {snippet} 66 | 67 | 68 | 69 |
70 | ); 71 | }; 72 | 73 | export default CodeSnippetLoader; 74 | -------------------------------------------------------------------------------- /frontend/src/components/Bars/Shares.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Avatar, 4 | IconButton, 5 | Tooltip, 6 | useMediaQuery, 7 | } from '@chakra-ui/react'; 8 | import { AddIcon } from '@chakra-ui/icons'; 9 | import { useFile } from '@context/EditorContext'; 10 | import { useDisclosure } from '@chakra-ui/react'; 11 | import ShareMenu from '../Modals/ShareMenu'; 12 | import { Project, ProjectShares } from '@types'; 13 | import { useSelector } from 'react-redux'; 14 | import { selectUserDetails } from '@store/selectors'; 15 | 16 | interface SharesProps { 17 | project: Project | ProjectShares; 18 | className?: string; 19 | [k: string]: any; 20 | } 21 | 22 | export default function Shares({ 23 | className = '', 24 | project, 25 | ...rest 26 | }: SharesProps) { 27 | const { awareness } = useFile()!; // why so excited? 28 | const { isOpen, onOpen, onClose } = useDisclosure(); 29 | const [isLessThan768] = useMediaQuery('(max-width: 768px)'); 30 | const userDetails = useSelector(selectUserDetails); 31 | 32 | // TODO: The plus icon is displayed only for the project owner 33 | return ( 34 | 35 | 39 | {Array.from(awareness)?.map(([_, value], index) => ( 40 | 41 | 42 | 43 | ))} 44 | {isLessThan768 && ( 45 | } 51 | /> 52 | )} 53 | 54 | {!isLessThan768 && project.username === userDetails.username ? ( 55 | } 66 | /> 67 | ) : null} 68 | 69 | 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /frontend/src/utils/addleaf.ts: -------------------------------------------------------------------------------- 1 | export interface FileNode { 2 | type: 'file'; 3 | id: string; 4 | name: string; 5 | parent: string; 6 | } 7 | 8 | export interface DirectoryNode { 9 | type: 'directory' | 'project'; 10 | id: string; 11 | name: string; 12 | parent: string; 13 | children?: (FileNode | DirectoryNode)[]; 14 | } 15 | 16 | type TreeNode = FileNode | DirectoryNode; 17 | 18 | export function addLeaf( 19 | tree: any, 20 | newFile: any, 21 | parentId: string, // ID of the directory where the new file should be added taken from the last value of fullPath or directly 22 | ): TreeNode { 23 | console.log('addleaf', tree, newFile, parentId); 24 | // Helper function to traverse and find the parent node 25 | function findDirectoryNode( 26 | node: TreeNode, 27 | targetId: string, 28 | ): DirectoryNode | null { 29 | console.log('findDirectoryNode method', node, targetId); 30 | if (node.type !== 'file' && node.id === targetId) { 31 | return node as DirectoryNode; 32 | } 33 | 34 | if (node.type !== 'file' && node.children) { 35 | for (const child of node.children) { 36 | const found = findDirectoryNode(child, targetId); 37 | if (found) return found; 38 | } 39 | } 40 | 41 | return null; 42 | } 43 | 44 | // Find the target directory to add the new file 45 | const targetDirectory = findDirectoryNode(tree, parentId); 46 | 47 | if (targetDirectory) { 48 | if (!targetDirectory.children) { 49 | targetDirectory.children = []; 50 | } 51 | targetDirectory.children.push(newFile); 52 | console.log('A new leaf has been added'); 53 | } else { 54 | console.error('Target directory not found.'); 55 | } 56 | 57 | return tree; 58 | } 59 | 60 | // Note: before adding an element to the tree, the new node object should be created and passed as an argument 61 | // addleaf is only used with a fresh brand new file that isn't found in the filetree used for traversing. 62 | 63 | export function createLeaf( 64 | filedir: string, 65 | id: string, 66 | name: string, 67 | parent: string, 68 | ) { 69 | if (filedir === 'file') { 70 | return { 71 | type: 'file', 72 | id, 73 | name, 74 | parent, 75 | } as FileNode; 76 | } else { 77 | return { 78 | type: 'directory', 79 | id, 80 | name, 81 | parent, 82 | children: [], 83 | } as DirectoryNode; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /frontend/src/store/slices/cookieConsentSlice.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file responsible for cookie consent slice 3 | */ 4 | 5 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 6 | 7 | interface CookieConsentState { 8 | showCookieConsent: boolean; 9 | consentOptions: { 10 | necessary: boolean; 11 | analytics: boolean; 12 | preferences: boolean; 13 | marketing: boolean; 14 | }; 15 | userChangedConsent: boolean; 16 | } 17 | 18 | const initialState: CookieConsentState = { 19 | showCookieConsent: false, 20 | consentOptions: { 21 | necessary: true, 22 | analytics: true, 23 | preferences: true, 24 | marketing: true, 25 | }, 26 | userChangedConsent: false, 27 | }; 28 | 29 | // Helper function to set consent mode in localStorage and push to dataLayer 30 | const setConsent = (consent) => { 31 | const consentMode = { 32 | functionality_storage: consent.necessary ? 'granted' : 'denied', 33 | analytics_storage: consent.analytics ? 'granted' : 'denied', 34 | preferences_storage: consent.preferences ? 'granted' : 'denied', 35 | marketing_storage: consent.marketing ? 'granted' : 'denied', 36 | }; 37 | localStorage.setItem('consentMode', JSON.stringify(consentMode)); 38 | window.dataLayer = window.dataLayer || []; 39 | window.dataLayer.push({ event: 'consentUpdate', consent: consentMode }); 40 | }; 41 | 42 | const cookieConsentSlice = createSlice({ 43 | name: 'cookieConsent', 44 | initialState, 45 | reducers: { 46 | showBanner(state) { 47 | state.showCookieConsent = true; 48 | }, 49 | hideBanner(state) { 50 | state.showCookieConsent = false; 51 | }, 52 | updateConsentOptions( 53 | state, 54 | action: PayloadAction>, 55 | ) { 56 | state.consentOptions = { 57 | ...state.consentOptions, 58 | ...action.payload, 59 | }; 60 | state.userChangedConsent = true; 61 | }, 62 | setConsentMode( 63 | state, 64 | action: PayloadAction>, 65 | ) { 66 | state.consentOptions = { 67 | ...state.consentOptions, 68 | ...action.payload, 69 | }; 70 | setConsent(state.consentOptions); 71 | }, 72 | }, 73 | }); 74 | 75 | export const { showBanner, hideBanner, updateConsentOptions, setConsentMode } = 76 | cookieConsentSlice.actions; 77 | export default cookieConsentSlice.reducer; 78 | -------------------------------------------------------------------------------- /backend/src/mail/templates/confirmation.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Email Confirmation 7 | 56 | 57 | 58 |
59 |

Email Verification Required

60 | 61 |

Hello {{ name }},

62 | 63 |

You’re one step away from joining the coolest code base! To confirm your email and complete your registration, just click the button below:

64 | 65 |

66 | Confirm My Email 67 |

68 | 69 |

If this wasn’t you, just ignore this email and keep coding the good code!

70 | 71 | 72 |
73 | 74 | 75 | -------------------------------------------------------------------------------- /frontend/src/utils/createfiledir.ts: -------------------------------------------------------------------------------- 1 | import * as Y from 'yjs'; 2 | import { DirectoryNode, FileNode } from './addleaf'; 3 | 4 | type Node = DirectoryNode | FileNode; 5 | type P = { 6 | parent: string; 7 | root: any; 8 | _id: string; 9 | filedir: string; 10 | newName: string; 11 | }; 12 | 13 | export function createDocuments({ 14 | parent, //parent to b used for the metadata 15 | root, // I'm creating files and directories as direct children of the root in the model structure 16 | _id, // id of the new file as key 17 | filedir, // string to differentiate files from dirs 18 | newName, // used in the metadata 19 | }: P) { 20 | if (root instanceof Y.Map) { 21 | let newfile = root.get(_id); 22 | if (!newfile) { 23 | console.log( 24 | 'The file you just clicked was created freshly and is not in the yjs object', 25 | ); 26 | newfile = filedir === 'file' ? new Y.Text() : new Y.Map(); 27 | console.log('id please have a value ============>', _id); 28 | const metadata = { 29 | id: _id, 30 | type: filedir, 31 | new: true, 32 | name: newName, 33 | parent_id: parent, 34 | }; 35 | root.set(`${_id}_metadata`, metadata); // Type Error 36 | root.set(_id, newfile); 37 | } else { 38 | console.log('file is already found in the ymap'); 39 | } 40 | return newfile; 41 | } 42 | } 43 | 44 | export function findNode(node: Node, id: string): Node | null { 45 | // Check if current node is the target node 46 | if (node.id === id) { 47 | return node; 48 | } 49 | 50 | // If it's a directory, recursively search its children 51 | if (node.type !== 'file' && node.children) { 52 | for (const child of node.children) { 53 | const childFound = findNode(child, id); 54 | if (childFound) { 55 | return childFound; 56 | } 57 | } 58 | } 59 | 60 | return null; 61 | } 62 | 63 | export function deleteNode(root: Node, id: string): Node | null { 64 | console.log('root', root.id, 'myid', id); 65 | if (root.id === id) { 66 | return null; 67 | } 68 | 69 | // If it's a directory, recursively search its children 70 | if (root.type !== 'file' && root.children) { 71 | root.children = root.children 72 | .map((child) => deleteNode(child, id)) 73 | .filter((child) => child !== null) as Node[]; // This filters the children list 74 | } 75 | return root; // Return the root node if it's not deleted 76 | } 77 | -------------------------------------------------------------------------------- /backend/src/auth/auth.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthController } from './auth.controller'; 3 | import { AuthService } from './auth.service'; 4 | import { JwtService } from '@nestjs/jwt'; 5 | import { UsersService } from '@users/users.service'; 6 | import { getRepositoryToken } from '@nestjs/typeorm'; 7 | import { Users } from '@users/user.entity'; 8 | import { Repository } from 'typeorm'; 9 | import { EnvironmentMongoModule } from '@environment-mongo/environment-mongo.module'; 10 | import { MailModule } from '@mail/mail.module'; 11 | import { RedisModule } from '@redis/redis.module'; 12 | import { PassportModule } from '@nestjs/passport'; 13 | import { JwtModule } from '@nestjs/jwt'; 14 | import { jwtConstants } from '@constants'; 15 | import { BullModule } from '@nestjs/bullmq'; 16 | 17 | describe('AuthController', () => { 18 | let authController: AuthController; 19 | let authService: AuthService; 20 | 21 | beforeEach(async () => { 22 | const module: TestingModule = await Test.createTestingModule({ 23 | controllers: [AuthController], 24 | providers: [ 25 | AuthService, 26 | { 27 | provide: JwtService, 28 | useValue: { 29 | sign: jest.fn(), 30 | verify: jest.fn(), 31 | }, 32 | }, 33 | { 34 | provide: UsersService, 35 | useValue: { 36 | findOne: jest.fn(), 37 | createUser: jest.fn(), 38 | // Add any other methods you use in AuthService 39 | }, 40 | }, 41 | { 42 | provide: getRepositoryToken(Users), 43 | useClass: Repository, // Mock Users repository if needed 44 | }, 45 | ], 46 | imports: [ 47 | PassportModule, 48 | JwtModule.register({ 49 | secret: jwtConstants.secret, 50 | signOptions: { expiresIn: '60s' }, 51 | }), 52 | EnvironmentMongoModule, 53 | MailModule, 54 | RedisModule, 55 | BullModule.forRoot({ 56 | connection: { 57 | host: 'localhost', 58 | port: 6379, 59 | }, 60 | }), 61 | ], 62 | }).compile(); 63 | 64 | authController = module.get(AuthController); 65 | authService = module.get(AuthService); 66 | }); 67 | 68 | it('should be defined', () => { 69 | expect(authController).toBeDefined(); 70 | }); 71 | }); 72 | 73 | -------------------------------------------------------------------------------- /backend/src/filters/http-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExceptionFilter, 3 | Catch, 4 | ArgumentsHost, 5 | HttpException, 6 | HttpStatus, 7 | NotFoundException, 8 | UnauthorizedException, 9 | ForbiddenException, 10 | BadRequestException, 11 | } from '@nestjs/common'; 12 | import { Request, Response } from 'express'; 13 | // TODO: Implement custom exception filters 14 | /* Different exceptions can be caught and handled differently 15 | * HttpException: Standard HTTP errors. 16 | * ValidationException: For validation errors. 17 | * DatabaseException: For database-related errors. 18 | * UnauthorizedException: For unauthorized access. 19 | * ForbiddenException: For forbidden access. 20 | * NotFoundException: When a resource is not found. 21 | * InternalServerErrorException: For general server errors. 22 | * CustomException: Any application-specific exceptions. 23 | */ 24 | 25 | @Catch(HttpException) 26 | export class HttpExceptionFilter implements ExceptionFilter { 27 | catch(exception: HttpException, host: ArgumentsHost) { 28 | const ctx = host.switchToHttp(); 29 | const request = ctx.getRequest(); 30 | const response = ctx.getResponse(); 31 | const status = exception.getStatus 32 | ? exception.getStatus() 33 | : HttpStatus.INTERNAL_SERVER_ERROR; 34 | const exceptionResponse = exception.getResponse(); 35 | 36 | let message: string; 37 | 38 | if (exception instanceof UnauthorizedException) { 39 | message = `Unauthorized access. ${exception.message}`; 40 | } else if (exception instanceof ForbiddenException) { 41 | message = `Forbidden access. ${exception.message}`; 42 | } else if (exception instanceof NotFoundException) { 43 | message = `Resource not found. ${exception.message}`; 44 | } else if (exception instanceof BadRequestException) { 45 | message = `Bad request. ${exception.message}`; 46 | } else if (typeof exceptionResponse === 'object' && exceptionResponse !== null) { 47 | message = (exceptionResponse as any).message || 'An error occurred'; 48 | } else { 49 | message = typeof exceptionResponse === 'string' 50 | ? exceptionResponse 51 | : 'An error occurred'; 52 | } 53 | 54 | const responseBody = { 55 | statusCode: status, 56 | message: message, 57 | timestamp: new Date().toISOString(), 58 | path: request.url, 59 | }; 60 | 61 | response.status(status).json(responseBody); 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /frontend/src/components/Buttons/CallToAction.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useGetGuestIPQuery, 3 | useLazyGetGuestProjectQuery, 4 | useLoginGuestMutation, 5 | } from '@store/services/auth'; 6 | import { Button } from '@chakra-ui/react'; 7 | import { useNavigate } from 'react-router-dom'; 8 | import { ChevronRightIcon } from '@chakra-ui/icons'; 9 | import { useCreateProjectShareMutation } from '@store/services/projectShare'; 10 | 11 | type Props = { 12 | className?: string; 13 | _redirect?: string; // Redirect to a specific project when is invited as guest by link share 14 | access_level?: 'write' | 'read'; 15 | [k: string]: any; 16 | }; 17 | export default function CallToAction({ 18 | _redirect = '', 19 | access_level = 'write', 20 | className = '', 21 | ...rest 22 | }: Props) { 23 | const { data } = useGetGuestIPQuery(); 24 | const [getProject, { isLoading: projectLoading }] = 25 | useLazyGetGuestProjectQuery(); 26 | const [createProjectShare, { isLoading: PSLoading }] = 27 | useCreateProjectShareMutation(); 28 | const [loginGuest, { isLoading: loginLoading }] = useLoginGuestMutation(); 29 | const navigate = useNavigate(); 30 | 31 | const handleTryItOut = async () => { 32 | console.log('Trying it out'); 33 | 34 | try { 35 | await loginGuest().unwrap(); 36 | 37 | const { redirect } = await getProject({ IP: data?.ip }).unwrap(); 38 | localStorage.setItem('project_id', redirect); 39 | 40 | if (_redirect) { 41 | await createProjectShare({ 42 | project_id: _redirect, 43 | access_level, 44 | username: 'guest', 45 | }).unwrap(); 46 | 47 | navigate(`/editor/${_redirect}`); 48 | } else { 49 | navigate(`/editor/${redirect}`); 50 | } 51 | } catch (error) { 52 | console.error('Error during Try It Out:', error); 53 | } 54 | }; 55 | 56 | return ( 57 | 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import { BrowserRouter } from 'react-router-dom'; 3 | import { ChakraProvider, extendTheme } from '@chakra-ui/react'; 4 | import { Provider } from 'react-redux'; 5 | import store from '@store/store.ts'; 6 | import { menuTheme } from './theme/MenuTheme.tsx'; 7 | import App from './App.tsx'; 8 | import './index.css'; 9 | 10 | // Define custom colors for the theme 11 | const colors = { 12 | brand: { 13 | 900: '#001845', 14 | 800: '#524175', 15 | 700: '#333333', 16 | 600: '#E6E85C', 17 | 500: '#F3F3F3', 18 | 400: '#FFD700', 19 | 300: '#FF4C4C', 20 | 200: '#6BE3E1', 21 | 150: '#D1D1D1', 22 | 100: '#524175', 23 | }, 24 | }; 25 | 26 | // Custom styles for alerts (used by toasts) 27 | const alertStyle = { 28 | baseStyle: { 29 | container: { 30 | fontFamily: 'mono', 31 | borderRadius: 'md', 32 | boxShadow: 'md', 33 | padding: ['0.5rem', '1rem'], 34 | border: '1px solid', 35 | maxWidth: ['90%', '80%'], 36 | mx: 'auto', 37 | }, 38 | }, 39 | variants: { 40 | subtle: (props: any) => ({ 41 | container: { 42 | bg: 'brand.150', 43 | color: 'brand.900', 44 | borderColor: 'brand.200', 45 | }, 46 | }), 47 | success: { 48 | container: { 49 | bg: 'brand.200', 50 | color: 'brand.700', 51 | borderColor: 'brand.600', 52 | }, 53 | }, 54 | error: { 55 | container: { 56 | bg: 'brand.300', 57 | color: 'brand.150', 58 | borderColor: 'brand.400', 59 | }, 60 | }, 61 | warning: { 62 | container: { 63 | bg: 'brand.400', 64 | color: 'brand.700', 65 | borderColor: 'brand.300', 66 | }, 67 | }, 68 | info: { 69 | container: { 70 | bg: 'brand.600', 71 | color: 'brand.900', 72 | borderColor: 'brand.800', 73 | }, 74 | }, 75 | }, 76 | defaultProps: { 77 | variant: 'subtle', 78 | }, 79 | }; 80 | 81 | // Extend the default Chakra UI theme with custom colors 82 | const theme = extendTheme({ 83 | colors, 84 | components: { 85 | Menu: menuTheme, 86 | Alert: alertStyle, 87 | }, 88 | }); 89 | 90 | createRoot(document.getElementById('root')!).render( 91 | 92 | 93 | 94 | 95 | 96 | 97 | , 98 | ); 99 | -------------------------------------------------------------------------------- /backend/src/directory-mongo/dto/create-directory-mongo.dto.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException } from '@nestjs/common'; 2 | // TODO: create dto for all the entities 3 | 4 | interface CreateDirectoryOutDto { 5 | name: string; 6 | parent_id: string; 7 | project_id: string; 8 | } 9 | 10 | interface UpdateDirectoryOutDto { 11 | name?: string; 12 | parent_id?: string; 13 | updated_at?: Date; 14 | } 15 | 16 | function validateCreateDirectoryDto(dto: any): CreateDirectoryOutDto { 17 | if (!dto || typeof dto !== 'object') { 18 | throw new BadRequestException('Invalid input'); 19 | } 20 | 21 | const { name, parent_id, project_id } = dto; 22 | 23 | if (typeof parent_id !== 'string' || parent_id.trim() === '') { 24 | throw new BadRequestException('parent ID is required and must be a string'); 25 | } 26 | 27 | if (typeof name !== 'string' || name.trim() === '') { 28 | throw new BadRequestException( 29 | 'Directory name is required and must be a string', 30 | ); 31 | } 32 | 33 | if (typeof project_id !== 'string' || project_id.trim() === '') { 34 | throw new BadRequestException( 35 | 'Project ID is required and must be a string', 36 | ); 37 | } 38 | 39 | return { 40 | name: name.trim(), 41 | parent_id: parent_id.trim(), 42 | project_id: project_id.trim(), 43 | }; 44 | } 45 | 46 | function validateUpdateDirectoryDto(dto: any): UpdateDirectoryOutDto { 47 | if (!dto || typeof dto !== 'object') { 48 | throw new BadRequestException('Invalid input'); 49 | } 50 | 51 | const { name, parent_id, updated_at } = dto; 52 | 53 | if (name && typeof name !== 'string') { 54 | throw new BadRequestException('Directory name must be a string'); 55 | } 56 | 57 | if (parent_id && typeof parent_id !== 'string') { 58 | throw new BadRequestException('Parent ID must be a string'); 59 | } 60 | 61 | return { 62 | name: name?.trim(), 63 | parent_id: parent_id?.trim(), 64 | updated_at: updated_at?.toString()?.trim(), 65 | }; 66 | } 67 | 68 | function parseCreateDirectoryMongoDto(requestBody: any): CreateDirectoryOutDto { 69 | const validated = validateCreateDirectoryDto(requestBody); 70 | 71 | return validated; 72 | } 73 | 74 | function parseUpdateDirectoryMongoDto(requestBody: any): UpdateDirectoryOutDto { 75 | const validated = validateUpdateDirectoryDto(requestBody); 76 | 77 | return validated; 78 | } 79 | 80 | export { 81 | parseCreateDirectoryMongoDto, 82 | CreateDirectoryOutDto, 83 | parseUpdateDirectoryMongoDto, 84 | UpdateDirectoryOutDto, 85 | }; 86 | -------------------------------------------------------------------------------- /frontend/src/store/services/user.ts: -------------------------------------------------------------------------------- 1 | import { api } from './api'; 2 | import { CreateUserDto, User, Project, ProjectSharesOutDto } from '@types'; 3 | interface UserFavorite { 4 | user: Partial; 5 | favorite_projects: Project[]; 6 | favorite_shares: ProjectSharesOutDto[]; 7 | } 8 | 9 | export const userApi = api.injectEndpoints({ 10 | endpoints: (builder) => ({ 11 | // Get current user profile 12 | getCurrentUserProfile: builder.query({ 13 | query: () => `/users/me`, 14 | providesTags: ['User'], 15 | }), 16 | // Update current user profile 17 | updateCurrentUserProfile: builder.mutation>({ 18 | query: (data) => ({ 19 | url: `/users/me`, 20 | method: 'PATCH', 21 | body: data, 22 | }), 23 | invalidatesTags: ['User'], 24 | }), 25 | // Delete current user profile 26 | deleteCurrentUserProfile: builder.mutation({ 27 | query: () => ({ 28 | url: `/users/me`, 29 | method: 'DELETE', 30 | }), 31 | invalidatesTags: ['User'], 32 | }), 33 | // Get user by ID 34 | getUserById: builder.query({ 35 | query: (id) => `/users/${id}`, 36 | providesTags: ['User'], 37 | }), 38 | getFriendById: builder.query< 39 | { username: string; profile_picture: string }, 40 | string 41 | >({ 42 | query: (id) => `/users/friend/${id}`, 43 | }), 44 | // Update user by ID 45 | updateUserById: builder.mutation< 46 | User, 47 | { id: string; data: Partial } 48 | >({ 49 | query: ({ id, data }) => ({ 50 | url: `/users/${id}`, 51 | method: 'PATCH', 52 | body: data, 53 | }), 54 | invalidatesTags: ['User'], 55 | }), 56 | // Delete user by ID 57 | deleteUserById: builder.mutation({ 58 | query: (id) => ({ 59 | url: `/users/${id}`, 60 | method: 'DELETE', 61 | }), 62 | invalidatesTags: ['User'], 63 | }), 64 | getUserByFavorites: builder.query({ 65 | query: () => `/users/me/favorites`, 66 | providesTags: ['User'], 67 | }), 68 | }), 69 | overrideExisting: false, 70 | }); 71 | 72 | export const { 73 | useGetCurrentUserProfileQuery, 74 | useUpdateCurrentUserProfileMutation, 75 | useDeleteCurrentUserProfileMutation, 76 | useGetUserByIdQuery, 77 | useUpdateUserByIdMutation, 78 | useDeleteUserByIdMutation, 79 | useGetUserByFavoritesQuery, 80 | useLazyGetFriendByIdQuery, 81 | } = userApi; 82 | -------------------------------------------------------------------------------- /backend/src/projects/projects.docs.ts: -------------------------------------------------------------------------------- 1 | import { applyDecorators } from '@nestjs/common'; 2 | import { ApiBody, ApiOkResponse, ApiOperation } from '@nestjs/swagger'; 3 | import { CreateProjectDto, UpdateProjectDto } from './dto/create-project.dto'; 4 | 5 | class ProjectDocs { 6 | static init({ summary, description }, ...args) { 7 | return applyDecorators( 8 | ApiOperation({ 9 | summary, 10 | description, 11 | }), 12 | ...args, 13 | ); 14 | } 15 | 16 | create() { 17 | return ProjectDocs.init( 18 | { 19 | summary: 'Create a new project', 20 | description: 'Create a new project using the provided data.', 21 | }, 22 | ApiBody({ type: CreateProjectDto }), 23 | ); 24 | } 25 | 26 | findAllByOwnerId() { 27 | return ProjectDocs.init({ 28 | summary: 'Get all projects of the logged in user', 29 | description: 'Retrieve a list of all projects associated with the logged in user using Id.', 30 | }); 31 | } 32 | 33 | findAllByUsernameDepth() { 34 | return ProjectDocs.init({ 35 | summary: 'Retrieve projects by username with depth', 36 | description: 37 | 'Retrieve projects associated with a specific username and a given depth level. This operation fetches projects under a specified directory up to a certain depth in the directory hierarchy.', 38 | }); 39 | } 40 | 41 | findAllByUsernamePaginated() { 42 | return ProjectDocs.init({ 43 | summary: 'Get all projects of the logged in user', 44 | description: 'Retrieve a list of all projects associated with the logged in user using username And is paginated.', 45 | }); 46 | } 47 | 48 | findAllForUser() { 49 | return ProjectDocs.init({ 50 | summary: 'Get all projects by username', 51 | description: 'Retrieve all projects associated with a specific username.', 52 | }); 53 | } 54 | 55 | findOne() { 56 | return ProjectDocs.init({ 57 | summary: 'Get project by ID', 58 | description: 'Retrieve project for a user based on project ID', 59 | }); 60 | } 61 | 62 | update() { 63 | return ProjectDocs.init( 64 | { 65 | summary: 'Update a project by ID', 66 | description: 'Update the details of an existing project using its ID.', 67 | }, 68 | ApiOkResponse({ type: UpdateProjectDto }), 69 | ); 70 | } 71 | 72 | remove() { 73 | return ProjectDocs.init({ 74 | summary: 'Delete a project by ID', 75 | description: 'Remove a project from the system using its ID.', 76 | }); 77 | } 78 | } 79 | 80 | export default new ProjectDocs(); 81 | -------------------------------------------------------------------------------- /frontend/src/store/services/api.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseQueryFn, 3 | createApi, 4 | FetchArgs, 5 | fetchBaseQuery, 6 | FetchBaseQueryError, 7 | } from '@reduxjs/toolkit/query/react'; 8 | import { RootState } from '@store/store'; 9 | import apiConfig from '@config/apiConfig'; 10 | import { setCredentials, unsetCredentials } from '@store/slices/authSlice'; 11 | 12 | interface RefreshTokenResponse { 13 | accessToken: string; 14 | } 15 | 16 | /** 17 | * Base query function with default fetch configuration. 18 | */ 19 | const baseQuery = fetchBaseQuery({ 20 | baseUrl: apiConfig.baseUrl, 21 | prepareHeaders: (headers, { getState }) => { 22 | const state = getState() as RootState; 23 | const token = localStorage.getItem('accessToken') || state.auth.accessToken; 24 | if (token) { 25 | headers.set('Authorization', `Bearer ${token}`); 26 | } 27 | return headers; 28 | }, 29 | }); 30 | 31 | /** 32 | * Base query function with token refresh logic. 33 | * 34 | * This function handles automatic token refreshing when a 401 Unauthorized 35 | * response is encountered. It attempts to refresh the token and retry 36 | * the original request. 37 | */ 38 | const baseQueryWithReauth: BaseQueryFn< 39 | string | FetchArgs, 40 | unknown, 41 | FetchBaseQueryError 42 | > = async (args, api, extraOptions) => { 43 | let result = await baseQuery(args, api, extraOptions); 44 | console.log('This function gets executed'); 45 | console.log('result', result); 46 | 47 | if (result.error && result.error?.status === 401) { 48 | // if (localStorage.getItem('accessToken')) { 49 | // api.dispatch( 50 | // setCredentials({ 51 | // accessToken: localStorage.getItem('accessToken'), 52 | // }), 53 | // ); 54 | // return result; 55 | // } 56 | const refreshResult = await baseQuery( 57 | { 58 | url: 'auth/refresh', 59 | method: 'POST', 60 | credentials: 'include', 61 | }, 62 | api, 63 | extraOptions, 64 | ); 65 | 66 | if (refreshResult.data) { 67 | const { accessToken } = refreshResult.data as RefreshTokenResponse; 68 | api.dispatch(setCredentials({ accessToken })); 69 | result = await baseQuery(args, api, extraOptions); 70 | } else { 71 | api.dispatch(unsetCredentials()); 72 | } 73 | } 74 | return result; 75 | }; 76 | 77 | /** 78 | * API service configuration Entry point. 79 | */ 80 | export const api = createApi({ 81 | baseQuery: baseQueryWithReauth, 82 | tagTypes: ['User', 'Profile', 'Environment', 'Project', 'ProjectShare'], 83 | endpoints: () => ({}), 84 | }); 85 | -------------------------------------------------------------------------------- /backend/src/mail/templates/reset-password.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Password Reset 7 | 56 | 57 | 58 |
59 |

Password Reset Request

60 | 61 |

Hey {{ name }},

62 | 63 |

Forgot your password? It happens to the best of us, even the most seasoned developers!

64 | 65 |

To get back to coding in no time, click the button below to reset your password:

66 | 67 |

68 | Reset Your Password 69 |

70 | 71 |

Keep your passwords strong and your coffee stronger!

72 | 73 | 74 | 75 | 76 |
77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /backend/src/users/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | PrimaryGeneratedColumn, 4 | Index, 5 | Column, 6 | CreateDateColumn, 7 | UpdateDateColumn, 8 | OneToMany, 9 | ManyToMany, 10 | JoinTable, 11 | } from 'typeorm'; 12 | import { Projects } from '@projects/project.entity'; 13 | import { ProjectShares } from '@project-shares/project-shares.entity'; 14 | // import { EnvironmentMongo } from '@environment-mongo/environment-mongo.entity'; 15 | import { Role } from '@auth/enums/role.enum'; 16 | 17 | @Entity({ name: 'Users' }) 18 | @Index('idx_username', ['username'], { unique: true }) 19 | export class Users { 20 | @PrimaryGeneratedColumn('uuid') 21 | user_id: string; // UUID autogenerated 22 | 23 | @Column({ type: 'varchar', length: 100, unique: true }) 24 | username: string; 25 | 26 | @Column({ type: 'varchar', length: 100 }) 27 | first_name: string; 28 | 29 | @Column({ type: 'varchar', length: 100 }) 30 | last_name: string; 31 | 32 | @Column({ type: 'boolean', default: false }) 33 | is_verified: boolean; 34 | 35 | // favorite languages list 36 | @Column('simple-array', { nullable: true }) 37 | favorite_languages: string[] | null; 38 | 39 | // TODO: add profile picture and other user details as needed 40 | @Column({ type: 'varchar', nullable: true, default: '' }) 41 | profile_picture: string | null; 42 | 43 | // bio 44 | @Column({ type: 'varchar', nullable: true, default: '' }) 45 | bio: string | null; 46 | 47 | // default 'user' 48 | @Column({ type: 'enum', enum: Role, default: Role.User }) 49 | roles: Role[]; // User roles 50 | 51 | @Column({ type: 'varchar', length: 100, unique: true }) 52 | email: string; 53 | 54 | @Column({ type: 'varchar', length: 255 }) 55 | password_hash: string; 56 | 57 | @Column({ type: 'varchar', nullable: true }) 58 | environment_id: string | null; 59 | 60 | @CreateDateColumn() 61 | created_at: Date; 62 | 63 | @UpdateDateColumn() 64 | updated_at: Date; 65 | 66 | @OneToMany(() => Projects, (project) => project.owner) 67 | ownedProjects: Projects[]; // Projects owned by the user 68 | 69 | @OneToMany(() => ProjectShares, (projectShare) => projectShare.user) 70 | projectShares: ProjectShares[]; // Project shares associated with the user 71 | 72 | // TODO: create favorite projects list 73 | 74 | @ManyToMany(() => Projects, (project) => project.favorite, { 75 | cascade: true, 76 | }) 77 | @JoinTable() 78 | favorite_projects: Projects[]; 79 | @ManyToMany(() => ProjectShares, (projectShare) => projectShare.favorite, { 80 | cascade: true, 81 | }) 82 | @JoinTable() 83 | favorite_shares: ProjectShares[]; 84 | } 85 | -------------------------------------------------------------------------------- /frontend/src/utils/codeExamples.ts: -------------------------------------------------------------------------------- 1 | export type LanguageCode = 2 | | 'javascript' 3 | | 'python' 4 | | 'c' 5 | | 'typescript' 6 | | 'markdown' 7 | | 'html' 8 | | 'unknown'; 9 | 10 | export const codeExamples: Record = { 11 | javascript: `// JavaScript Example: Function to Calculate Factorial 12 | function factorial(n) { 13 | if (n === 0 || n === 1) return 1; 14 | return n * factorial(n - 1); 15 | } 16 | 17 | // Test the function 18 | console.log('Factorial of 5:', factorial(5)); // Output: Factorial of 5: 120 19 | `, 20 | 21 | python: `# Python Example: Function to Calculate Factorial 22 | def factorial(n): 23 | """Return the factorial of n.""" 24 | if n == 0 or n == 1: 25 | return 1 26 | return n * factorial(n - 1) 27 | 28 | # Test the function 29 | print('Factorial of 5:', factorial(5)) # Output: Factorial of 5: 120 30 | `, 31 | 32 | c: `/* C Example: Function to Calculate Factorial */ 33 | #include 34 | 35 | // Function to calculate factorial 36 | int factorial(int n) { 37 | if (n == 0 || n == 1) return 1; 38 | return n * factorial(n - 1); 39 | } 40 | 41 | int main() { 42 | // Test the function 43 | printf("Factorial of 5: %d\\n", factorial(5)); // Output: Factorial of 5: 120 44 | return 0; 45 | } 46 | `, 47 | 48 | typescript: `// TypeScript Example: Function to Calculate Factorial 49 | function factorial(n: number): number { 50 | if (n === 0 || n === 1) return 1; 51 | return n * factorial(n - 1); 52 | } 53 | 54 | // Test the function 55 | console.log('Factorial of 5:', factorial(5)); // Output: Factorial of 5: 120 56 | `, 57 | 58 | markdown: `# Markdown Example 59 | 60 | ## Introduction 61 | 62 | This is an example of Markdown syntax. Markdown is a lightweight markup language with plain text formatting syntax. 63 | 64 | ### Features 65 | 66 | - **Bold Text**: \`**bold**\` 67 | - *Italic Text*: \`*italic*\` 68 | - [Link](https://www.example.com) 69 | 70 | ### Code Example 71 | 72 | \`\`\`javascript 73 | console.log('Hello World'); 74 | \`\`\` 75 | `, 76 | 77 | html: ` 78 | 79 | 80 | 81 | 82 | 83 | Hello World 84 | 85 | 86 |
87 |

Hello World

88 |
89 |
90 |

This is a simple HTML example.

91 |
92 |
93 |

© 2024 Example Corp.

94 |
95 | 96 | 97 | `, 98 | unknown: `// Unknown Language Example`, 99 | }; 100 | -------------------------------------------------------------------------------- /frontend/src/components/Dashboard/DBMenu.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Menu, 3 | MenuButton, 4 | MenuList, 5 | MenuItem, 6 | MenuGroup, 7 | MenuDivider, 8 | Avatar, 9 | Text, 10 | useToast, 11 | } from '@chakra-ui/react'; 12 | import { FiUser, FiLogOut } from 'react-icons/fi'; 13 | import { ReactNode } from 'react'; 14 | import { selectUserDetails } from '@store/selectors/userSelectors'; 15 | import { performLogout } from '@store/slices/authSlice'; 16 | import { useNavigate } from 'react-router-dom'; 17 | import { useAppDispatch, useAppSelector } from '@hooks/useApp'; 18 | 19 | type DBMenuProps = { 20 | children: ReactNode; 21 | isGuest?: boolean; 22 | className?: string; 23 | }; 24 | 25 | export default function DBMenu({ 26 | isGuest = false, 27 | className = '', 28 | children, 29 | }: DBMenuProps) { 30 | const navigate = useNavigate(); 31 | const toast = useToast(); 32 | const userDetails: any = useAppSelector(selectUserDetails) || 'username'; 33 | const dispatch = useAppDispatch(); 34 | 35 | const handleLogout = () => { 36 | navigate(location.pathname.concat('?logout=true')); 37 | dispatch(performLogout()); 38 | toast({ 39 | title: 'See you later, coder! 👋', 40 | description: 'You’re now logged out. Until next time!', 41 | variant: 'subtle', 42 | position: 'bottom-left', 43 | status: 'success', 44 | isClosable: true, 45 | }); 46 | }; 47 | 48 | const handleProfile = () => { 49 | if (isGuest) return; 50 | navigate('/profile'); 51 | }; 52 | 53 | return ( 54 | 55 | {children} 56 | 57 | 58 | 59 | 65 | {`@${userDetails?.username}`} 66 | 67 | 68 | 69 | 70 | {!isGuest && ( 71 | } 75 | > 76 | Profile 77 | 78 | )} 79 | } 82 | _hover={{ bg: 'red.500', color: 'white' }} 83 | onClick={handleLogout} 84 | > 85 | Log Out 86 | 87 | 88 | 89 | 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /frontend/src/context/EditorContext.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-refresh/only-export-components */ 2 | /* eslint-disable no-unused-vars */ 3 | import { 4 | createContext, 5 | useState, 6 | ReactNode, 7 | useContext, 8 | useEffect, 9 | } from 'react'; 10 | import * as Y from 'yjs'; 11 | 12 | export type YMapValueType = Y.Text | null | Y.Map; 13 | type Theme = string; 14 | type Language = string; 15 | type Awareness = [ 16 | number, 17 | { 18 | [x: string]: any; // hopeless type error 19 | }, 20 | ][]; 21 | export type FileType = { 22 | name: string; 23 | value: YMapValueType; 24 | id: string; 25 | language: Language; 26 | }; 27 | 28 | interface SettingsContextType { 29 | theme: Theme; 30 | setTheme: (theme: Theme) => void; 31 | language: Language; 32 | setLanguage: (language: Language) => void; 33 | mode: boolean; 34 | setMode: (mode: boolean) => void; 35 | } 36 | 37 | interface FileContextType { 38 | fileSelected: FileType | null; 39 | setFileSelected: (file: FileType) => void; 40 | awareness: Awareness; 41 | setAwareness: (awareness: Awareness) => void; 42 | fileTree: Y.Map | null; 43 | setFileTree: (fileTree: Y.Map | null) => void; 44 | } 45 | 46 | // Initialize contexts with null as default 47 | const SettingsContext = createContext(null); 48 | const FileContext = createContext(null); 49 | 50 | interface EditorProviderProps { 51 | children: ReactNode; 52 | } 53 | 54 | export function useSettings() { 55 | return useContext(SettingsContext); 56 | } 57 | 58 | export function useFile() { 59 | return useContext(FileContext); 60 | } 61 | 62 | export function EditorProvider({ children }: EditorProviderProps) { 63 | const [theme, setTheme] = useState('dracula'); 64 | const [language, setLanguage] = useState('typescript'); 65 | const [fileSelected, setFileSelected] = useState(null); 66 | const [awareness, setAwareness] = useState([]); 67 | const [fileTree, setFileTree] = useState | null>(null); 68 | const [mode, setMode] = useState(true); 69 | 70 | useEffect(() => { 71 | if (fileSelected) { 72 | setLanguage(fileSelected.language); 73 | } 74 | }, [fileSelected]); 75 | 76 | return ( 77 | 80 | 90 | {children} 91 | 92 | 93 | ); 94 | } 95 | --------------------------------------------------------------------------------