├── pnpm-workspace.yaml ├── apps ├── back │ ├── src │ │ ├── config │ │ │ ├── index.ts │ │ │ └── env.config.ts │ │ ├── app │ │ │ ├── auth │ │ │ │ ├── guards │ │ │ │ │ ├── index.ts │ │ │ │ │ └── roles.guard.ts │ │ │ │ ├── strategies │ │ │ │ │ ├── index.ts │ │ │ │ │ └── jwt.strategy.ts │ │ │ │ ├── models │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── jwt.model.ts │ │ │ │ │ └── roles.model.ts │ │ │ │ ├── decorators │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── set-roles.decorator.ts │ │ │ │ │ └── auth.decorator.ts │ │ │ │ ├── dto │ │ │ │ │ ├── check-email.dto.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── check-username.dto.ts │ │ │ │ │ ├── sign-in.dto.ts │ │ │ │ │ └── sign-up.dto.ts │ │ │ │ ├── auth.module.ts │ │ │ │ ├── auth.controller.ts │ │ │ │ └── auth.service.ts │ │ │ ├── common │ │ │ │ ├── exceptions │ │ │ │ │ ├── index.ts │ │ │ │ │ └── bad-credentials.exception.ts │ │ │ │ └── util │ │ │ │ │ ├── index.ts │ │ │ │ │ └── postgres-error-handler.util.ts │ │ │ ├── email-verification │ │ │ │ ├── entities │ │ │ │ │ ├── index.ts │ │ │ │ │ └── unverified-email.entity.ts │ │ │ │ ├── email-verification.controller.ts │ │ │ │ ├── email-verification.module.ts │ │ │ │ └── email-verification.service.ts │ │ │ ├── user │ │ │ │ ├── user.controller.ts │ │ │ │ ├── user.module.ts │ │ │ │ ├── user.service.ts │ │ │ │ └── entities │ │ │ │ │ └── user.entity.ts │ │ │ └── app.module.ts │ │ └── main.ts │ ├── .prettierrc │ ├── tsconfig.build.json │ ├── nest-cli.json │ ├── test │ │ ├── jest-e2e.json │ │ └── app.e2e-spec.ts │ ├── turbo.json │ ├── docker-compose.yaml │ ├── .vscode │ │ └── settings.json │ ├── tsconfig.json │ ├── .eslintrc.js │ ├── .env.template │ ├── package.json │ └── README.md └── web-app │ ├── src │ ├── models │ │ ├── dtos │ │ │ ├── index.ts │ │ │ └── user.dto.ts │ │ ├── validations │ │ │ ├── index.ts │ │ │ └── user.validation.ts │ │ ├── environment.model.ts │ │ ├── index.ts │ │ ├── component.model.ts │ │ ├── timer.model.ts │ │ ├── api.model.ts │ │ ├── user.model.ts │ │ └── window.model.ts │ ├── vite-env.d.ts │ ├── pages │ │ ├── Home │ │ │ ├── layouts │ │ │ │ ├── index.ts │ │ │ │ └── MainLayout.tsx │ │ │ ├── components │ │ │ │ └── feature │ │ │ │ │ ├── RecordingWindow │ │ │ │ │ ├── models │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── recorder.model.ts │ │ │ │ │ ├── hooks │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── useRecorderWindow.ts │ │ │ │ │ │ └── useRecorder.ts │ │ │ │ │ ├── services │ │ │ │ │ │ └── context │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── recorderWindow.provider.tsx │ │ │ │ │ │ │ ├── recorder.provider.tsx │ │ │ │ │ │ │ ├── recorderWindow.context.ts │ │ │ │ │ │ │ └── recorder.context.ts │ │ │ │ │ ├── components │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── RecordVideo.tsx │ │ │ │ │ │ ├── RecordingWindowDropdownMenu.tsx │ │ │ │ │ │ ├── RecordingWindowWrap.tsx │ │ │ │ │ │ ├── RecordControls.tsx │ │ │ │ │ │ └── RecordData.tsx │ │ │ │ │ └── RecordingWindow.tsx │ │ │ │ │ ├── WatchRecordingWindow │ │ │ │ │ ├── hooks │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── useWatchRecording.ts │ │ │ │ │ └── WatchRecordingWindow.tsx │ │ │ │ │ ├── CompatibilityWindow │ │ │ │ │ ├── hooks │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── useCompatibilityWindow.ts │ │ │ │ │ ├── models │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── default-values.model.ts │ │ │ │ │ └── CompatibilityWindow.tsx │ │ │ │ │ ├── DownloadRecordingWindow │ │ │ │ │ ├── hooks │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── useDownloadRecording.ts │ │ │ │ │ └── DownloadRecordingWindow.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── AddWindowsDropdownMenu.tsx │ │ │ └── Home.tsx │ │ ├── SignIn │ │ │ ├── components │ │ │ │ ├── index.ts │ │ │ │ └── SignInForm.tsx │ │ │ ├── models │ │ │ │ ├── index.ts │ │ │ │ └── signIn.schema.ts │ │ │ └── SignIn.tsx │ │ ├── SignUp │ │ │ ├── models │ │ │ │ ├── index.ts │ │ │ │ └── signUp.schema.ts │ │ │ ├── components │ │ │ │ ├── FormTextFieldValidateUserData │ │ │ │ │ ├── hooks │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── useFormTextFieldValidateUserData.ts │ │ │ │ │ └── FormTextFieldValidateUserData.tsx │ │ │ │ ├── index.ts │ │ │ │ └── SignUpForm.tsx │ │ │ └── signUp.tsx │ │ ├── Root │ │ │ └── Root.tsx │ │ └── EmailVerification │ │ │ └── EmailVerification.tsx │ ├── routing │ │ ├── guards │ │ │ ├── index.ts │ │ │ └── Auth.guard.tsx │ │ ├── index.ts │ │ ├── routes.model.ts │ │ └── Routing.tsx │ ├── hooks │ │ ├── private │ │ │ ├── index.ts │ │ │ └── useLocalAuth.ts │ │ ├── public │ │ │ ├── useGlobalAuth.ts │ │ │ ├── useRouting.ts │ │ │ ├── useDebounce.ts │ │ │ ├── useCheckServerStatus.ts │ │ │ └── useStopwatch.ts │ │ └── index.ts │ ├── assets │ │ ├── Loaders │ │ │ ├── index.ts │ │ │ └── BasicLoader │ │ │ │ └── BasicLoader.tsx │ │ └── Icons.tsx │ ├── workers │ │ ├── index.ts │ │ └── stopwatch.workerBuilder.ts │ ├── services │ │ ├── store │ │ │ ├── zustand │ │ │ │ ├── index.ts │ │ │ │ └── windowSystem.zustand.ts │ │ │ └── context │ │ │ │ └── user │ │ │ │ ├── index.ts │ │ │ │ ├── user.context.ts │ │ │ │ └── user.provider.tsx │ │ └── others │ │ │ ├── env.service.ts │ │ │ ├── index.ts │ │ │ ├── logger.service.ts │ │ │ ├── server.service.ts │ │ │ └── auth.service.ts │ ├── config │ │ ├── index.ts │ │ ├── app.config.ts │ │ └── axios.config.ts │ ├── components │ │ ├── ui │ │ │ ├── DropdownMenu │ │ │ │ ├── components │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── Item.tsx │ │ │ │ │ └── Content.tsx │ │ │ │ └── DropdownMenu.tsx │ │ │ ├── Window │ │ │ │ ├── components │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── WindowContent.tsx │ │ │ │ │ └── WindowHeader.tsx │ │ │ │ └── Window.tsx │ │ │ ├── Select │ │ │ │ ├── components │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── SelectGroupLabel.tsx │ │ │ │ │ ├── SelectCurrentValue.tsx │ │ │ │ │ └── SelectItem.tsx │ │ │ │ └── Select.tsx │ │ │ ├── TextField │ │ │ │ ├── components │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── TextFieldLabel.tsx │ │ │ │ │ ├── TextFieldError.tsx │ │ │ │ │ └── TextFieldLoader.tsx │ │ │ │ └── TextField.tsx │ │ │ ├── Table │ │ │ │ ├── components │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── TableBody.tsx │ │ │ │ │ ├── TableHead.tsx │ │ │ │ │ ├── TableRow.tsx │ │ │ │ │ └── TableCell.tsx │ │ │ │ └── Table.tsx │ │ │ ├── index.ts │ │ │ ├── Anchor │ │ │ │ └── Anchor.tsx │ │ │ ├── Title │ │ │ │ └── Title.tsx │ │ │ ├── P │ │ │ │ └── P.tsx │ │ │ ├── Sheet.tsx │ │ │ ├── Input.tsx │ │ │ ├── SwitchInput.tsx │ │ │ └── Button.tsx │ │ └── feature │ │ │ ├── index.ts │ │ │ ├── FormTextField │ │ │ └── FormTextField.tsx │ │ │ └── AppLogo.tsx │ ├── layouts │ │ ├── index.ts │ │ ├── SectionLayout │ │ │ ├── components │ │ │ │ ├── index.ts │ │ │ │ ├── SectionLayoutContent.tsx │ │ │ │ └── SectionLayoutHeader.tsx │ │ │ └── SectionLayout.tsx │ │ └── DividedLayout │ │ │ └── DividedLayout.tsx │ ├── utils │ │ └── others │ │ │ ├── index.ts │ │ │ ├── formatTime.util.ts │ │ │ ├── driver.util.ts │ │ │ ├── toast.util.ts │ │ │ ├── local-storage.util.ts │ │ │ ├── download-recording.util.ts │ │ │ ├── logger.util.ts │ │ │ ├── ffmpeg.util.ts │ │ │ └── screen-recorder.util.ts │ ├── main.tsx │ ├── index.css │ ├── App.tsx │ └── styles │ │ └── driverjs.css │ ├── .env.template │ ├── postcss.config.js │ ├── vercel.json │ ├── tsconfig.node.json │ ├── tailwind.config.js │ ├── vite.config.ts │ ├── playwright │ └── e2e │ │ └── user-journal.spec.ts │ ├── tsconfig.json │ ├── .eslintrc.cjs │ ├── public │ └── stopwatch.worker.js │ ├── index.html │ ├── package.json │ └── playwright.config.ts ├── .commitlintrc.json ├── .husky ├── pre-commit └── commit-msg ├── turbo.json ├── .github ├── CODEOWNERS └── workflows │ └── playwright.yml ├── .gitignore ├── README.md ├── package.json ├── LICENSE.md └── CODE_OF_CONDUCT.md /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'apps/*' -------------------------------------------------------------------------------- /apps/back/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './env.config'; 2 | -------------------------------------------------------------------------------- /apps/web-app/src/models/dtos/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.dto' 2 | -------------------------------------------------------------------------------- /apps/back/src/app/auth/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './roles.guard'; 2 | -------------------------------------------------------------------------------- /apps/web-app/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { "extends": [ "@commitlint/config-conventional" ] } 2 | -------------------------------------------------------------------------------- /apps/back/src/app/auth/strategies/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jwt.strategy'; 2 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/layouts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MainLayout' 2 | -------------------------------------------------------------------------------- /apps/web-app/src/routing/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Auth.guard' 2 | 3 | -------------------------------------------------------------------------------- /apps/web-app/src/hooks/private/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useLocalAuth' 2 | 3 | -------------------------------------------------------------------------------- /apps/web-app/src/models/validations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.validation' 2 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/SignIn/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SignInForm' 2 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/SignIn/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './signIn.schema' 2 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/SignUp/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './signUp.schema' 2 | -------------------------------------------------------------------------------- /apps/back/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /apps/web-app/.env.template: -------------------------------------------------------------------------------- 1 | # BACK HOST 2 | VITE_BACK_HOST="http://localhost:3000/api/" -------------------------------------------------------------------------------- /apps/web-app/src/assets/Loaders/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BasicLoader/BasicLoader' 2 | -------------------------------------------------------------------------------- /apps/web-app/src/workers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './stopwatch.workerBuilder' 2 | 3 | -------------------------------------------------------------------------------- /apps/back/src/app/common/exceptions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bad-credentials.exception'; 2 | -------------------------------------------------------------------------------- /apps/back/src/app/common/util/index.ts: -------------------------------------------------------------------------------- 1 | export * from './postgres-error-handler.util'; 2 | -------------------------------------------------------------------------------- /apps/web-app/src/services/store/zustand/index.ts: -------------------------------------------------------------------------------- 1 | export * from './windowSystem.zustand' 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm exec turbo lint -------------------------------------------------------------------------------- /apps/back/src/app/email-verification/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './unverified-email.entity'; 2 | -------------------------------------------------------------------------------- /apps/web-app/src/routing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Routing' 2 | export * from './routes.model' 3 | 4 | -------------------------------------------------------------------------------- /apps/back/src/app/auth/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jwt.model'; 2 | export * from './roles.model'; 3 | -------------------------------------------------------------------------------- /apps/web-app/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app.config' 2 | export * from './axios.config' 3 | 4 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm exec -- commitlint --edit ${1} -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/RecordingWindow/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './recorder.model' 2 | -------------------------------------------------------------------------------- /apps/back/src/app/auth/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.decorator'; 2 | export * from './set-roles.decorator'; 3 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/WatchRecordingWindow/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useWatchRecording' 2 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/DropdownMenu/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Content' 2 | export * from './Item' 3 | 4 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/CompatibilityWindow/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useCompatibilityWindow' 2 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/CompatibilityWindow/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './default-values.model' 2 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/DownloadRecordingWindow/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useDownloadRecording' 2 | -------------------------------------------------------------------------------- /apps/web-app/src/components/feature/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AppLogo' 2 | export * from './FormTextField/FormTextField' 3 | 4 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/Window/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './WindowContent' 2 | export * from './WindowHeader' 3 | 4 | -------------------------------------------------------------------------------- /apps/web-app/src/services/others/env.service.ts: -------------------------------------------------------------------------------- 1 | export const getEnv = (key: string) => { 2 | return import.meta.env[key] 3 | } 4 | -------------------------------------------------------------------------------- /apps/web-app/src/services/store/context/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.context' 2 | export * from './user.provider' 3 | 4 | -------------------------------------------------------------------------------- /apps/back/src/app/auth/models/jwt.model.ts: -------------------------------------------------------------------------------- 1 | import { UUID } from 'crypto'; 2 | 3 | export interface JwtPayload { 4 | id: UUID; 5 | } 6 | -------------------------------------------------------------------------------- /apps/web-app/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /apps/web-app/src/layouts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DividedLayout/DividedLayout' 2 | export * from './SectionLayout/SectionLayout' 3 | 4 | -------------------------------------------------------------------------------- /apps/back/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /apps/web-app/src/layouts/SectionLayout/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SectionLayoutContent' 2 | export * from './SectionLayoutHeader' 3 | 4 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/SignUp/components/FormTextFieldValidateUserData/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useFormTextFieldValidateUserData' 2 | 3 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/RecordingWindow/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useRecorder' 2 | export * from './useRecorderWindow' 3 | 4 | -------------------------------------------------------------------------------- /apps/web-app/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/(.*)", 5 | "destination": "/" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /apps/web-app/src/pages/SignUp/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './FormTextFieldValidateUserData/FormTextFieldValidateUserData' 2 | export * from './SignUpForm' 3 | 4 | -------------------------------------------------------------------------------- /apps/back/src/app/auth/dto/check-email.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail } from 'class-validator'; 2 | 3 | export class CheckEmailDTO { 4 | @IsEmail() 5 | email: string; 6 | } 7 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/Select/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SelectCurrentValue' 2 | export * from './SelectGroupLabel' 3 | export * from './SelectItem' 4 | 5 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/TextField/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TextFieldError' 2 | export * from './TextFieldLabel' 3 | export * from './TextFieldLoader' 4 | 5 | -------------------------------------------------------------------------------- /apps/web-app/src/hooks/public/useGlobalAuth.ts: -------------------------------------------------------------------------------- 1 | import { useAuthContext } from '@/services/store/context/user' 2 | 3 | export const useGlobalAuth = () => useAuthContext() 4 | -------------------------------------------------------------------------------- /apps/back/src/app/auth/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './check-email.dto'; 2 | export * from './check-username.dto'; 3 | export * from './sign-in.dto'; 4 | export * from './sign-up.dto'; 5 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/Table/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TableBody' 2 | export * from './TableCell' 3 | export * from './TableHead' 4 | export * from './TableRow' 5 | 6 | -------------------------------------------------------------------------------- /apps/web-app/src/services/others/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.service' 2 | export * from './env.service' 3 | export * from './logger.service' 4 | export * from './server.service' 5 | 6 | -------------------------------------------------------------------------------- /apps/back/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 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/back/src/app/auth/decorators/set-roles.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Reflector } from '@nestjs/core'; 2 | import { TValidRoleArray } from '../models'; 3 | 4 | export const SetRoles = Reflector.createDecorator(); 5 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/Table/components/TableBody.tsx: -------------------------------------------------------------------------------- 1 | import { type BaseComponentType } from '@/models' 2 | 3 | export const TableBody: BaseComponentType = (props) => { 4 | return ( 5 | 6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/Table/components/TableHead.tsx: -------------------------------------------------------------------------------- 1 | import { type BaseComponentType } from '@/models' 2 | 3 | export const TableHead: BaseComponentType = (props) => { 4 | return ( 5 | 6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "pipeline": { 4 | "dev": { 5 | "cache": false, 6 | "persistent": true 7 | }, 8 | "lint": {} 9 | } 10 | } -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners are the default owners for everything in 2 | # the repo. Unless a later match takes precedence, 3 | # @global-owner1 will be requested for 4 | # review when someone opens a pull request. 5 | * @Jes015 6 | -------------------------------------------------------------------------------- /apps/back/src/app/auth/dto/check-username.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, MaxLength, MinLength } from 'class-validator'; 2 | 3 | export class CheckUsernameDTO { 4 | @IsString() 5 | @MinLength(4) 6 | @MaxLength(40) 7 | username: string; 8 | } 9 | -------------------------------------------------------------------------------- /apps/web-app/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './public/useCheckServerStatus' 2 | export * from './public/useDebounce' 3 | export * from './public/useGlobalAuth' 4 | export * from './public/useRouting' 5 | export * from './public/useStopwatch' 6 | 7 | -------------------------------------------------------------------------------- /apps/back/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 | -------------------------------------------------------------------------------- /apps/web-app/src/services/others/logger.service.ts: -------------------------------------------------------------------------------- 1 | import { type ViteEnvironmentType } from '@/models' 2 | import { Logger } from '@/utils/others/logger.util' 3 | 4 | export const LoggerService = new Logger(import.meta.env.MODE as ViteEnvironmentType) 5 | -------------------------------------------------------------------------------- /apps/web-app/src/models/environment.model.ts: -------------------------------------------------------------------------------- 1 | export const ViteEnvironment = { 2 | development: 'development', 3 | production: 'production' 4 | } as const 5 | 6 | export type ViteEnvironmentType = typeof ViteEnvironment[keyof typeof ViteEnvironment] 7 | 8 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/RecordingWindow/services/context/index.ts: -------------------------------------------------------------------------------- 1 | export * from './recorder.context' 2 | export * from './recorder.provider' 3 | export * from './recorderWindow.context' 4 | export * from './recorderWindow.provider' 5 | 6 | -------------------------------------------------------------------------------- /apps/web-app/src/models/dtos/user.dto.ts: -------------------------------------------------------------------------------- 1 | import { type User } from '@/models' 2 | 3 | export interface UserSignInDTO extends Pick {} 4 | 5 | export interface UserSignUpDTO extends Pick {} 6 | 7 | -------------------------------------------------------------------------------- /apps/back/src/app/auth/models/roles.model.ts: -------------------------------------------------------------------------------- 1 | export const CValidRoles = { 2 | user: 'user', 3 | admin: 'admin', 4 | } as const; 5 | 6 | export type TValidRole = (typeof CValidRoles)[keyof typeof CValidRoles]; 7 | 8 | export type TValidRoleArray = TValidRole[]; 9 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/SignIn/models/signIn.schema.ts: -------------------------------------------------------------------------------- 1 | import { userValidations } from '@/models/validations' 2 | import { z } from 'zod' 3 | 4 | export const signInSchema = z.object({ 5 | email: userValidations.email, 6 | password: userValidations.password 7 | }) 8 | -------------------------------------------------------------------------------- /apps/web-app/src/config/app.config.ts: -------------------------------------------------------------------------------- 1 | export const appConfig = { 2 | name: 'WebCapture', 3 | localStorageKeys: { 4 | token: 'token', 5 | user: 'user-data' 6 | }, 7 | links: { 8 | github: 'https://github.com/Jes015/Web-Capture' 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/web-app/src/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api.model' 2 | export * from './component.model' 3 | export * from './dtos' 4 | export * from './environment.model' 5 | export * from './timer.model' 6 | export * from './user.model' 7 | export * from './window.model' 8 | 9 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/RecordingWindow/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RecordControls' 2 | export * from './RecordData' 3 | export * from './RecordVideo' 4 | export * from './RecordingWindowDropdownMenu' 5 | export * from './RecordingWindowWrap' 6 | 7 | -------------------------------------------------------------------------------- /apps/web-app/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/back/src/app/common/exceptions/bad-credentials.exception.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus } from '@nestjs/common'; 2 | 3 | export class BadCredentialsException extends HttpException { 4 | constructor(error?: string) { 5 | super(error ?? 'Bad credentials', HttpStatus.UNAUTHORIZED); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/SignUp/models/signUp.schema.ts: -------------------------------------------------------------------------------- 1 | import { userValidations } from '@/models/validations' 2 | import { z } from 'zod' 3 | 4 | export const signUpSchema = z.object({ 5 | email: userValidations.email, 6 | password: userValidations.password, 7 | username: userValidations.username 8 | }) 9 | -------------------------------------------------------------------------------- /apps/web-app/src/models/component.model.ts: -------------------------------------------------------------------------------- 1 | export interface BaseComponentProps extends React.HTMLAttributes { 2 | } 3 | 4 | export type BaseComponentType = React.FC 5 | 6 | export type FC = React.FC 7 | 8 | export type ComponentIcon = React.FC> 9 | -------------------------------------------------------------------------------- /apps/web-app/src/models/timer.model.ts: -------------------------------------------------------------------------------- 1 | export interface Time { 2 | seconds: number 3 | minutes: number 4 | hours: number 5 | } 6 | 7 | export type TimeStrap = Omit 8 | 9 | export interface WorkerStopwatchMessage { 10 | type?: 'start' | 'stop' | 'setInitial' 11 | time?: Time 12 | } 13 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/RecordingWindow/models/recorder.model.ts: -------------------------------------------------------------------------------- 1 | export const recordingStatusType = { 2 | on: 'on', 3 | off: 'off', 4 | paused: 'paused', 5 | resumed: 'resumed' 6 | } as const 7 | 8 | export type RecordingStatus = typeof recordingStatusType[keyof typeof recordingStatusType] 9 | -------------------------------------------------------------------------------- /apps/web-app/src/services/store/context/user/user.context.ts: -------------------------------------------------------------------------------- 1 | import { defaultUserValues, type AuthContext } from '@/models' 2 | import { createContext, useContext } from 'react' 3 | 4 | export const authContext = createContext(defaultUserValues) 5 | 6 | export const useAuthContext = () => useContext(authContext) 7 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/CompatibilityWindow/models/default-values.model.ts: -------------------------------------------------------------------------------- 1 | export const defaultCompatibilityValues = { 2 | 'Webcam Recording': true, 3 | 'Screen Recording': true, 4 | 'Take Screenshot': true 5 | } 6 | 7 | export type CompatibilityType = typeof defaultCompatibilityValues 8 | 9 | 10 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AddWindowsDropdownMenu' 2 | export * from './CompatibilityWindow/CompatibilityWindow' 3 | export * from './DownloadRecordingWindow/DownloadRecordingWindow' 4 | export * from './RecordingWindow/RecordingWindow' 5 | export * from './WatchRecordingWindow/WatchRecordingWindow' 6 | 7 | -------------------------------------------------------------------------------- /apps/back/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "extends": ["//"], 4 | "pipeline": { 5 | "upDB": { 6 | "cache": false 7 | }, 8 | "dev": { 9 | "cache": false, 10 | "persistent": true, 11 | "dependsOn": ["upDB"] 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /apps/web-app/src/utils/others/index.ts: -------------------------------------------------------------------------------- 1 | export * from './download-recording.util' 2 | export * from './driver.util' 3 | // export * from './ffmpeg.util' 4 | export * from './formatTime.util' 5 | export * from './local-storage.util' 6 | export * from './logger.util' 7 | export * from './screen-recorder.util' 8 | export * from './toast.util' 9 | 10 | -------------------------------------------------------------------------------- /apps/web-app/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: { 9 | animation: { 10 | 'pulse-fast': 'pulse 900ms linear infinite;' 11 | } 12 | }, 13 | }, 14 | plugins: [], 15 | } -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/TextField/components/TextFieldLabel.tsx: -------------------------------------------------------------------------------- 1 | import { type BaseComponentType } from '@/models' 2 | 3 | export const TextFieldLabel: BaseComponentType = ({ children, className }) => { 4 | return ( 5 | 8 | {children} 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /apps/back/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | db: 5 | image: postgres:14.3 6 | restart: always 7 | ports: 8 | - "5432:5432" 9 | environment: 10 | POSTGRES_DB: ${DB_NAME} 11 | POSTGRES_PASSWORD: ${DB_PASSWORD} 12 | container_name: ScreenRecorderDB 13 | volumes: 14 | - './postgres:/var/lib/postgresql/data' -------------------------------------------------------------------------------- /apps/back/src/app/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { UserService } from './user.service'; 3 | 4 | @Controller('user') 5 | export class UserController { 6 | constructor(private readonly userService: UserService) {} 7 | 8 | @Get() 9 | getAllUsers() { 10 | return this.userService.getAll(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/back/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.format.semicolons": "insert", 3 | "javascript.format.semicolons": "insert", 4 | "editor.autoIndent": "full", 5 | "editor.insertSpaces": false, 6 | "editor.comments.insertSpace": true, 7 | "javascript.format.insertSpaceAfterCommaDelimiter": true, 8 | "typescript.format.insertSpaceAfterCommaDelimiter": true 9 | } -------------------------------------------------------------------------------- /apps/web-app/src/main.tsx: -------------------------------------------------------------------------------- 1 | import '@fontsource/roboto/400.css' 2 | import '@fontsource/roboto/500.css' 3 | import '@fontsource/roboto/700.css' 4 | import { createRoot } from 'react-dom/client' 5 | import App from './App.tsx' 6 | import './index.css' 7 | 8 | const entryPoint = document.getElementById('root') 9 | const root = createRoot(entryPoint as Element) 10 | root.render( 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /apps/web-app/src/layouts/SectionLayout/components/SectionLayoutContent.tsx: -------------------------------------------------------------------------------- 1 | import { type BaseComponentType } from '@/models' 2 | 3 | export const SectionLayoutContent: BaseComponentType = ({ children, className }) => { 4 | return ( 5 |
12 | {children} 13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /apps/web-app/src/workers/stopwatch.workerBuilder.ts: -------------------------------------------------------------------------------- 1 | import { type Time } from '@/models' 2 | 3 | export class StopwatchWorkerBuilder extends Worker { 4 | startStopwatch () { 5 | this.postMessage({ type: 'start' }) 6 | } 7 | 8 | stopStopwatch () { 9 | this.postMessage({ type: 'stop' }) 10 | } 11 | 12 | setInitialTime (time: Time) { 13 | this.postMessage({ type: 'setInitial', time }) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/Table/components/TableRow.tsx: -------------------------------------------------------------------------------- 1 | import { type BaseComponentType } from '@/models' 2 | import clsx from 'clsx' 3 | 4 | export const TableRow: BaseComponentType = ({ className, ...props }) => { 5 | return ( 6 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /apps/web-app/src/hooks/public/useRouting.ts: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom' 2 | 3 | export const useRouting = () => { 4 | const navigate = useNavigate() 5 | 6 | const goTo = (path: string) => { 7 | navigate(path) 8 | } 9 | 10 | const goBack = () => { 11 | navigate(-1) 12 | } 13 | 14 | const goForward = () => { 15 | navigate(+1) 16 | } 17 | 18 | return { goTo, goBack, goForward } 19 | } 20 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/Window/components/WindowContent.tsx: -------------------------------------------------------------------------------- 1 | import { SectionLayout } from '@/layouts' 2 | import { type BaseComponentType } from '@/models' 3 | 4 | export const WindowContent: BaseComponentType = ({ className, children }) => { 5 | return ( 6 | 7 | {children} 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Anchor/Anchor' 2 | export * from './Button' 3 | export * from './DropdownMenu/DropdownMenu' 4 | export * from './Input' 5 | export * from './P/P' 6 | export * from './Select/Select' 7 | export * from './Sheet' 8 | export * from './SwitchInput' 9 | export * from './Table/Table' 10 | export * from './TextField/TextField' 11 | export * from './Title/Title' 12 | export * from './Window/Window' 13 | 14 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Root/Root.tsx: -------------------------------------------------------------------------------- 1 | import { useCheckServerStatus } from '@/hooks' 2 | import { type BaseComponentType } from '@/models' 3 | import { UserProvider } from '@/services/store/context/user' 4 | import { Outlet } from 'react-router-dom' 5 | 6 | const RootPage: BaseComponentType = () => { 7 | useCheckServerStatus() 8 | 9 | return ( 10 | 11 | 12 | 13 | ) 14 | } 15 | 16 | export default RootPage 17 | -------------------------------------------------------------------------------- /apps/web-app/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react-swc' 2 | import { resolve } from 'path' 3 | import { defineConfig } from 'vite' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | resolve: { 9 | alias: { 10 | '@': resolve(__dirname, './src/') 11 | } 12 | }, 13 | optimizeDeps: { 14 | exclude: ['vite:worker', 'vite:worker-import-meta-url', 'worker', 'worker.js'] 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /apps/back/src/app/auth/decorators/auth.decorator.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards, applyDecorators } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | import { RoleGuard } from '../guards'; 4 | import { TValidRoleArray } from '../models'; 5 | import { SetRoles } from './set-roles.decorator'; 6 | 7 | export const Auth = (...roles: TValidRoleArray) => 8 | applyDecorators( 9 | SetRoles([...roles, 'user']), 10 | UseGuards(AuthGuard(), RoleGuard), 11 | ); 12 | -------------------------------------------------------------------------------- /apps/web-app/src/models/api.model.ts: -------------------------------------------------------------------------------- 1 | import { type PublicUser } from '@/models' 2 | 3 | export interface AuthSuccessApi { 4 | token: string 5 | user: PublicUser 6 | } 7 | 8 | export interface NotFoundApi { 9 | statusCode: 404 10 | } 11 | 12 | export type ApiOK = 'OK' 13 | 14 | export const StatusCodes = { 15 | Conflict: 409, 16 | NotFound: 404, 17 | Forbidden: 401, 18 | TooManyRequests: 429 19 | } 20 | 21 | export type CheckUserTypeParams = 'email' | 'username' 22 | -------------------------------------------------------------------------------- /apps/web-app/src/services/store/context/user/user.provider.tsx: -------------------------------------------------------------------------------- 1 | import { useLocalAuth } from '@/hooks/private' 2 | import { type BaseComponentType } from '@/models' 3 | import { authContext } from './user.context' 4 | 5 | 6 | export const UserProvider: BaseComponentType = ({ children }) => { 7 | const values = useLocalAuth() 8 | 9 | return ( 10 | 11 | {children} 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/Select/components/SelectGroupLabel.tsx: -------------------------------------------------------------------------------- 1 | import { type BaseComponentType } from '@/models' 2 | import { SelectLabel } from '@radix-ui/react-select' 3 | 4 | export const SelectGroupLabel: BaseComponentType = ({ children, className }) => { 5 | return ( 6 | 11 | {children} 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/TextField/components/TextFieldError.tsx: -------------------------------------------------------------------------------- 1 | import { P } from '@/components/ui' 2 | import { type BaseComponentType } from '@/models' 3 | import clsx from 'clsx' 4 | 5 | export const TextFieldError: BaseComponentType = ({ className, ...props }) => { 6 | return ( 7 |

17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /apps/back/src/app/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { User } from './entities/user.entity'; 4 | import { UserController } from './user.controller'; 5 | import { UserService } from './user.service'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([User])], 9 | controllers: [UserController], 10 | providers: [UserService], 11 | exports: [TypeOrmModule], 12 | }) 13 | export class UserModule {} 14 | -------------------------------------------------------------------------------- /apps/web-app/src/routing/guards/Auth.guard.tsx: -------------------------------------------------------------------------------- 1 | import { useGlobalAuth } from '@/hooks' 2 | import { testUserRole, type BaseComponentType } from '@/models' 3 | import { frontRoutes } from '@/routing' 4 | import { Navigate, Outlet } from 'react-router-dom' 5 | 6 | export const AuthGuard: BaseComponentType = () => { 7 | const { user } = useGlobalAuth() 8 | 9 | if (user.roles?.[0] !== testUserRole) { 10 | return 11 | } 12 | 13 | return 14 | } 15 | -------------------------------------------------------------------------------- /apps/web-app/src/utils/others/formatTime.util.ts: -------------------------------------------------------------------------------- 1 | import { type Time } from '@/models' 2 | 3 | export const formatTime = (time: Time | null) => { 4 | let hours = '00' 5 | let minutes = '00' 6 | let seconds = '00' 7 | 8 | if (time == null) return { hours, minutes, seconds } 9 | 10 | hours = time.hours.toString().padStart(2, '0') 11 | minutes = time.minutes.toString().padStart(2, '0') 12 | seconds = time.seconds.toString().padStart(2, '0') 13 | 14 | return { hours, minutes, seconds } 15 | } 16 | -------------------------------------------------------------------------------- /apps/back/src/app/common/util/postgres-error-handler.util.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ConflictException, 3 | InternalServerErrorException, 4 | } from '@nestjs/common'; 5 | 6 | export const postgresErrorHandler = (error: any) => { 7 | const errorMessage = error?.detail ?? 'Unknown error'; 8 | const errorClass = 9 | DB_ERROR_EXCEPTIONS?.[error.code] ?? InternalServerErrorException; 10 | 11 | throw new errorClass(errorMessage); 12 | }; 13 | 14 | const DB_ERROR_EXCEPTIONS = { 15 | '23505': ConflictException, 16 | }; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # Playwright 27 | /test-results/ 28 | /playwright-report/ 29 | /blob-report/ 30 | /playwright/.cache/ 31 | /test-examples 32 | 33 | 34 | # BACK 35 | postgres 36 | .env -------------------------------------------------------------------------------- /apps/web-app/src/hooks/public/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | export const useDebounce = (value: unknown, time: number) => { 4 | const [debouncedValue, setDebouncedValue] = useState(value) 5 | 6 | useEffect(() => { 7 | const timeout = setTimeout(() => { 8 | setDebouncedValue(value) 9 | }, time) 10 | 11 | return () => { 12 | clearTimeout(timeout) 13 | } 14 | // eslint-disable-next-line react-hooks/exhaustive-deps 15 | }, [value]) 16 | 17 | return { debouncedValue } 18 | } 19 | -------------------------------------------------------------------------------- /apps/web-app/src/utils/others/driver.util.ts: -------------------------------------------------------------------------------- 1 | import '@/styles/driverjs.css' 2 | import { driver } from 'driver.js' 3 | import 'driver.js/dist/driver.css' 4 | 5 | export const showDriver = () => { 6 | const driverObj = driver({ 7 | popoverClass: 'driverjs-theme', 8 | steps: [ 9 | { 10 | element: '#addWindows', 11 | popover: { 12 | title: 'Windows menu', 13 | description: 'Click here to add recording windows' 14 | } 15 | } 16 | ] 17 | }) 18 | 19 | driverObj.drive() 20 | } 21 | -------------------------------------------------------------------------------- /apps/web-app/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | overflow: hidden; 7 | background-color: #171717; 8 | background-image: url("data:image/svg+xml,%3Csvg width='40' height='12' viewBox='0 0 40 12' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 6.172L6.172 0h5.656L0 11.828V6.172zm40 5.656L28.172 0h5.656L40 6.172v5.656zM6.172 12l12-12h3.656l12 12h-5.656L20 3.828 11.828 12H6.172zm12 0L20 10.172 21.828 12h-3.656z' fill='%23262626' fill-opacity='0.09' fill-rule='evenodd'/%3E%3C/svg%3E"); 9 | } -------------------------------------------------------------------------------- /apps/web-app/src/utils/others/toast.util.ts: -------------------------------------------------------------------------------- 1 | import { toast as defaultToast } from 'sonner' 2 | 3 | type ToastTypes = 'message' | 'success' | 'info' | 'warning' | 'error' 4 | 5 | export const toast = { 6 | message: (message: string, type: ToastTypes) => { 7 | defaultToast?.[type](message) 8 | }, 9 | async: (messageSuccess: string, messageLoading: string, messageError: string, promise: () => Promise) => { 10 | defaultToast.promise(promise, { success: messageSuccess, loading: messageLoading, error: messageError }) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/Anchor/Anchor.tsx: -------------------------------------------------------------------------------- 1 | import { type FC } from '@/models' 2 | import clsx from 'clsx' 3 | import { type AnchorHTMLAttributes } from 'react' 4 | 5 | export interface AnchorProps extends AnchorHTMLAttributes { } 6 | 7 | export const Anchor: FC = ({ className, ...props }) => { 8 | return ( 9 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /apps/back/src/app/email-verification/entities/unverified-email.entity.ts: -------------------------------------------------------------------------------- 1 | import { UUID } from 'crypto'; 2 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 3 | 4 | @Entity('unverified-emails') 5 | export class UnverifiedEmail { 6 | @PrimaryGeneratedColumn('uuid') 7 | id: UUID; 8 | 9 | @Column('text', { 10 | unique: true, 11 | }) 12 | email: string; 13 | 14 | @Column('text', { 15 | unique: true, 16 | }) 17 | username: string; 18 | 19 | @Column('bigint', { 20 | default: new Date().getTime(), 21 | }) 22 | requestedVerification: number; 23 | } 24 | -------------------------------------------------------------------------------- /apps/web-app/src/hooks/public/useCheckServerStatus.ts: -------------------------------------------------------------------------------- 1 | import { checkServerStatusService } from '@/services/others' 2 | import { toast } from '@/utils/others' 3 | import { useEffect } from 'react' 4 | 5 | export const useCheckServerStatus = () => { 6 | useEffect(() => { 7 | toast.async( 8 | 'The server is online', 9 | 'The server is setting up', 10 | 'The server is offline, but don\'t worry, normal actions like recording does not need the server <3. The server is only used for auth and mp4/mp3 processing.', 11 | checkServerStatusService 12 | ) 13 | }, []) 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Screen Recording Project 2 | 3 | This project provides an easy-to-use screen recording tool, developed in TypeScript, React and NestJS. [Try it out](https://screen-capture-nine.vercel.app) 4 | 5 | 6 | 7 | ## Features 8 | 9 | - **Screen Recording:** Capture video of the user's screen. 10 | - **File Format Support:** Save recordings in various video formats (webp). 11 | - **Cross-Platform Compatibility:** Works seamlessly on different operating systems. 12 | 13 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/Select/components/SelectCurrentValue.tsx: -------------------------------------------------------------------------------- 1 | import { ChevronDownIcon } from '@radix-ui/react-icons' 2 | import { SelectIcon, SelectValue } from '@radix-ui/react-select' 3 | 4 | interface SelectCurrentValueProps { 5 | placeholder?: string 6 | } 7 | 8 | export const SelectCurrentValue: React.FC = ({ placeholder }) => { 9 | return ( 10 | <> 11 | 12 | 13 | 14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /apps/web-app/src/models/validations/user.validation.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod' 2 | 3 | export const userValidations = { 4 | username: z.string().min(4).max(20).regex(/^[a-zA-Z0-9_-]+$/, { message: 'The username should not contain symbols or operators' }), 5 | email: z.string().email().max(50).refine((email) => email.endsWith('@gmail.com'), { message: 'Only gmail.com directions are allowed' }), 6 | password: z.string().min(5).max(40).regex(/(?:(?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, { 7 | message: 'The password must have an uppercase, lowercase letter, and a number' 8 | }) 9 | } as const 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ScreenRecorder", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "keywords": [], 7 | "author": "", 8 | "license": "ISC", 9 | "devDependencies": { 10 | "@commitlint/cli": "^18.6.0", 11 | "@commitlint/config-conventional": "^18.6.0", 12 | "husky": "^9.0.10" 13 | }, 14 | "scripts": { 15 | "prepare": "husky", 16 | "dev": "turbo run dev" 17 | }, 18 | "dependencies": { 19 | "turbo": "^1.12.3" 20 | }, 21 | "packageManager": "pnpm@9.6.0+sha256.dae0f7e822c56b20979bb5965e3b73b8bdabb6b8b8ef121da6d857508599ca35" 22 | } 23 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/layouts/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import { AppLogo } from '@/components/feature' 2 | import { type BaseComponentType } from '@/models' 3 | import { AddWindowsDropdownMenu, CompatibilityWindow } from '@/pages/Home/components/feature' 4 | 5 | export const MainLayout: BaseComponentType = ({ children }) => { 6 | return ( 7 |

13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /apps/back/src/app/email-verification/email-verification.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Param } from '@nestjs/common'; 2 | import { EmailVerificationService } from './email-verification.service'; 3 | 4 | @Controller('email-verification') 5 | export class EmailVerificationController { 6 | constructor( 7 | private readonly emailVerificationService: EmailVerificationService, 8 | ) {} 9 | 10 | @Get(':verificationToken') 11 | verifyEmail(@Param('verificationToken') verificationToken: string) { 12 | return this.emailVerificationService.verifyAndSignUpEmail( 13 | verificationToken, 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/back/src/app/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { User } from './entities/user.entity'; 5 | 6 | @Injectable() 7 | export class UserService { 8 | constructor( 9 | @InjectRepository(User) 10 | private userRepository: Repository, 11 | ) {} 12 | 13 | async getAll() { 14 | try { 15 | const users = await this.userRepository.find({}); 16 | console.log(users); 17 | return users; 18 | } catch (error) { 19 | console.log(error); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/RecordingWindow/hooks/useRecorderWindow.ts: -------------------------------------------------------------------------------- 1 | import { type RecordWindowData } from '@/models' 2 | import { useState } from 'react' 3 | 4 | export const useRecorderWindow = (windowData: RecordWindowData) => { 5 | const [isDisplayingVideo, setIsDisplayingVideo] = useState(false) 6 | 7 | const toggleVideoVisibility = (newSate?: boolean) => { 8 | setIsDisplayingVideo((prevState) => newSate ?? !prevState) 9 | } 10 | 11 | const getWindowData = () => { 12 | return structuredClone(windowData) 13 | } 14 | 15 | return { isDisplayingVideo, toggleVideoVisibility, getWindowData } 16 | } 17 | -------------------------------------------------------------------------------- /apps/back/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/web-app/src/layouts/SectionLayout/SectionLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Sheet } from '@/components/ui' 2 | import { type BaseComponentProps } from '@/models' 3 | import { SectionLayoutContent, SectionLayoutHeader } from './components' 4 | 5 | export const SectionLayout = ({ children, className }: BaseComponentProps) => { 6 | return ( 7 | 16 | {children} 17 | 18 | ) 19 | } 20 | 21 | SectionLayout.Header = SectionLayoutHeader 22 | SectionLayout.Content = SectionLayoutContent 23 | -------------------------------------------------------------------------------- /apps/back/src/app/auth/dto/sign-in.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsEmail, 3 | IsString, 4 | Matches, 5 | MaxLength, 6 | MinLength, 7 | } from 'class-validator'; 8 | 9 | export class SignInDto { 10 | @IsEmail( 11 | { 12 | host_whitelist: ['gmail.com'], 13 | }, 14 | { 15 | message: 'email must be an email gmail.', 16 | }, 17 | ) 18 | @MaxLength(50) 19 | email: string; 20 | 21 | @IsString() 22 | @MinLength(4) 23 | @MaxLength(40) 24 | @Matches(/(?:(?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, { 25 | message: 26 | 'The password must have a Uppercase, lowercase letter and a number', 27 | }) 28 | password: string; 29 | } 30 | -------------------------------------------------------------------------------- /apps/web-app/src/services/others/server.service.ts: -------------------------------------------------------------------------------- 1 | import { type NotFoundApi } from '@/models' 2 | import { backRoutes } from '@/routing' 3 | import axios, { type AxiosError } from 'axios' 4 | 5 | export const checkServerStatusService = async () => { 6 | return await new Promise((resolve, reject) => { 7 | axios.get(backRoutes.home ?? '/').then(() => { 8 | resolve('OK') 9 | }).catch((error: AxiosError) => { 10 | const statusCode = error.response?.data.statusCode 11 | 12 | if (statusCode === 404) { 13 | resolve('OK') 14 | return 15 | } 16 | 17 | reject(new Error('Something went wrong')) 18 | }) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/Table/Table.tsx: -------------------------------------------------------------------------------- 1 | import { type BaseComponentProps } from '@/models' 2 | import clsx from 'clsx' 3 | import { TableBody, TableCell, TableHead, TableRow } from './components' 4 | 5 | export const Table = ({ className, ...props }: BaseComponentProps) => { 6 | return ( 7 | 16 | ) 17 | } 18 | 19 | Table.Body = TableBody 20 | Table.Cell = TableCell 21 | Table.Head = TableHead 22 | Table.Row = TableRow 23 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/Table/components/TableCell.tsx: -------------------------------------------------------------------------------- 1 | import { type BaseComponentProps, type FC } from '@/models' 2 | import clsx from 'clsx' 3 | 4 | interface TableCellProps extends BaseComponentProps { 5 | as: 'th' | 'td' 6 | } 7 | 8 | export const TableCell: FC = ({ className, as, ...props }) => { 9 | const Component = as 10 | 11 | return ( 12 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/Title/Title.tsx: -------------------------------------------------------------------------------- 1 | import { type BaseComponentProps } from '@/models' 2 | import clsx from 'clsx' 3 | 4 | interface TitleProps extends BaseComponentProps { 5 | as?: 'h1' | 'h2' | 'h3' | 'h4' 6 | } 7 | 8 | export const Title: React.FC = ({ as, className, ...props }) => { 9 | const Component = as ?? 'h1' 10 | 11 | return ( 12 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /apps/web-app/src/services/others/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { type AuthSuccessApi } from '@/models' 2 | import { type UserSignInDTO, type UserSignUpDTO } from '@/models/dtos' 3 | import { backRoutes } from '@/routing' 4 | import axios from 'axios' 5 | 6 | export const signInService = async (userSignInDTO: UserSignInDTO) => { 7 | return await axios.post(backRoutes.signIn, userSignInDTO) 8 | } 9 | 10 | export const signUpService = async (userSignUpDTO: UserSignUpDTO) => { 11 | return await axios.post(backRoutes.signUp, userSignUpDTO) 12 | } 13 | 14 | export const verifyEmailService = async (token: string) => { 15 | return await axios.get(backRoutes.emailVerification(token)) 16 | } 17 | -------------------------------------------------------------------------------- /apps/web-app/playwright/e2e/user-journal.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test' 2 | 3 | 4 | test.describe('User Journal', () => { 5 | test('The default components must render', async ({ page }) => { 6 | await page.goto('/') 7 | await page.getByRole('button', { name: 'Done' }).click() 8 | await page.getByLabel('Customize options').click() 9 | await page.getByRole('menuitem', { name: 'Recording', exact: true }).click() 10 | await page.getByRole('button').nth(2).click() 11 | await page.getByLabel('Customize options').click() 12 | await page.getByRole('menuitem', { name: 'Watch Recording' }).click() 13 | await page.getByRole('main').getByRole('button').click() 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /apps/back/src/app/user/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { UUID } from 'crypto'; 2 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 3 | 4 | @Entity('users') 5 | export class User { 6 | @PrimaryGeneratedColumn('uuid') 7 | id: UUID; 8 | 9 | @Column('text', { 10 | unique: true, 11 | }) 12 | email: string; 13 | 14 | @Column('text', { 15 | unique: true, 16 | }) 17 | username: string; 18 | 19 | @Column('text', { 20 | select: false, 21 | }) 22 | password: string; 23 | 24 | @Column('boolean', { 25 | default: true, 26 | }) 27 | isActive: boolean; 28 | 29 | @Column('text', { 30 | array: true, 31 | default: ['user'], 32 | }) 33 | roles: string[]; 34 | } 35 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/RecordingWindow/services/context/recorderWindow.provider.tsx: -------------------------------------------------------------------------------- 1 | import { type BaseComponentProps, type RecordWindowData } from '@/models' 2 | import { useRecorderWindow } from '../../hooks' 3 | import { recorderWindowContext } from './recorderWindow.context' 4 | 5 | interface RecorderWindowProviderProps extends BaseComponentProps { 6 | windowData: RecordWindowData 7 | } 8 | 9 | export const RecorderWindowProvider: React.FC = ({ children, windowData }) => { 10 | const values = useRecorderWindow(windowData) 11 | 12 | return 13 | {children} 14 | 15 | } 16 | 17 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/SignUp/signUp.tsx: -------------------------------------------------------------------------------- 1 | import { Anchor, P, Title } from '@/components/ui' 2 | import { DividedLayout } from '@/layouts' 3 | import { frontRoutes } from '@/routing' 4 | import { SignUpForm } from './components' 5 | 6 | const SignUpPage = () => { 7 | return ( 8 | 9 |
10 | Sign up 11 |

12 | Do you have an account? Sign In 13 |

14 |
15 |
16 | 17 |
18 |
19 | ) 20 | } 21 | 22 | export default SignUpPage 23 | -------------------------------------------------------------------------------- /apps/web-app/src/utils/others/local-storage.util.ts: -------------------------------------------------------------------------------- 1 | export type StorageType = 'sessionStorage' | 'localStorage' 2 | 3 | export const setToStorage = (key: string, data: string, type: StorageType) => { 4 | window[type].setItem(key, data) 5 | } 6 | 7 | export const getFromStorageObject = (key: string, type: StorageType, dataType: 'object' | 'string' = 'object'): T | null => { 8 | const plainData = window[type].getItem(key) 9 | 10 | if (plainData == null) return null 11 | 12 | if (dataType === 'object') { 13 | return JSON.parse(plainData) as T 14 | } 15 | 16 | return plainData as T 17 | } 18 | 19 | export const removeFromStorage = (key: string, type: StorageType) => { 20 | window[type].removeItem(key) 21 | } 22 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/RecordingWindow/services/context/recorder.provider.tsx: -------------------------------------------------------------------------------- 1 | import { type BaseComponentProps, type WindowData } from '@/models' 2 | import { useRecorder } from '../../hooks' 3 | import { recorderContext } from './recorder.context' 4 | 5 | interface RecorderProviderProps extends BaseComponentProps { 6 | windowData: WindowData 7 | } 8 | 9 | export const RecorderProvider: React.FC = ({ children, windowData }) => { 10 | const values = useRecorder(windowData?.recordingCoreType ?? 'screen') 11 | 12 | return ( 13 | 16 | {children} 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /apps/back/src/main.ts: -------------------------------------------------------------------------------- 1 | import { ValidationPipe } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import helmet from 'helmet'; 4 | import { AppModule } from './app/app.module'; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule, { 8 | cors: { 9 | allowedHeaders: ['Content-Type'], 10 | origin: [process.env.WEB_APP_ORIGIN], 11 | methods: ['GET', 'POST', 'PUT', 'DELETE'], 12 | }, 13 | }); 14 | 15 | app.use(helmet()); 16 | 17 | app.useGlobalPipes( 18 | new ValidationPipe({ 19 | whitelist: true, 20 | forbidNonWhitelisted: true, 21 | }), 22 | ); 23 | 24 | app.setGlobalPrefix('api'); 25 | 26 | await app.listen(3000); 27 | } 28 | bootstrap(); 29 | -------------------------------------------------------------------------------- /apps/back/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from '../src/app/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 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/RecordingWindow/RecordingWindow.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { type RecordWindowData } from '@/models' 3 | import { RecordingWindowWrap } from './components' 4 | import { RecorderProvider, RecorderWindowProvider } from './services/context' 5 | 6 | interface RecordWindowProps { 7 | windowData: RecordWindowData 8 | } 9 | 10 | export const RecordingWindow: React.FC = ({ windowData }) => { 11 | return ( 12 | 13 | 14 | 17 | 18 | 19 | ) 20 | } 21 | 22 | export default RecordingWindow 23 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/RecordingWindow/components/RecordVideo.tsx: -------------------------------------------------------------------------------- 1 | import { type MutableRefObject } from 'react' 2 | import { useRecorderContext, useRecorderWindowContext } from '../services/context' 3 | 4 | export const RecordVideo = () => { 5 | const { videoSourceRef } = useRecorderContext() 6 | const { isDisplayingVideo } = useRecorderWindowContext() 7 | 8 | return ( 9 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/RecordingWindow/services/context/recorderWindow.context.ts: -------------------------------------------------------------------------------- 1 | import { type RecordWindowData } from '@/models' 2 | import { createContext, useContext } from 'react' 3 | 4 | interface RecorderWindowContext { 5 | isDisplayingVideo: boolean 6 | toggleVideoVisibility: (newState: boolean) => void 7 | getWindowData: () => RecordWindowData | undefined 8 | } 9 | 10 | const defaultValues: RecorderWindowContext = { 11 | isDisplayingVideo: false, 12 | toggleVideoVisibility: () => {}, 13 | getWindowData: () => { return undefined } 14 | } 15 | 16 | export const recorderWindowContext = createContext(defaultValues) 17 | 18 | export const useRecorderWindowContext = () => useContext(recorderWindowContext) 19 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/TextField/components/TextFieldLoader.tsx: -------------------------------------------------------------------------------- 1 | import { BasicLoader } from '@/assets/Loaders' 2 | import { type BaseComponentProps, type FC } from '@/models' 3 | import clsx from 'clsx' 4 | 5 | interface TextFieldLoaderProps extends BaseComponentProps { 6 | active?: boolean 7 | } 8 | 9 | export const TextFieldLoader: FC = ({ active = false, ...props }) => { 10 | const { className, ...loaderProps } = props 11 | 12 | return ( 13 |
16 | { 17 | active && 18 | } 19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /apps/web-app/src/routing/routes.model.ts: -------------------------------------------------------------------------------- 1 | import { type CheckUserTypeParams } from '@/models' 2 | import { getEnv } from '@/services/others' 3 | 4 | export const frontRoutes = { 5 | home: '/', 6 | signIn: '/sign-in', 7 | signUp: '/sign-up', 8 | emailVerification: { 9 | route: '/email-verification', 10 | paramName: 'verificationToken' 11 | } 12 | } 13 | 14 | export const backRoutes = (() => { 15 | const baseRoute = getEnv('VITE_BACK_HOST') 16 | return ({ 17 | home: baseRoute as string, 18 | signIn: baseRoute + 'auth/signIn', 19 | signUp: baseRoute + 'auth/signUp', 20 | emailVerification: (token: string) => baseRoute + 'email-verification/' + token, 21 | checkUserData: (key: CheckUserTypeParams) => baseRoute + `auth/check/${key}/` 22 | }) 23 | })() 24 | -------------------------------------------------------------------------------- /apps/back/.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', 'postgres'], 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 | -------------------------------------------------------------------------------- /apps/web-app/src/config/axios.config.ts: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from '@/models' 2 | import { toast } from '@/utils/others' 3 | import axios from 'axios' 4 | 5 | export const setUpAxiosConfig = () => { 6 | axios.interceptors.response.use((config) => { console.log('interceptors'); return config }, async (error) => { 7 | const errorStatusCode = error?.response?.data?.statusCode 8 | const errorCode = error?.code 9 | 10 | const isAUnknownStatusCode = !Object.values(StatusCodes).some(statusCode => statusCode === errorStatusCode) 11 | const wasCanceled = errorCode === 'ERR_CANCELED' 12 | 13 | if (isAUnknownStatusCode && !wasCanceled) { 14 | console.log(error) 15 | toast.message('Something went wrong', 'error') 16 | } 17 | 18 | return Promise.reject(error) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | branches: [ main ] 7 | jobs: 8 | test: 9 | timeout-minutes: 60 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: 18 16 | - name: Install dependencies 17 | run: npm install -g pnpm && pnpm install 18 | - name: Install Playwright Browsers 19 | run: pnpm exec playwright install --with-deps 20 | - name: Run Playwright tests 21 | run: pnpm exec playwright test 22 | - uses: actions/upload-artifact@v3 23 | if: always() 24 | with: 25 | name: playwright-report 26 | path: playwright-report/ 27 | retention-days: 30 28 | -------------------------------------------------------------------------------- /apps/web-app/src/assets/Loaders/BasicLoader/BasicLoader.tsx: -------------------------------------------------------------------------------- 1 | import { type BaseComponentType } from '@/models' 2 | import clsx from 'clsx' 3 | 4 | export const BasicLoader: BaseComponentType = ({ className, ...props }) => { 5 | return ( 6 |
16 | 19 | Loading... 20 | 21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/P/P.tsx: -------------------------------------------------------------------------------- 1 | import { type BaseComponentProps, type FC } from '@/models' 2 | import clsx from 'clsx' 3 | 4 | interface PProps extends BaseComponentProps { 5 | level?: 'primary' | 'secondary' 6 | color2?: 'error' | 'warning' | 'base' 7 | } 8 | 9 | export const P: FC = ({ className, level = 'primary', color2 = 'base', ...props }) => { 10 | return ( 11 |

23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/Sheet.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, type HTMLAttributes } from 'react' 2 | 3 | interface SheetProps extends HTMLAttributes { 4 | as?: keyof JSX.IntrinsicElements 5 | } 6 | 7 | export type SheetPropsPartial = Partial 8 | 9 | export const Sheet = forwardRef( 10 | ({ as, children, className, ...props }, ref) => { 11 | const Element = as ?? 'div' 12 | return ( 13 | // @ts-expect-error TYPE ERROR EXPECTED 14 | 24 | {children} 25 | 26 | ) 27 | } 28 | ) 29 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/RecordingWindow/services/context/recorder.context.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react' 2 | import { type RecordingStatus } from '../../models/recorder.model' 3 | 4 | interface RecorderContext { 5 | toggleRecordingStatus: (newStatus: RecordingStatus) => Promise 6 | recordingStatus: RecordingStatus 7 | error: string | undefined 8 | videoSourceRef: React.MutableRefObject | undefined 9 | } 10 | 11 | const defaultRecorderContext: RecorderContext = { 12 | toggleRecordingStatus: async () => {}, 13 | recordingStatus: 'off', 14 | error: undefined, 15 | videoSourceRef: undefined 16 | } 17 | 18 | export const recorderContext = createContext(defaultRecorderContext) 19 | 20 | export const useRecorderContext = () => useContext(recorderContext) 21 | -------------------------------------------------------------------------------- /apps/back/src/config/env.config.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from 'joi'; 2 | 3 | export const SchemaEnv = Joi.object({ 4 | DB_NAME: Joi.string().min(3).required(), 5 | DB_USERNAME: Joi.string().min(3).required(), 6 | DB_PASSWORD: Joi.string().min(9).required(), 7 | DB_HOST: Joi.string().required(), 8 | DB_PORT: Joi.number().default(5432).required(), 9 | DB_SSL: Joi.boolean().default('true'), 10 | AUTH_SECRET: Joi.string().min(20).required(), 11 | AUTH_EXPIRES: Joi.string().min(2).required(), 12 | AUTH_RESEND_API_KEY: Joi.string().min(10).required(), 13 | AUTH_RESEND_TOKEN_SECRET: Joi.string().min(10).required(), 14 | AUTH_RESEND_TOKEN_EXPIRATION: Joi.string().min(2).required(), 15 | RESEND_FROM_SUBJECT: Joi.string().min(2).required(), 16 | RESEND_FROM_EMAIL: Joi.string().min(4).required(), 17 | WEB_APP_ORIGIN: Joi.string().uri().required(), 18 | }).required(); 19 | -------------------------------------------------------------------------------- /apps/web-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2023", "ES2020", "DOM", "DOM.Iterable", "ES2023.Array", "ES2023.Collection"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "baseUrl": "./", 23 | "paths": { 24 | "@/*": ["./src/*"] 25 | } 26 | }, 27 | "include": ["src", "playwright"], 28 | "references": [{ "path": "./tsconfig.node.json" }] 29 | } 30 | -------------------------------------------------------------------------------- /apps/back/src/app/auth/dto/sign-up.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsEmail, 3 | IsString, 4 | Matches, 5 | MaxLength, 6 | MinLength, 7 | } from 'class-validator'; 8 | 9 | export class SignUpDto { 10 | @IsEmail( 11 | { 12 | host_whitelist: ['gmail.com'], 13 | }, 14 | { 15 | message: 'email must be an email gmail.', 16 | }, 17 | ) 18 | @MaxLength(50) 19 | email: string; 20 | 21 | @IsString() 22 | @MinLength(4) 23 | @MaxLength(20) 24 | @Matches(/^[a-zA-Z0-9_-]+$/, { 25 | message: 'The username should not contain symbols or operators.', 26 | }) 27 | username: string; 28 | 29 | @IsString() 30 | @MinLength(4) 31 | @MaxLength(40) 32 | @Matches(/(?:(?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, { 33 | message: 34 | 'The password must have a Uppercase, lowercase letter and a number', 35 | }) 36 | password: string; 37 | } 38 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/TextField/TextField.tsx: -------------------------------------------------------------------------------- 1 | import { type BaseComponentProps } from '@/models' 2 | import clsx from 'clsx' 3 | import { TextFieldError, TextFieldLabel, TextFieldLoader } from './components' 4 | 5 | interface TextFieldProps extends BaseComponentProps { 6 | direction?: 'column' | 'row' 7 | } 8 | 9 | export const TextField = ({ children, className, direction = 'column' }: TextFieldProps) => { 10 | return ( 11 | 21 | ) 22 | } 23 | 24 | TextField.Label = TextFieldLabel 25 | TextField.Error = TextFieldError 26 | TextField.Loader = TextFieldLoader 27 | -------------------------------------------------------------------------------- /apps/web-app/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | 'standard-with-typescript' 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parser: '@typescript-eslint/parser', 12 | plugins: ['react-refresh'], 13 | rules: { 14 | 'react-refresh/only-export-components': [ 15 | 'warn', 16 | { allowConstantExport: true }, 17 | ], 18 | '@typescript-eslint/explicit-function-return-type': 'off', 19 | 'no-multiple-empty-lines': 'off', 20 | '@typescript-eslint/return-await': 'off', 21 | '@typescript-eslint/unbound-method': 'off', 22 | '@typescript-eslint/no-misused-promises': 'off' 23 | }, 24 | ignorePatterns: ["vite.config.ts", "vite-env.d.ts", "playwright.config.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /apps/web-app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Analytics } from '@vercel/analytics/react' 2 | import { Toaster } from 'sonner' 3 | import { Routing } from './routing' 4 | 5 | function App () { 6 | return ( 7 | <> 8 | 9 | 23 | 24 | 25 | ) 26 | } 27 | 28 | export default App 29 | -------------------------------------------------------------------------------- /apps/web-app/src/layouts/DividedLayout/DividedLayout.tsx: -------------------------------------------------------------------------------- 1 | import { AppLogo } from '@/components/feature' 2 | import { type BaseComponentType } from '@/models' 3 | import clsx from 'clsx' 4 | 5 | export const DividedLayout: BaseComponentType = ({ children, className, ...props }) => { 6 | return ( 7 |

10 |
19 | {children} 20 |
21 |
22 | 23 |
24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/SignIn/SignIn.tsx: -------------------------------------------------------------------------------- 1 | import { Anchor, P, Title } from '@/components/ui' 2 | import { DividedLayout } from '@/layouts' 3 | import { frontRoutes } from '@/routing' 4 | import { SignInForm } from './components' 5 | 6 | const SignInPage = () => { 7 | return ( 8 | 9 |
10 | Sign in 11 |

12 | Don't you have an account? Sign Up 13 |

14 |
15 |
16 | 17 |
18 |
21 |

Lost your password? Recover

22 |
23 |
24 | ) 25 | } 26 | 27 | export default SignInPage 28 | -------------------------------------------------------------------------------- /apps/web-app/src/models/user.model.ts: -------------------------------------------------------------------------------- 1 | import { type useLocalAuth } from '@/hooks/private' 2 | import { type UUID } from 'crypto' 3 | 4 | export interface User { 5 | id: UUID 6 | email: string 7 | username: string 8 | password: string 9 | isActive: boolean 10 | roles: string[] 11 | } 12 | 13 | export interface PublicUser extends Omit { } 14 | 15 | export type AuthContext = ReturnType 16 | 17 | export const testUserRole = 'default' 18 | 19 | export const defaultUserValue = { email: 'user@gmail.com', id: crypto.randomUUID(), isActive: true, roles: [testUserRole], username: 'default' } 20 | 21 | export const defaultUserValues: AuthContext = { 22 | user: defaultUserValue, 23 | signIn: async () => { return new Promise(() => {}) }, 24 | signUp: async () => { return new Promise(() => {}) }, 25 | signOut: () => {}, 26 | verifyEmail: async () => { return new Promise(() => {}) } 27 | } 28 | -------------------------------------------------------------------------------- /apps/back/.env.template: -------------------------------------------------------------------------------- 1 | # DB CONFIG 2 | DB_NAME="DB_NAME" # POSTGRES DB NAME 3 | DB_USERNAME="postgres" # POSTGRES DB USERNAME 4 | DB_PASSWORD="password123" # POSTGRES DB PASSWORD 5 | DB_HOST="localhost" # HOST 6 | DB_PORT=5432 # POSTGRES DB PORT 7 | DB_SSL=false 8 | 9 | # AUTH SESSION CONFIG 10 | AUTH_SECRET="SECRET_SHHHHHH" # AUTH JWT SECRET (IT OCULD BE SOMETHING RANDOM) 11 | AUTH_EXPIRES="2 days" # JWT EXPIRES TIME 12 | 13 | # AUTH EMAIL PROVIDER 14 | AUTH_RESEND_API_KEY="your resend token" 15 | AUTH_RESEND_TOKEN_SECRET="SECRET_SHHHHHH" # AUTH JWT EMAIL SECRET (IT OCULD BE SOMETHING RANDOM) 16 | AUTH_RESEND_TOKEN_EXPIRATION="1 hour" # JWT EXPIRES TIME 17 | 18 | RESEND_FROM_SUBJECT="jes015" # Jes015 This would be the first part "Jes015" 19 | RESEND_FROM_EMAIL="" # Jes015 This would be the other part "" 20 | 21 | # APP CONFIG 22 | WEB_APP_ORIGIN='http://localhost:4321' -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/Select/components/SelectItem.tsx: -------------------------------------------------------------------------------- 1 | import { CheckIcon } from '@radix-ui/react-icons' 2 | import { SelectItem as DefaultSelectItem, SelectItemIndicator, SelectItemText, type SelectItemProps } from '@radix-ui/react-select' 3 | import { forwardRef } from 'react' 4 | 5 | export const SelectItem = forwardRef( 6 | ({ children, className, ...props }, forwardedRef) => { 7 | return ( 8 | } 12 | > 13 | {children} 14 | 15 | 16 | 17 | 18 | ) 19 | }) 20 | -------------------------------------------------------------------------------- /apps/web-app/src/models/window.model.ts: -------------------------------------------------------------------------------- 1 | import { type RecordingType } from '@/utils/others' 2 | import { type UUID } from 'crypto' 3 | 4 | export const CWindowType = { 5 | record: 'Record', 6 | watchRecord: 'Watch Record', 7 | downloadRecord: 'Download Record' 8 | } as const 9 | 10 | export type TWindowType = typeof CWindowType[keyof typeof CWindowType] 11 | 12 | export interface WindowData { 13 | id: UUID 14 | name: string 15 | type: TWindowType 16 | recordingCoreType?: RecordingType 17 | zIndex?: number 18 | oneOnly?: true 19 | } 20 | 21 | export interface WatchRecordingWindowData extends WindowData { 22 | videoAndAudioBlob: Blob | null 23 | type: 'Watch Record' 24 | } 25 | 26 | export interface RecordWindowData extends WindowData { 27 | type: 'Record' 28 | } 29 | 30 | export interface DownloadRecordingWindowData extends WindowData { 31 | videoAndAudioBlob: Blob | null 32 | type: 'Download Record' 33 | } 34 | 35 | export type WindowTypes = RecordWindowData | WatchRecordingWindowData | DownloadRecordingWindowData 36 | 37 | export type WindowDataArray = WindowData[] 38 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/DropdownMenu/components/Item.tsx: -------------------------------------------------------------------------------- 1 | import { Item as DefaultItem, type DropdownMenuItemProps } from '@radix-ui/react-dropdown-menu' 2 | 3 | interface ItemProps extends DropdownMenuItemProps { 4 | clickable?: boolean 5 | } 6 | 7 | export type ItemPropsPartial = Partial 8 | 9 | export const Item: React.FC = ({ className, children, clickable = true, ...props }) => { 10 | if (clickable) { 11 | return ( 12 | 21 | {children} 22 | 23 | ) 24 | } 25 | 26 | return ( 27 |
35 | {children} 36 |
37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /apps/web-app/src/components/feature/FormTextField/FormTextField.tsx: -------------------------------------------------------------------------------- 1 | import { Input, TextField, type InputProps } from '@/components/ui' 2 | import { type FC } from '@/models' 3 | import clsx from 'clsx' 4 | import { type UseFormRegisterReturn } from 'react-hook-form' 5 | 6 | interface FormTextFieldProps { 7 | label: React.ReactNode 8 | error: string | undefined 9 | register: UseFormRegisterReturn 10 | inputProps: InputProps 11 | } 12 | 13 | export const FormTextField: FC = ({ error, register, label, inputProps }) => { 14 | return ( 15 | 16 | {label} 17 | 27 | {error} 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/Input.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, type DetailedHTMLProps, type InputHTMLAttributes } from 'react' 2 | 3 | export interface InputProps extends DetailedHTMLProps, HTMLInputElement> { 4 | size2?: 'sm' | 'base' | 'resizable' | 'lg' 5 | } 6 | 7 | export const Input = forwardRef( 8 | ({ className, readOnly = false, size2, ...props }, ref) => { 9 | return ( 10 | 24 | ) 25 | } 26 | ) 27 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/DropdownMenu/DropdownMenu.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui' 2 | import { type BaseComponentProps } from '@/models' 3 | import { Portal, Root, Trigger } from '@radix-ui/react-dropdown-menu' 4 | import { Content, Item } from './components' 5 | 6 | interface DropdownMenuProps extends BaseComponentProps { 7 | triggerContent: React.ReactNode 8 | triggerClassName?: string 9 | } 10 | 11 | export const DropdownMenu = ({ children, triggerContent, triggerClassName, id }: DropdownMenuProps) => { 12 | return ( 13 | 14 | 19 | 25 | 26 | 27 | 28 | {children} 29 | 30 | 31 | ) 32 | } 33 | 34 | DropdownMenu.Item = Item 35 | DropdownMenu.Content = Content 36 | 37 | export default DropdownMenu 38 | -------------------------------------------------------------------------------- /apps/web-app/src/utils/others/download-recording.util.ts: -------------------------------------------------------------------------------- 1 | /* import { UtilFfmpeg } from '@/utils/others' */ 2 | 3 | interface DownloadRecordingParams { 4 | blob: Blob 5 | title: string 6 | format: 'mp4' | 'mp3' | 'webm' 7 | } 8 | 9 | 10 | export class UtilDownloadRecording { 11 | /* public async downloadByMpTech (recordingTitle: string, recordingBlob: Blob, toFormat: 'mp4' | 'mp3') { 12 | const ffmpegInstance = new UtilFfmpeg() 13 | await ffmpegInstance.loadFfmpeg() 14 | const blob = await ffmpegInstance.transcode(recordingBlob, toFormat) 15 | this.download({ blob, title: recordingTitle, format: toFormat }) 16 | } */ 17 | 18 | public downloadByWebTechs (recordingTitle: string, recordingBlob: Blob, toFormat: 'webm') { 19 | this.download({ blob: recordingBlob, title: recordingTitle, format: toFormat }) 20 | } 21 | 22 | private download ({ blob, title, format }: DownloadRecordingParams) { 23 | const recordingURL = URL.createObjectURL(blob) 24 | 25 | const anchorElement = document.createElement('a') 26 | anchorElement.href = recordingURL 27 | anchorElement.download = `${title}.${format}` 28 | anchorElement.click() 29 | 30 | URL.revokeObjectURL(recordingURL) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/web-app/src/routing/Routing.tsx: -------------------------------------------------------------------------------- 1 | import { setUpAxiosConfig } from '@/config' 2 | import EmailVerificationPage from '@/pages/EmailVerification/EmailVerification' 3 | import HomePage from '@/pages/Home/Home' 4 | import RootPage from '@/pages/Root/Root' 5 | import SignInPage from '@/pages/SignIn/SignIn' 6 | import SignUpPage from '@/pages/SignUp/signUp' 7 | import { RouterProvider, createBrowserRouter } from 'react-router-dom' 8 | import { AuthGuard } from './guards' 9 | 10 | setUpAxiosConfig() 11 | 12 | const router = createBrowserRouter([ 13 | { 14 | element: , 15 | children: [ 16 | { 17 | path: '/', 18 | element: 19 | }, 20 | { 21 | element: , 22 | children: [ 23 | { 24 | path: 'sign-in', 25 | element: 26 | }, 27 | { 28 | path: 'sign-up', 29 | element: 30 | } 31 | ] 32 | }, 33 | { 34 | path: 'email-verification/:verificationToken', 35 | element: 36 | } 37 | ] 38 | } 39 | ]) 40 | 41 | export const Routing = () => 42 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/DropdownMenu/components/Content.tsx: -------------------------------------------------------------------------------- 1 | import { Sheet } from '@/components/ui' 2 | import { type BaseComponentProps, type FC } from '@/models' 3 | import { Content as DefaultContent } from '@radix-ui/react-dropdown-menu' 4 | import clsx from 'clsx' 5 | import { forwardRef } from 'react' 6 | 7 | interface ContentProps extends BaseComponentProps { 8 | contentStyles?: 'default' | 'main-menu' 9 | } 10 | 11 | export const Content: FC = forwardRef( 12 | ({ children, contentStyles = 'default' }, ref) => { 13 | return ( 14 | 22 | 30 | {children} 31 | 32 | 33 | ) 34 | } 35 | ) 36 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/CompatibilityWindow/hooks/useCompatibilityWindow.ts: -------------------------------------------------------------------------------- 1 | import { showDriver } from '@/utils/others' 2 | import { useEffect, useState } from 'react' 3 | import { defaultCompatibilityValues, type CompatibilityType } from '../models' 4 | 5 | export const useCompatibilityWindow = () => { 6 | const [showWindow, setShowWindow] = useState(false) 7 | const [compatibility, setCompatibility] = useState(defaultCompatibilityValues) 8 | 9 | useEffect(() => { 10 | const newCompatibilityValues: CompatibilityType = { 11 | 'Screen Recording': navigator.mediaDevices?.getDisplayMedia != null, 12 | 'Webcam Recording': navigator.mediaDevices?.getUserMedia != null, 13 | 'Take Screenshot': navigator.mediaDevices?.getDisplayMedia != null && VideoFrame != null 14 | } 15 | setCompatibility(newCompatibilityValues) 16 | 17 | const shouldShowWindow = Object.values(newCompatibilityValues).some(value => !value) 18 | 19 | if (!shouldShowWindow) { 20 | showDriver() 21 | } 22 | 23 | setShowWindow(shouldShowWindow) 24 | }, []) 25 | 26 | const toggleShowWindowStatus = () => { 27 | setShowWindow(prev => !prev) 28 | } 29 | 30 | return { compatibility, showWindow, toggleShowWindowStatus } 31 | } 32 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/SwitchInput.tsx: -------------------------------------------------------------------------------- 1 | import { type BaseComponentProps } from '@/models' 2 | import { Root, Thumb } from '@radix-ui/react-switch' 3 | 4 | interface SwitchInputProps extends Omit { 5 | onChange?: (e: boolean) => void 6 | defaultChecked?: boolean 7 | thumbClassName?: string 8 | } 9 | 10 | export const SwitchInput = ({ className, onChange, defaultChecked = false, thumbClassName }: SwitchInputProps) => { 11 | return ( 12 | 22 | 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/WatchRecordingWindow/hooks/useWatchRecording.ts: -------------------------------------------------------------------------------- 1 | import { CWindowType, type WatchRecordingWindowData } from '@/models' 2 | import { useWindowSystemStore } from '@/services/store/zustand' 3 | import { useEffect, useRef } from 'react' 4 | 5 | export const useWatchRecording = (windowData: WatchRecordingWindowData) => { 6 | const videoElementRef = useRef(null) 7 | const [setError, addWindow] = useWindowSystemStore(state => [state.setError, state.addWindow]) 8 | 9 | useEffect(() => { 10 | if (videoElementRef.current == null || windowData.videoAndAudioBlob == null) return 11 | const videoURL = URL.createObjectURL(windowData.videoAndAudioBlob) 12 | 13 | videoElementRef.current.src = videoURL 14 | 15 | return () => { 16 | URL.revokeObjectURL(videoURL) 17 | } 18 | // eslint-disable-next-line react-hooks/exhaustive-deps 19 | }, []) 20 | 21 | const downloadRecording = () => { 22 | if (windowData.videoAndAudioBlob === null) { 23 | setError('No source found') 24 | return 25 | } 26 | 27 | addWindow({ id: crypto.randomUUID(), name: windowData.name, type: CWindowType.downloadRecord, videoAndAudioBlob: windowData.videoAndAudioBlob }) 28 | } 29 | 30 | return { videoElementRef, downloadRecording } 31 | } 32 | -------------------------------------------------------------------------------- /apps/web-app/src/components/feature/AppLogo.tsx: -------------------------------------------------------------------------------- 1 | import { IconGithub } from '@/assets/Icons' 2 | import { Title } from '@/components/ui' 3 | import { type BaseComponentType } from '@/models' 4 | import clsx from 'clsx' 5 | 6 | export const AppLogo: BaseComponentType = ({ className }) => { 7 | return ( 8 |
16 |
17 | 18 | WebCapture 19 | 20 |
21 |
22 | 28 | 29 | 30 |
31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/DownloadRecordingWindow/hooks/useDownloadRecording.ts: -------------------------------------------------------------------------------- 1 | import { type DownloadRecordingWindowData } from '@/models' 2 | import { useWindowSystemStore } from '@/services/store/zustand' 3 | import { UtilDownloadRecording } from '@/utils/others' 4 | import { useState } from 'react' 5 | 6 | interface DownloadRecordingParams { 7 | windowData: DownloadRecordingWindowData 8 | } 9 | export const useDownloadRecording = ({ windowData: { videoAndAudioBlob } }: DownloadRecordingParams) => { 10 | const [setError] = useWindowSystemStore(state => [state.setError]) 11 | const [loading, setLoading] = useState(false) 12 | 13 | const downloadRecording = async (recordingTitle: string, format: 'mp4' | 'mp3' | 'webm') => { 14 | if (videoAndAudioBlob == null) return 15 | 16 | setLoading(true) 17 | 18 | const utilDownloadRecording = new UtilDownloadRecording() 19 | 20 | if (format === 'webm') { 21 | utilDownloadRecording.downloadByWebTechs(recordingTitle, videoAndAudioBlob, format) 22 | } else if (format === 'mp3' || format === 'mp4') { 23 | /* await utilDownloadRecording.downloadByMpTech(recordingTitle, videoAndAudioBlob, format) */ 24 | setError('We will add support for mp4 and mp3 soon') 25 | } 26 | 27 | setLoading(false) 28 | } 29 | return { loading, downloadRecording } 30 | } 31 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/RecordingWindow/components/RecordingWindowDropdownMenu.tsx: -------------------------------------------------------------------------------- 1 | import { DropdownMenu, SwitchInput, TextField } from '@/components/ui' 2 | import { type BaseComponentType } from '@/models' 3 | import { HamburgerMenuIcon } from '@radix-ui/react-icons' 4 | import { useRecorderWindowContext } from '../services/context' 5 | 6 | export const RecordingWindowDropdownMenu: BaseComponentType = () => { 7 | const { isDisplayingVideo, toggleVideoVisibility } = useRecorderWindowContext() 8 | 9 | const handleOnClickForDisplayVideo = (newValue: boolean) => { 10 | toggleVideoVisibility(newValue) 11 | } 12 | 13 | return ( 14 | }> 15 | 16 | 20 | 24 | 25 | Display Video 26 | 27 | 31 | 32 | 33 | 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /apps/back/src/app/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, forwardRef } from '@nestjs/common'; 2 | import { ConfigModule, ConfigService } from '@nestjs/config'; 3 | import { JwtModule } from '@nestjs/jwt'; 4 | import { PassportModule } from '@nestjs/passport'; 5 | import { EmailVerificationModule } from '../email-verification/email-verification.module'; 6 | import { UserModule } from '../user/user.module'; 7 | import { AuthController } from './auth.controller'; 8 | import { AuthService } from './auth.service'; 9 | import { JwtStrategy } from './strategies'; 10 | 11 | @Module({ 12 | imports: [ 13 | UserModule, 14 | PassportModule.register({ defaultStrategy: 'jwt' }), 15 | JwtModule.registerAsync({ 16 | imports: [ConfigModule], 17 | inject: [ConfigService], 18 | useFactory(configService: ConfigService) { 19 | const secret = configService.get('AUTH_SECRET'); 20 | const tokenDuration = configService.get('AUTH_EXPIRES'); 21 | 22 | return { 23 | secret, 24 | signOptions: { 25 | expiresIn: tokenDuration, 26 | }, 27 | }; 28 | }, 29 | }), 30 | forwardRef(() => EmailVerificationModule), 31 | ], 32 | controllers: [AuthController], 33 | providers: [AuthService, JwtStrategy], 34 | exports: [AuthService, PassportModule, JwtModule, JwtStrategy], 35 | }) 36 | export class AuthModule {} 37 | -------------------------------------------------------------------------------- /apps/back/src/app/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post } from '@nestjs/common'; 2 | import { Throttle, days, hours } from '@nestjs/throttler'; 3 | import { AuthService } from './auth.service'; 4 | import { Auth } from './decorators'; 5 | import { CheckEmailDTO, CheckUsernameDTO, SignInDto, SignUpDto } from './dto'; 6 | 7 | @Controller('auth') 8 | @Throttle({ default: { limit: 5, ttl: days(1) } }) 9 | export class AuthController { 10 | constructor(private readonly authService: AuthService) {} 11 | 12 | @Post('signIn') 13 | @Throttle({ default: { limit: 20, ttl: hours(5) } }) 14 | signIn(@Body() signInDto: SignInDto) { 15 | return this.authService.signIn(signInDto); 16 | } 17 | 18 | @Post('signUp') 19 | signUp(@Body() signUpDto: SignUpDto) { 20 | return this.authService.sendUserEmailValidation(signUpDto); 21 | } 22 | 23 | @Post('check/username') 24 | @Throttle({ default: { limit: 300, ttl: days(1) } }) 25 | checkUsername(@Body() checkUsernameDTO: CheckUsernameDTO) { 26 | return this.authService.checkUser('username', checkUsernameDTO.username); 27 | } 28 | 29 | @Post('check/email') 30 | @Throttle({ default: { limit: 300, ttl: days(1) } }) 31 | checkEmail(@Body() checkEmailDTO: CheckEmailDTO) { 32 | return this.authService.checkUser('email', checkEmailDTO.email); 33 | } 34 | 35 | @Auth('admin') 36 | @Get('private') 37 | findAll() { 38 | return 'private route prr'; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/Button.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import { forwardRef, type ButtonHTMLAttributes } from 'react' 3 | 4 | interface ButtonProps extends ButtonHTMLAttributes { 5 | size?: 'sm' | 'base' | 'resizable' | 'xl' 6 | color?: 'default' | 'light' 7 | } 8 | 9 | export const Button: React.FC = forwardRef( 10 | ({ children, className, disabled, size = 'resizable', color = 'default', ...props }, ref) => { 11 | return ( 12 | 30 | ) 31 | } 32 | ) 33 | 34 | Button.displayName = 'Button' 35 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/SignUp/components/FormTextFieldValidateUserData/FormTextFieldValidateUserData.tsx: -------------------------------------------------------------------------------- 1 | import { Input, TextField, type InputProps } from '@/components/ui' 2 | import { type CheckUserTypeParams, type FC } from '@/models' 3 | import clsx from 'clsx' 4 | import { type UseFormRegisterReturn } from 'react-hook-form' 5 | import { useFormTextFieldValidateUserData } from './hooks' 6 | 7 | interface AsyncInputProps { 8 | userKey: CheckUserTypeParams 9 | label: React.ReactNode 10 | error: string | undefined 11 | register: UseFormRegisterReturn 12 | inputProps: InputProps 13 | } 14 | 15 | export const FormTextFieldValidateUserData: FC = ({ userKey, inputProps, error, label, register }) => { 16 | const inputName = register.name 17 | 18 | const { loading } = useFormTextFieldValidateUserData(inputName, userKey) 19 | 20 | return ( 21 | 22 | {label} 23 |
24 | 34 | 35 |
36 | {error} 37 |
38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /apps/web-app/public/stopwatch.worker.js: -------------------------------------------------------------------------------- 1 | class Stopwatch { 2 | constructor (initialTime) { 3 | this.time = initialTime 4 | this.interval = undefined 5 | } 6 | 7 | setTime (time) { 8 | this.time = time 9 | } 10 | 11 | startStopwatch () { 12 | this.interval = setInterval(() => { this.decreaseTime() }, 1000) 13 | } 14 | 15 | stopStopwatch () { 16 | clearInterval(this.interval) 17 | } 18 | 19 | decreaseTime () { 20 | if (this.time.seconds < 59) { 21 | this.time.seconds += 1 22 | } else if (this.time.minutes < 59) { 23 | this.time.seconds = 0 24 | this.time.minutes += 1 25 | } else { 26 | this.time = { seconds: 0, minutes: 0, hours: this.time.hours + 1 } 27 | } 28 | 29 | let message = { time: this.time } 30 | 31 | if (this.time.seconds === 0 && this.time.minutes === 0 && this.time.hours === 0) { 32 | message = { type: 'stop', ...message } 33 | this.stopStopwatch() 34 | } 35 | 36 | self.postMessage(message) 37 | } 38 | } 39 | 40 | const stopwatch = new Stopwatch({ hours: 0, minutes: 0, seconds: 0 }) 41 | 42 | self.onmessage = ({ data }) => { 43 | const dataTyped = data 44 | 45 | if (dataTyped.type === 'start') { 46 | stopwatch.startStopwatch() 47 | } else if (dataTyped.type === 'stop') { 48 | stopwatch.stopStopwatch() 49 | } else if (dataTyped.type === 'setInitial' && dataTyped.time != null) { 50 | stopwatch.setTime(dataTyped.time) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /apps/web-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Web capture - Capture moments 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Creative Commons Attribution-NonCommercial 4.0 International (CC BY-NC 4.0) License 2 | 3 | This work is licensed under a [Creative Commons Attribution-NonCommercial 4.0 International License](https://creativecommons.org/licenses/by-nc/4.0/). 4 | 5 | ## You are free to 6 | 7 | - Share: copy and redistribute the material in any medium or format. 8 | - Adapt: remix, transform, and build upon the material. 9 | 10 | ## Under the following terms 11 | 12 | - Attribution: You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. 13 | 14 | - NonCommercial: You may not use the material for commercial purposes. 15 | 16 | - No additional restrictions: You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits. 17 | 18 | ## Notices 19 | 20 | - You do not have to comply with the license for elements of the material in the public domain or where your use is permitted by an applicable exception or limitation. 21 | 22 | - No warranties are given. The license may not give you all of the permissions necessary for your intended use. For example, other rights such as publicity, privacy, or moral rights may limit how you use the material. 23 | 24 | More about the [Creative Commons Attribution-NonCommercial 4.0 International (CC BY-NC 4.0) License](https://creativecommons.org/licenses/by-nc/4.0/). 25 | 26 | -------------------------------------------------------------------------------- /apps/back/src/app/auth/guards/roles.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CanActivate, 3 | ExecutionContext, 4 | Injectable, 5 | UnauthorizedException, 6 | } from '@nestjs/common'; 7 | import { Reflector } from '@nestjs/core'; 8 | import { BadCredentialsException } from 'src/app/common/exceptions'; 9 | import { User } from 'src/app/user/entities/user.entity'; 10 | import { SetRoles } from '../decorators'; 11 | import { TValidRoleArray } from '../models'; 12 | 13 | @Injectable() 14 | export class RoleGuard implements CanActivate { 15 | constructor(private reflector: Reflector) {} 16 | canActivate(context: ExecutionContext) { 17 | const validRoles = this.reflector.get(SetRoles, context.getHandler()); 18 | 19 | if (!Array.isArray(validRoles) || validRoles?.[0] == null) { 20 | return true; 21 | } 22 | 23 | const request = context.switchToHttp().getRequest(); 24 | const userData = request.user as User; 25 | const userRoles = userData.roles; 26 | 27 | if (userData == null) { 28 | throw new BadCredentialsException(); 29 | } 30 | 31 | const hasValidRoles = (validRoles as TValidRoleArray).every((role) => 32 | userRoles.includes(role), 33 | ); 34 | 35 | if (!hasValidRoles) { 36 | const intlInstance = new Intl.ListFormat('en', { 37 | style: 'long', 38 | type: 'conjunction', 39 | }); 40 | 41 | throw new UnauthorizedException( 42 | `Roles needed for this route: ${intlInstance.format(validRoles)}`, 43 | ); 44 | } 45 | 46 | return true; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /apps/back/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | import { APP_GUARD } from '@nestjs/core'; 4 | import { ScheduleModule } from '@nestjs/schedule'; 5 | import { ThrottlerGuard, ThrottlerModule, seconds } from '@nestjs/throttler'; 6 | import { TypeOrmModule } from '@nestjs/typeorm'; 7 | import { SchemaEnv } from '../config'; 8 | import { AuthModule } from './auth/auth.module'; 9 | import { EmailVerificationModule } from './email-verification/email-verification.module'; 10 | import { UserModule } from './user/user.module'; 11 | 12 | @Module({ 13 | imports: [ 14 | ConfigModule.forRoot({ 15 | isGlobal: true, 16 | validationSchema: SchemaEnv, 17 | }), 18 | TypeOrmModule.forRoot({ 19 | ssl: process.env.DB_SSL === 'true', 20 | type: 'postgres', 21 | database: process.env.DB_NAME, 22 | username: process.env.DB_USERNAME, 23 | password: process.env.DB_PASSWORD, 24 | host: process.env.DB_HOST, 25 | port: Number(process.env.DB_PORT), 26 | autoLoadEntities: true, 27 | synchronize: true, 28 | }), 29 | UserModule, 30 | AuthModule, 31 | EmailVerificationModule, 32 | ScheduleModule.forRoot(), 33 | ThrottlerModule.forRoot([ 34 | { 35 | ttl: seconds(1), 36 | limit: 1, 37 | }, 38 | ]), 39 | ], 40 | controllers: [], 41 | providers: [ 42 | { 43 | provide: APP_GUARD, 44 | useClass: ThrottlerGuard, 45 | }, 46 | ], 47 | }) 48 | export class AppModule {} 49 | -------------------------------------------------------------------------------- /apps/back/src/app/auth/strategies/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { PassportStrategy } from '@nestjs/passport'; 4 | import { InjectRepository } from '@nestjs/typeorm'; 5 | import { ExtractJwt, Strategy } from 'passport-jwt'; 6 | import { BadCredentialsException } from 'src/app/common/exceptions'; 7 | import { User } from 'src/app/user/entities/user.entity'; 8 | import { Repository } from 'typeorm'; 9 | import { JwtPayload } from '../models'; 10 | 11 | @Injectable() 12 | export class JwtStrategy extends PassportStrategy(Strategy) { 13 | constructor( 14 | private readonly configService: ConfigService, 15 | 16 | @InjectRepository(User) 17 | private readonly userRepository: Repository, 18 | ) { 19 | super({ 20 | secretOrKey: configService.get('AUTH_SECRET'), 21 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 22 | }); 23 | } 24 | 25 | async validate(payload: JwtPayload) { 26 | if (payload.id == null) { 27 | throw new BadCredentialsException(`Bad credentials in jwt: id not found`); 28 | } 29 | 30 | const user = await this.userRepository.findOneBy({ id: payload.id }); 31 | 32 | if (user == null) { 33 | throw new BadCredentialsException( 34 | `Bad credentials in jwt: User with id ${payload.id} not found`, 35 | ); 36 | } else if (user.isActive === false) { 37 | throw new BadCredentialsException(`Bad credentials: user inactive`); 38 | } 39 | 40 | return user; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/Home.tsx: -------------------------------------------------------------------------------- 1 | import { CWindowType } from '@/models' 2 | import { MainLayout } from '@/pages/Home/layouts' 3 | import { useWindowSystemStore } from '@/services/store/zustand' 4 | import { Suspense, lazy } from 'react' 5 | 6 | const RecordingWindow = lazy(async () => import('@/pages/Home/components/feature/RecordingWindow/RecordingWindow')) 7 | const WatchRecordingWindow = lazy(async () => import('@/pages/Home/components/feature/WatchRecordingWindow/WatchRecordingWindow')) 8 | const DownloadRecordingWindow = lazy(async () => import('@/pages/Home/components/feature/DownloadRecordingWindow/DownloadRecordingWindow')) 9 | 10 | const windowComponents = { 11 | [CWindowType.record]: RecordingWindow, 12 | [CWindowType.watchRecord]: WatchRecordingWindow, 13 | [CWindowType.downloadRecord]: DownloadRecordingWindow 14 | } as const 15 | 16 | const HomePage = () => { 17 | const { windows } = useWindowSystemStore((state) => ({ windows: state.windows, addWindow: state.addWindow })) 18 | 19 | return ( 20 | 21 |
24 | 25 | { 26 | windows.map(windowData => { 27 | const WindowComponent = windowComponents[windowData.type] 28 | 29 | // @ts-expect-error we need to type this better to avoid to use a ts-expect 30 | return 31 | }) 32 | } 33 | 34 |
35 |
36 | ) 37 | } 38 | 39 | export default HomePage 40 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/Window/Window.tsx: -------------------------------------------------------------------------------- 1 | import { SectionLayout } from '@/layouts' 2 | import { type BaseComponentProps, type WindowData } from '@/models' 3 | import { useWindowSystemStore } from '@/services/store/zustand' 4 | import { useEffect } from 'react' 5 | import { Rnd, type Props as RndProps } from 'react-rnd' 6 | import { WindowContent, WindowHeader } from './components' 7 | 8 | interface WindowProps extends BaseComponentProps { 9 | rndconfig?: RndProps 10 | windowData: WindowData 11 | } 12 | 13 | export const Window = ({ children, className, rndconfig, windowData }: WindowProps) => { 14 | const [superposeAWindow] = useWindowSystemStore(state => [state.superposeAWindow]) 15 | 16 | useEffect(() => { 17 | handleOnClickToSuperposeWindow() 18 | // eslint-disable-next-line react-hooks/exhaustive-deps 19 | }, []) 20 | 21 | const handleOnClickToSuperposeWindow = () => { 22 | superposeAWindow(windowData.id) 23 | } 24 | 25 | return ( 26 | 38 | 46 | {children} 47 | 48 | 49 | ) 50 | } 51 | 52 | Window.Header = WindowHeader 53 | Window.Content = WindowContent 54 | -------------------------------------------------------------------------------- /apps/back/src/app/email-verification/email-verification.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, forwardRef } from '@nestjs/common'; 2 | import { ConfigModule, ConfigService } from '@nestjs/config'; 3 | import { JwtModule } from '@nestjs/jwt'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { ResendModule } from 'nestjs-resend'; 6 | import { AuthModule } from '../auth/auth.module'; 7 | import { EmailVerificationController } from './email-verification.controller'; 8 | import { EmailVerificationService } from './email-verification.service'; 9 | import { UnverifiedEmail } from './entities/unverified-email.entity'; 10 | 11 | @Module({ 12 | imports: [ 13 | TypeOrmModule.forFeature([UnverifiedEmail]), 14 | ResendModule.forAsyncRoot({ 15 | imports: [ConfigModule], 16 | inject: [ConfigService], 17 | useFactory: async (configService: ConfigService) => { 18 | return { 19 | apiKey: configService.get('AUTH_RESEND_API_KEY'), 20 | }; 21 | }, 22 | }), 23 | JwtModule.registerAsync({ 24 | imports: [ConfigModule], 25 | inject: [ConfigService], 26 | useFactory(configService: ConfigService) { 27 | const secret = configService.get('AUTH_RESEND_TOKEN_SECRET'); 28 | const tokenDuration = configService.get( 29 | 'AUTH_RESEND_TOKEN_EXPIRATION', 30 | ); 31 | 32 | return { 33 | secret, 34 | signOptions: { 35 | expiresIn: tokenDuration, 36 | }, 37 | }; 38 | }, 39 | }), 40 | forwardRef(() => AuthModule), 41 | ], 42 | controllers: [EmailVerificationController], 43 | providers: [EmailVerificationService], 44 | exports: [EmailVerificationService], 45 | }) 46 | export class EmailVerificationModule {} 47 | -------------------------------------------------------------------------------- /apps/web-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0 && tsc --noemit", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@ffmpeg/ffmpeg": "^0.12.10", 14 | "@ffmpeg/util": "^0.12.1", 15 | "@fontsource/roboto": "^5.0.8", 16 | "@hookform/resolvers": "^3.3.4", 17 | "@radix-ui/react-dropdown-menu": "^2.0.6", 18 | "@radix-ui/react-icons": "^1.3.0", 19 | "@radix-ui/react-select": "^2.0.0", 20 | "@radix-ui/react-switch": "^1.0.3", 21 | "@vercel/analytics": "^1.1.1", 22 | "axios": "^1.6.7", 23 | "clsx": "^2.1.0", 24 | "driver.js": "^1.3.1", 25 | "react": "^18.2.0", 26 | "react-dom": "^18.2.0", 27 | "react-hook-form": "^7.50.1", 28 | "react-rnd": "^10.4.1", 29 | "react-router-dom": "^6.22.0", 30 | "sonner": "^1.4.3", 31 | "zod": "^3.22.4", 32 | "zustand": "^4.4.7" 33 | }, 34 | "devDependencies": { 35 | "@playwright/test": "^1.41.1", 36 | "@types/node": "^20.10.8", 37 | "@types/react": "^18.2.43", 38 | "@types/react-dom": "^18.2.17", 39 | "@typescript-eslint/eslint-plugin": "^6.14.0", 40 | "@typescript-eslint/parser": "^6.14.0", 41 | "@vitejs/plugin-react-swc": "^3.5.0", 42 | "autoprefixer": "^10.4.16", 43 | "eslint": "^8.55.0", 44 | "eslint-config-standard-with-typescript": "^43.0.0", 45 | "eslint-plugin-react-hooks": "^4.6.0", 46 | "eslint-plugin-react-refresh": "^0.4.5", 47 | "postcss": "^8.4.33", 48 | "tailwindcss": "^3.4.1", 49 | "typescript": "^5.2.2", 50 | "vite": "^5.0.8" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /apps/web-app/src/layouts/SectionLayout/components/SectionLayoutHeader.tsx: -------------------------------------------------------------------------------- 1 | import { Sheet } from '@/components/ui' 2 | import { forwardRef, type HTMLAttributes } from 'react' 3 | 4 | interface SectionLayoutHeaderProps extends HTMLAttributes { 5 | name?: React.ReactNode 6 | icon?: React.ReactNode 7 | rightNode?: React.ReactNode 8 | } 9 | 10 | export type SectionLayoutHeaderPropsPartial = Partial 11 | 12 | export const SectionLayoutHeader = forwardRef( 13 | ({ name, icon, children, className, rightNode, ...props }, ref) => { 14 | return ( 15 | 25 | { 26 | name != null || icon != null 27 | ? ( 28 |
31 |
34 |
35 | {icon} 36 |
37 |
38 | {name} 39 |
40 |
41 |
42 | {rightNode} 43 |
44 |
45 | ) 46 | : children 47 | } 48 |
49 | ) 50 | } 51 | ) 52 | 53 | SectionLayoutHeader.displayName = 'SectionLayoutHeader' 54 | -------------------------------------------------------------------------------- /apps/web-app/src/utils/others/logger.util.ts: -------------------------------------------------------------------------------- 1 | import { ViteEnvironment, type ViteEnvironmentType } from '@/models' 2 | 3 | export class Logger { 4 | private readonly environment: ViteEnvironmentType 5 | 6 | constructor (env: ViteEnvironmentType) { 7 | this.environment = env 8 | this.welcomeMessage() 9 | } 10 | 11 | private welcomeMessage () { 12 | console.log('%c' + 'Screen Capture', 'font-family:Roboto; color:white; font-size:40px; font-weight:bold; background-color: #171717; background-image: url("data:image/svg+xml,%3Csvg width=\'40\' height=\'12\' viewBox=\'0 0 40 12\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cpath d=\'M0 6.172L6.172 0h5.656L0 11.828V6.172zm40 5.656L28.172 0h5.656L40 6.172v5.656zM6.172 12l12-12h3.656l12 12h-5.656L20 3.828 11.828 12H6.172zm12 0L20 10.172 21.828 12h-3.656z\' fill=\'%23262626\' fill-opacity=\'0.3\' fill-rule=\'evenodd\'/%3E%3C/svg%3E"); border-radius: 5px; padding: 20px') 13 | console.log('%c' + 'OPEN SOURCE PROJECT - YOU CAN USE THE CODE ONLY FOR NON-PROFIT PURPOSES', 'font-family:Roboto; color:white; font-size:12px; font-weight:bold; background-color: #171717; border-radius: 2px; padding: 2px') 14 | console.log('%c' + 'Github: https://github.com/Jes015/ScreenCapture', 'font-family:Roboto; color:white; font-size:12px; font-weight:bold; background-color: #171717; border-radius: 2px; padding: 2px') 15 | console.log('%c' + 'Jes015 Portfolio: https://portfolio-three-chi-27.vercel.app/ | Jes015 Blog: https://blog-one-murex.vercel.app/', 'font-family:Roboto; color:white; font-size:8px; font-weight:bold; background-color: #171717; border-radius: 2px; padding: 2px') 16 | } 17 | 18 | public message (message: unknown, title: string, type: 'log' | 'error' = 'log') { 19 | if (this.environment === ViteEnvironment.production) return 20 | 21 | console.group(title) 22 | console[type](message) 23 | console.groupEnd() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/RecordingWindow/components/RecordingWindowWrap.tsx: -------------------------------------------------------------------------------- 1 | import { IconRecording } from '@/assets/Icons' 2 | import { Window } from '@/components/ui' 3 | import { type RecordWindowData } from '@/models' 4 | import { recordingStatusType } from '../models' 5 | import { useRecorderContext } from '../services/context' 6 | import { 7 | RecordControls, 8 | RecordData, 9 | RecordVideo, 10 | RecordingWindowDropdownMenu 11 | } from './' 12 | 13 | interface RecordingWindowWrapProps { 14 | windowData: RecordWindowData 15 | } 16 | 17 | export const RecordingWindowWrap: React.FC = ({ 18 | windowData 19 | }) => { 20 | const { recordingStatus } = useRecorderContext() 21 | 22 | return ( 23 | 39 | 49 | } 50 | rightNode={} 51 | {...{ windowData }} 52 | /> 53 | 54 |
55 | 56 |
57 | 58 | 59 |
60 |
61 |
62 |
63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /apps/web-app/src/utils/others/ffmpeg.util.ts: -------------------------------------------------------------------------------- 1 | /* import { FFmpeg } from '@ffmpeg/ffmpeg' 2 | import { toBlobURL } from '@ffmpeg/util' 3 | 4 | // This works too slowly. So in the future we could talk about implement this, but right now it would be really slow. 5 | 6 | export class UtilFfmpeg { 7 | private readonly core: FFmpeg 8 | 9 | constructor () { 10 | this.core = new FFmpeg() 11 | } 12 | 13 | async loadFfmpeg () { 14 | const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm' 15 | this.core.on('log', ({ message }) => { 16 | console.log(message) 17 | }) 18 | // toBlobURL is used to bypass CORS issue, urls with the same 19 | // domain can be used directly. 20 | await this.core.load({ 21 | coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'), 22 | wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm') 23 | }) 24 | } 25 | 26 | async transcode (recordingBlob: Blob, toFormat: 'mp4' | 'mp3') { 27 | const recordingUint8Array = new Uint8Array(await recordingBlob.arrayBuffer()) 28 | await this.core.writeFile('input.webm', recordingUint8Array) 29 | let ffmpegQuery = [''] 30 | 31 | if (toFormat === 'mp4') { 32 | ffmpegQuery = ['-i', 'input.webm', '-c:v', 'libx264', '-crf', '23', '-c:a', 'aac', '-b:a', '192K', '-movflags', '+faststart', 'output.mp4'] 33 | } else if (toFormat === 'mp3') { 34 | ffmpegQuery = ['-i', 'input.webm', '-vn', '-acodec', 'libmp3lame', '-ab', '192K', 'output.mp3'] 35 | } 36 | 37 | await this.core.exec(ffmpegQuery) 38 | const recordingData = await this.core.readFile(`output.${toFormat}`) 39 | 40 | const fileType = toFormat === 'mp3' ? 'audio/mp3' : 'video/mp4' 41 | 42 | // @ts-expect-error @ffmpeg/ffmpeg has not typed the property ".buffer" of the FileData type 43 | const recordingTranscodedBlob = new Blob([recordingData.buffer], { type: fileType }) 44 | 45 | console.log(recordingTranscodedBlob) 46 | return recordingTranscodedBlob 47 | } 48 | } 49 | */ 50 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/Window/components/WindowHeader.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Input } from '@/components/ui' 2 | import { SectionLayout } from '@/layouts' 3 | import { type SectionLayoutHeaderPropsPartial } from '@/layouts/SectionLayout/components' 4 | import { type WindowData } from '@/models' 5 | import { useWindowSystemStore } from '@/services/store/zustand' 6 | import { Cross1Icon } from '@radix-ui/react-icons' 7 | 8 | interface WindowHeaderProps extends SectionLayoutHeaderPropsPartial { 9 | windowData: WindowData 10 | readonlyTitle?: boolean 11 | } 12 | 13 | export const WindowHeader: React.FC = ({ icon, rightNode, windowData, readonlyTitle = false }) => { 14 | const [removeWindow, updateWindow] = useWindowSystemStore(state => [state.removeWindow, state.updateWindow]) 15 | 16 | const handleOnClickToCloseWindow = () => { 17 | removeWindow(windowData.id) 18 | } 19 | 20 | const handleOnInputToRename = (event: React.FormEvent) => { 21 | const newWindowName = event.currentTarget.value 22 | 23 | const newWindowData = { ...windowData } 24 | 25 | newWindowData.name = newWindowName 26 | updateWindow(windowData.id, newWindowData) 27 | } 28 | 29 | return ( 30 | 43 | } 44 | rightNode={ 45 |
48 | {rightNode} 49 | 56 |
57 | } 58 | {...{ icon }} 59 | /> 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/SignUp/components/FormTextFieldValidateUserData/hooks/useFormTextFieldValidateUserData.ts: -------------------------------------------------------------------------------- 1 | import { useDebounce } from '@/hooks' 2 | import { StatusCodes, type ApiOK, type CheckUserTypeParams } from '@/models' 3 | import { backRoutes } from '@/routing' 4 | import axios, { isAxiosError } from 'axios' 5 | import { useEffect, useState } from 'react' 6 | import { useFormContext } from 'react-hook-form' 7 | 8 | export const useFormTextFieldValidateUserData = (inputName: string, userKey: CheckUserTypeParams) => { 9 | const { watch, setError, clearErrors, formState: { errors, isDirty } } = useFormContext() 10 | const inputValue = watch(inputName) 11 | const inputError = errors?.[inputName] 12 | const [loading, setLoading] = useState(false) 13 | 14 | const { debouncedValue: inputValueDebounced } = useDebounce(inputValue, 700) 15 | const { debouncedValue: inputErrorDebounced } = useDebounce(inputError, 700) 16 | 17 | useEffect(() => { 18 | if (inputError != null || inputValue === '' || inputValue == null || !isDirty) return 19 | 20 | setLoading(true) 21 | 22 | const abortController = new AbortController() 23 | 24 | const executionContext = async () => { 25 | try { 26 | await axios.post(backRoutes.checkUserData(userKey), { [userKey]: inputValue }, { signal: abortController.signal }) 27 | clearErrors(inputName) 28 | } catch (error) { 29 | if (isAxiosError(error)) { 30 | const errorStatusCode = error?.response?.data?.statusCode 31 | if (errorStatusCode === StatusCodes.Conflict) { 32 | setError(inputName, { message: `${userKey} already exists`, type: 'validate', types: { validate: true } }) 33 | } 34 | } 35 | } finally { 36 | setLoading(false) 37 | } 38 | } 39 | 40 | void executionContext() 41 | 42 | return () => { 43 | if (inputError != null && inputValue != null) return 44 | abortController.abort() 45 | setLoading(false) 46 | } 47 | // eslint-disable-next-line react-hooks/exhaustive-deps 48 | }, [inputValueDebounced, inputErrorDebounced]) 49 | 50 | return { loading } 51 | } 52 | -------------------------------------------------------------------------------- /apps/web-app/src/components/ui/Select/Select.tsx: -------------------------------------------------------------------------------- 1 | import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons' 2 | import { SelectContent, SelectGroup, SelectPortal, Root as SelectRoot, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectViewport, type SelectProps } from '@radix-ui/react-select' 3 | import { SelectCurrentValue, SelectGroupLabel, SelectItem } from './components' 4 | 5 | interface CustomSelectProps extends SelectProps { 6 | triggerContent: React.ReactNode 7 | } 8 | 9 | export const Select = ({ children, triggerContent, ...props }: CustomSelectProps) => { 10 | return ( 11 | 12 | 15 | {triggerContent} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {children} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ) 32 | } 33 | 34 | 35 | Select.Item = SelectItem 36 | Select.GroupLabel = SelectGroupLabel 37 | Select.TriggerContent = SelectCurrentValue 38 | Select.Group = SelectGroup 39 | Select.Separator = SelectSeparator 40 | -------------------------------------------------------------------------------- /apps/web-app/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test' 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // require('dotenv').config(); 8 | 9 | /** 10 | * See https://playwright.dev/docs/test-configuration. 11 | */ 12 | export default defineConfig({ 13 | testDir: './playwright', 14 | /* Run tests in files in parallel */ 15 | fullyParallel: true, 16 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 17 | forbidOnly: !!process.env.CI, 18 | /* Retry on CI only */ 19 | retries: process.env.CI ? 2 : 0, 20 | /* Opt out of parallel tests on CI. */ 21 | workers: process.env.CI ? 1 : undefined, 22 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 23 | reporter: process.env.CI ? 'dot' : 'list', 24 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 25 | use: { 26 | /* Base URL to use in actions like `await page.goto('/')`. */ 27 | baseURL: 'http://localhost:5173/', 28 | 29 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 30 | trace: 'on-first-retry', 31 | }, 32 | 33 | /* Configure projects for major browsers */ 34 | projects: [ 35 | { 36 | name: 'chromium', 37 | use: { ...devices['Desktop Chrome'] }, 38 | }, 39 | 40 | { 41 | name: 'firefox', 42 | use: { ...devices['Desktop Firefox'] }, 43 | }, 44 | 45 | /* Test against mobile viewports. */ 46 | // { 47 | // name: 'Mobile Chrome', 48 | // use: { ...devices['Pixel 5'] }, 49 | // }, 50 | // { 51 | // name: 'Mobile Safari', 52 | // use: { ...devices['iPhone 12'] }, 53 | // }, 54 | 55 | /* Test against branded browsers. */ 56 | // { 57 | // name: 'Microsoft Edge', 58 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 59 | // }, 60 | // { 61 | // name: 'Google Chrome', 62 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 63 | // }, 64 | ], 65 | 66 | /* Run your local dev server before starting the tests */ 67 | webServer: { 68 | command: 'pnpm dev', 69 | url: 'http://localhost:5173/', 70 | reuseExistingServer: !process.env.CI, 71 | }, 72 | }); 73 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/RecordingWindow/components/RecordControls.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui' 2 | import { recordingStatusType } from '../models' 3 | import { useRecorderContext } from '../services/context' 4 | 5 | export const RecordControls = () => { 6 | const { recordingStatus, toggleRecordingStatus } = useRecorderContext() 7 | 8 | const handleOnClickToStartRecording = () => { 9 | void toggleRecordingStatus(recordingStatusType.on) 10 | } 11 | 12 | const handleOnClickToStopRecording = () => { 13 | void toggleRecordingStatus(recordingStatusType.off) 14 | } 15 | 16 | const handleOnClickToResumeRecording = () => { 17 | void toggleRecordingStatus(recordingStatusType.resumed) 18 | } 19 | 20 | const handleOnClickToPauseRecording = () => { 21 | void toggleRecordingStatus(recordingStatusType.paused) 22 | } 23 | 24 | const disablePauseResumeButton = recordingStatus === recordingStatusType.off 25 | 26 | return ( 27 |
30 | 31 | { 32 | (recordingStatus === recordingStatusType.on || recordingStatus === recordingStatusType.resumed || recordingStatus === recordingStatusType.off) && ( 33 | 39 | ) 40 | } 41 | { 42 | recordingStatus === recordingStatusType.paused && ( 43 | 49 | ) 50 | } 51 | { 52 | recordingStatus === recordingStatusType.off && ( 53 | 59 | ) 60 | } 61 | { 62 | (recordingStatus !== recordingStatusType.off) && ( 63 | 69 | ) 70 | } 71 |
72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /apps/web-app/src/hooks/private/useLocalAuth.ts: -------------------------------------------------------------------------------- 1 | import { appConfig } from '@/config' 2 | import { useRouting } from '@/hooks' 3 | import { defaultUserValue, type AuthSuccessApi, type PublicUser, type UserSignInDTO, type UserSignUpDTO } from '@/models' 4 | import { frontRoutes } from '@/routing' 5 | import { signInService, signUpService, verifyEmailService } from '@/services/others' 6 | import { getFromStorageObject, removeFromStorage, setToStorage } from '@/utils/others' 7 | import { type AxiosResponse } from 'axios' 8 | import { useLayoutEffect, useState } from 'react' 9 | 10 | // Private to use it in context 11 | export const useLocalAuth = () => { 12 | const [user, setUser] = useState(defaultUserValue) 13 | const { goTo } = useRouting() 14 | 15 | useLayoutEffect(() => { 16 | const userData = getFromStorageObject(appConfig.localStorageKeys.user, 'localStorage') 17 | const accessToken = getFromStorageObject(appConfig.localStorageKeys.token, 'localStorage', 'string') 18 | 19 | if (userData == null || accessToken == null) return 20 | 21 | setUser(userData) 22 | }, []) 23 | 24 | const signIn = async (userSignInDTO: UserSignInDTO) => { 25 | const userData: AxiosResponse = await signInService(userSignInDTO) 26 | 27 | setValuesAndRedirectToHome(userData) 28 | } 29 | 30 | const signUp = async (userSignUpDTO: UserSignUpDTO) => { 31 | await signUpService(userSignUpDTO) 32 | } 33 | 34 | const signOut = () => { 35 | setUser(defaultUserValue) 36 | removeFromStorage(appConfig.localStorageKeys.token, 'localStorage') 37 | removeFromStorage(appConfig.localStorageKeys.user, 'localStorage') 38 | } 39 | 40 | const verifyEmail = async (token: string) => { 41 | const userData: AxiosResponse = await verifyEmailService(token) 42 | 43 | setValuesAndRedirectToHome(userData) 44 | } 45 | 46 | const setValuesAndRedirectToHome = (userRequest: AxiosResponse) => { 47 | const user = userRequest.data.user 48 | const token = userRequest.data.token 49 | setUser(user) 50 | setToStorage(appConfig.localStorageKeys.token, token, 'localStorage') 51 | setToStorage(appConfig.localStorageKeys.user, JSON.stringify(user), 'localStorage') 52 | goTo(frontRoutes.home) 53 | } 54 | 55 | return { user, signIn, signUp, signOut, verifyEmail } as const 56 | } 57 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/SignIn/components/SignInForm.tsx: -------------------------------------------------------------------------------- 1 | import { FormTextField } from '@/components/feature' 2 | import { Button } from '@/components/ui' 3 | import { useGlobalAuth } from '@/hooks' 4 | import { StatusCodes, type BaseComponentType, type UserSignInDTO } from '@/models' 5 | import { toast } from '@/utils/others' 6 | import { zodResolver } from '@hookform/resolvers/zod' 7 | import { isAxiosError } from 'axios' 8 | import { useForm } from 'react-hook-form' 9 | import { signInSchema } from '../models' 10 | 11 | export const SignInForm: BaseComponentType = () => { 12 | const { signIn } = useGlobalAuth() 13 | 14 | const { 15 | register, 16 | handleSubmit, 17 | formState: { 18 | isSubmitting, 19 | errors 20 | } 21 | } = useForm({ 22 | resolver: zodResolver(signInSchema) 23 | }) 24 | 25 | const handleOnSubmit = handleSubmit( 26 | async (data: UserSignInDTO) => { 27 | try { 28 | await signIn(data) 29 | toast.message('You\'ve successfully signed in', 'success') 30 | } catch (error) { 31 | if (isAxiosError(error)) { 32 | const errorStatusCode = error?.response?.data?.statusCode 33 | if (errorStatusCode === StatusCodes.TooManyRequests) { 34 | toast.message('You\'ve reached the sign-in attempt limit. Try again in 5 hours.', 'warning') 35 | } else if (errorStatusCode === StatusCodes.Forbidden) { 36 | toast.message('Invalid email or password', 'error') 37 | } 38 | } 39 | } 40 | } 41 | ) 42 | 43 | return ( 44 |
48 | 58 | 67 | 68 | 69 | ) 70 | } 71 | 72 | 73 | -------------------------------------------------------------------------------- /apps/web-app/src/hooks/public/useStopwatch.ts: -------------------------------------------------------------------------------- 1 | import { type Time, type TimeStrap, type WorkerStopwatchMessage } from '@/models' 2 | import { StopwatchWorkerBuilder } from '@/workers' 3 | import { useEffect, useRef, useState } from 'react' 4 | export const useStopwatch = () => { 5 | const [time, setTime] = useState
38 | 39 | 40 | Functionality 41 | Available 42 | 43 | 44 | 45 | { 46 | Object.entries(compatibility).map(([key, value]) => ( 47 | 48 | {key} 49 | 50 | {value ? : } 51 | 52 | 53 | )) 54 | } 55 | 56 |
57 | 58 | 59 |
60 |

61 | Use a based chromium browser like brave for better compatibility 62 |

63 |
64 | 65 | 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /apps/back/src/app/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ConflictException, 3 | Inject, 4 | Injectable, 5 | forwardRef, 6 | } from '@nestjs/common'; 7 | import { JwtService } from '@nestjs/jwt'; 8 | import { InjectRepository } from '@nestjs/typeorm'; 9 | import { compareSync } from 'bcrypt'; 10 | import { Repository } from 'typeorm'; 11 | import { BadCredentialsException } from '../common/exceptions'; 12 | import { postgresErrorHandler } from '../common/util'; 13 | import { EmailVerificationService } from '../email-verification/email-verification.service'; 14 | import { User } from '../user/entities/user.entity'; 15 | import { SignInDto, SignUpDto } from './dto'; 16 | 17 | @Injectable() 18 | export class AuthService { 19 | constructor( 20 | @InjectRepository(User) 21 | private readonly userRepository: Repository, 22 | 23 | private readonly jwtService: JwtService, 24 | 25 | @Inject(forwardRef(() => EmailVerificationService)) 26 | private readonly emailVerificationService: EmailVerificationService, 27 | ) {} 28 | 29 | async signIn(signInDto: SignInDto) { 30 | const userPreloaded = await this.userRepository.findOne({ 31 | where: { email: signInDto.email }, 32 | select: { 33 | id: true, 34 | username: true, 35 | email: true, 36 | isActive: true, 37 | password: true, 38 | roles: true, 39 | }, 40 | }); 41 | 42 | if (userPreloaded == null) { 43 | throw new BadCredentialsException(); 44 | } 45 | 46 | const isValidUser = compareSync(signInDto.password, userPreloaded.password); 47 | 48 | if (!isValidUser) { 49 | throw new BadCredentialsException(); 50 | } 51 | 52 | return this.getUserAndJwt(userPreloaded); 53 | } 54 | 55 | async signUp(signUpDto: SignUpDto) { 56 | try { 57 | const user = this.userRepository.create(signUpDto); 58 | 59 | const userData = await this.userRepository.save(user); 60 | 61 | return this.getUserAndJwt(userData); 62 | } catch (error) { 63 | postgresErrorHandler(error); 64 | } 65 | } 66 | 67 | public async sendUserEmailValidation(data: SignUpDto) { 68 | const userFound = await this.userRepository.findOne({ 69 | where: [{ email: data.email }, { username: data.username }], 70 | }); 71 | 72 | if (userFound != null) { 73 | throw new ConflictException( 74 | 'This username or email already exists in db', 75 | ); 76 | } 77 | 78 | return await this.emailVerificationService.startEmailVerification(data); 79 | } 80 | 81 | public async checkUser(key: 'username' | 'email', value: string) { 82 | const userFound = await this.userRepository.findOneBy({ [key]: value }); 83 | 84 | if (userFound != null) { 85 | throw new ConflictException(`${value} already exits`); 86 | } 87 | 88 | return 'OK'; 89 | } 90 | 91 | private async getUserAndJwt(user: User) { 92 | const publicUserData: Partial = structuredClone(user); 93 | 94 | delete publicUserData.password; 95 | 96 | const tokenPayload = { id: user.id }; 97 | 98 | return { 99 | user: publicUserData, 100 | token: await this.jwtService.signAsync(tokenPayload), 101 | }; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/DownloadRecordingWindow/DownloadRecordingWindow.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Select, TextField, Window } from '@/components/ui' 2 | import { Input } from '@/components/ui/Input' 3 | import { type DownloadRecordingWindowData } from '@/models' 4 | import { DownloadIcon } from '@radix-ui/react-icons' 5 | import { useDownloadRecording } from './hooks' 6 | 7 | interface DownloadRecordingWindowProps { 8 | windowData: DownloadRecordingWindowData 9 | } 10 | 11 | export const DownloadRecordingWindow: React.FC = ({ windowData }) => { 12 | const { downloadRecording, loading } = useDownloadRecording({ windowData }) 13 | 14 | const handleOnSubmitToDownload = (e: React.FormEvent) => { 15 | e.preventDefault() 16 | 17 | const formData = e.currentTarget.elements 18 | 19 | const fileNameInput = formData.namedItem('filename') as HTMLInputElement 20 | const formatInput = formData.namedItem('format') as HTMLSelectElement 21 | 22 | const fileName = fileNameInput.value ?? 'download' 23 | const formatType = (formatInput.value as 'mp3' | 'webm' | 'mp4') ?? 'webm' 24 | 25 | void downloadRecording(fileName, formatType) 26 | } 27 | 28 | return ( 29 | 36 | } 39 | /> 40 | 41 | {loading && 'loading'} 42 |
46 |
49 | 50 | 53 | File name 54 | 55 | 56 | 57 | 58 | 59 | 62 | Format 63 | 64 | 84 | 85 |
86 | 91 |
92 |
93 |
94 | ) 95 | } 96 | 97 | export default DownloadRecordingWindow 98 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/RecordingWindow/components/RecordData.tsx: -------------------------------------------------------------------------------- 1 | import { useStopwatch } from '@/hooks' 2 | import { formatTime } from '@/utils/others' 3 | import { useEffect } from 'react' 4 | import { recordingStatusType } from '../models' 5 | import { useRecorderContext } from '../services/context' 6 | 7 | export const RecordData = () => { 8 | const { startStopwatch, stopStopwatch, time, startTime, endTime, pauseStopwatch, resumeStopwatch } = useStopwatch() 9 | const { recordingStatus } = useRecorderContext() 10 | 11 | useEffect(() => { 12 | if (recordingStatus === recordingStatusType.on) { 13 | startStopwatch() 14 | } else if (recordingStatus === recordingStatusType.off) { 15 | stopStopwatch() 16 | } else if (recordingStatus === recordingStatusType.paused) { 17 | pauseStopwatch() 18 | } else if (recordingStatus === recordingStatusType.resumed) { 19 | resumeStopwatch() 20 | } 21 | // eslint-disable-next-line react-hooks/exhaustive-deps 22 | }, [recordingStatus]) 23 | 24 | const timeFormatted = formatTime(time) 25 | 26 | return ( 27 |
28 |
31 |
34 | 37 | Time 38 | 39 |
40 | 43 | {timeFormatted?.hours} 44 | : 45 | {timeFormatted?.minutes} 46 | 47 | 50 | :{timeFormatted?.seconds} 51 | 52 |
53 |
54 |
57 |
60 | 63 | Started at 64 | 65 | 68 | {`${startTime?.hours ?? '- -'}:${startTime?.minutes ?? '- -'}`} 69 | 70 |
71 |
74 | 77 | Stopped at 78 | 79 | 82 | {`${endTime?.hours ?? '- -'}:${endTime?.minutes ?? '- -'}`} 83 | 84 |
85 |
86 |
87 |
88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /apps/web-app/src/utils/others/screen-recorder.util.ts: -------------------------------------------------------------------------------- 1 | import { LoggerService } from '@/services/others' 2 | 3 | let defaultValues: DisplayMediaStreamOptions = { 4 | video: { 5 | frameRate: { 6 | ideal: 60 7 | }, 8 | displaySurface: 'browser' 9 | }, 10 | audio: { 11 | sampleRate: 44000 12 | }, 13 | // @ts-expect-error systemAudio and monitorTypeSurfaces are an experimental property 14 | systemAudio: 'include', 15 | monitorTypeSurfaces: 'include' 16 | } 17 | 18 | export type RecordingType = 'screen' | 'web-cam' 19 | 20 | export class CustomMediaRecorder { 21 | private mediaStreamInstance: MediaStream | null 22 | private mediaRecorderInstance: MediaRecorder | null 23 | private readonly recordingType: RecordingType 24 | 25 | constructor (recordingType: RecordingType, mediaValues?: DisplayMediaStreamOptions) { 26 | defaultValues = { 27 | ...defaultValues, 28 | ...mediaValues 29 | } 30 | 31 | this.recordingType = recordingType 32 | this.mediaStreamInstance = null 33 | this.mediaRecorderInstance = null 34 | } 35 | 36 | async startStreaming () { 37 | const recordingKey = this.recordingType === 'screen' ? 'getDisplayMedia' : 'getUserMedia' 38 | this.mediaStreamInstance = await navigator.mediaDevices[recordingKey](defaultValues) 39 | this.mediaRecorderInstance?.addEventListener('dataavailable', () => { console.log('dataavailable') }) 40 | this.mediaRecorderInstance?.addEventListener('stop', () => { console.log('stop') }) 41 | this.mediaRecorderInstance?.addEventListener('error', () => { console.log('error') }) 42 | LoggerService.message({ trackSettings: this.mediaStreamInstance.getTracks()[0].getSettings() }, 'screen-recorder.util --> startStreaming:32') 43 | return this.mediaStreamInstance 44 | } 45 | 46 | async stopStreaming () { 47 | const tracks = this.mediaStreamInstance?.getTracks() 48 | 49 | tracks?.forEach((track) => { 50 | track.stop() 51 | }) 52 | } 53 | 54 | async startRecording () { 55 | if (this.mediaStreamInstance == null) { 56 | return 57 | } 58 | 59 | this.mediaRecorderInstance = new MediaRecorder(this.mediaStreamInstance, { mimeType: 'video/webm;codecs=vp8,opus' }) 60 | this.mediaRecorderInstance.start() 61 | } 62 | 63 | async pauseRecording () { 64 | if (this.mediaRecorderInstance == null) { 65 | return 66 | } 67 | 68 | this.mediaRecorderInstance.pause() 69 | } 70 | 71 | async resumeRecording () { 72 | if (this.mediaRecorderInstance == null) { 73 | return 74 | } 75 | 76 | this.mediaRecorderInstance.resume() 77 | } 78 | 79 | async stopRecording () { 80 | if (this.mediaRecorderInstance == null) { 81 | return 82 | } 83 | 84 | this.mediaRecorderInstance.stop() 85 | } 86 | 87 | async getVideoAndAudioBlob (): Promise { 88 | return await new Promise((resolve) => { 89 | this.mediaRecorderInstance?.addEventListener('dataavailable', (event) => { 90 | const videoAndAudio = event.data 91 | resolve(videoAndAudio) 92 | }) 93 | }) 94 | } 95 | 96 | async onStopStreaming (callback: () => void) { 97 | this.mediaStreamInstance?.getTracks()[0].addEventListener('ended', callback) 98 | } 99 | 100 | async removeOnStopStreaming (callback: () => void) { 101 | this.mediaStreamInstance?.getTracks()[0].removeEventListener('ended', callback) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /apps/back/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ pnpm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ pnpm run start 40 | 41 | # watch mode 42 | $ pnpm run start:dev 43 | 44 | # production mode 45 | $ pnpm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ pnpm run test 53 | 54 | # e2e tests 55 | $ pnpm run test:e2e 56 | 57 | # test coverage 58 | $ pnpm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/AddWindowsDropdownMenu.tsx: -------------------------------------------------------------------------------- 1 | import { Anchor, DropdownMenu } from '@/components/ui' 2 | import { useGlobalAuth } from '@/hooks' 3 | import { CWindowType } from '@/models' 4 | import { frontRoutes } from '@/routing' 5 | import { useWindowSystemStore } from '@/services/store/zustand' 6 | 7 | export const AddWindowsDropdownMenu = () => { 8 | const [addWindow] = useWindowSystemStore((state) => [state.addWindow]) 9 | const { user, signOut } = useGlobalAuth() 10 | 11 | const handleOnClickToAddScreenRecordingWindow = () => { 12 | addWindow({ id: crypto.randomUUID(), name: 'Screen recording', type: CWindowType.record, recordingCoreType: 'screen' }) 13 | } 14 | 15 | const handleOnClickToAddWebcamRecordingWindow = () => { 16 | addWindow({ id: crypto.randomUUID(), name: 'Webcam recording', type: CWindowType.record, recordingCoreType: 'web-cam' }) 17 | } 18 | 19 | const handleOnClickToSignOut = () => { 20 | signOut() 21 | } 22 | 23 | return ( 24 |
27 | { 28 | user.roles[0] === 'default' 29 | ?
30 | Sign up 31 | or 32 | Sign in 33 |
34 | : 38 | {user.username} 39 | 40 | 41 | 42 | } 43 | > 44 | 45 | 46 | Sign out 47 | 48 | 49 | 50 | } 51 | Windows 56 | } 57 | > 58 | 59 | 62 | Screen | Tab | Window 63 | 64 | 67 | Web cam 68 | 69 | 70 | 71 |
72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /apps/back/src/app/email-verification/email-verification.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Inject, 3 | Injectable, 4 | Logger, 5 | NotFoundException, 6 | forwardRef, 7 | } from '@nestjs/common'; 8 | import { ConfigService } from '@nestjs/config'; 9 | import { JwtService } from '@nestjs/jwt'; 10 | import { Cron, CronExpression } from '@nestjs/schedule'; 11 | import { InjectRepository } from '@nestjs/typeorm'; 12 | import { hashSync } from 'bcrypt'; 13 | import { ResendService } from 'nestjs-resend'; 14 | import { LessThan, Repository } from 'typeorm'; 15 | import { AuthService } from '../auth/auth.service'; 16 | import { SignUpDto } from '../auth/dto'; 17 | import { BadCredentialsException } from '../common/exceptions'; 18 | import { postgresErrorHandler } from '../common/util'; 19 | import { UnverifiedEmail } from './entities'; 20 | 21 | @Injectable() 22 | export class EmailVerificationService { 23 | private readonly logger = new Logger(EmailVerificationService.name); 24 | 25 | constructor( 26 | @InjectRepository(UnverifiedEmail) 27 | private readonly unverifiedEmailRepository: Repository, 28 | 29 | private readonly resendService: ResendService, 30 | 31 | private readonly jwtService: JwtService, 32 | 33 | private readonly configService: ConfigService, 34 | 35 | @Inject(forwardRef(() => AuthService)) 36 | private readonly authService: AuthService, 37 | ) {} 38 | 39 | async startEmailVerification(createUnverifiedEmailDto: SignUpDto) { 40 | try { 41 | createUnverifiedEmailDto.password = hashSync( 42 | createUnverifiedEmailDto.password, 43 | 10, 44 | ); 45 | 46 | const unverifiedEmail = this.unverifiedEmailRepository.create({ 47 | email: createUnverifiedEmailDto.email, 48 | username: createUnverifiedEmailDto.username, 49 | }); 50 | await this.unverifiedEmailRepository.save(unverifiedEmail); 51 | 52 | let validationLink = `${this.configService.getOrThrow('WEB_APP_ORIGIN')}/email-verification/`; 53 | validationLink += await this.jwtService.signAsync( 54 | createUnverifiedEmailDto, 55 | ); 56 | 57 | this.sendEmail( 58 | unverifiedEmail.email, 59 | `Click CONFIRM to confirm your email`, 60 | ); 61 | 62 | return 'Email verification sended'; 63 | } catch (error) { 64 | postgresErrorHandler(error); 65 | } 66 | } 67 | 68 | async verifyAndSignUpEmail(token: string) { 69 | try { 70 | await this.jwtService.verifyAsync(token); 71 | 72 | const userDataDecoded = this.jwtService.decode(token); 73 | 74 | const { affected } = await this.unverifiedEmailRepository.delete({ 75 | email: userDataDecoded.email, 76 | }); 77 | 78 | if (affected === 0) { 79 | throw new NotFoundException(`Email ${userDataDecoded.email} not found`); 80 | } 81 | 82 | this.sendEmail(userDataDecoded.email, `Your email has been verified`); 83 | 84 | return await this.authService.signUp(userDataDecoded); 85 | } catch (error) { 86 | throw new BadCredentialsException(); 87 | } 88 | } 89 | 90 | private async sendEmail(to: string, html: string) { 91 | const fromData = { 92 | subject: this.configService.get('RESEND_FROM_SUBJECT'), 93 | email: this.configService.get('RESEND_FROM_EMAIL'), 94 | }; 95 | 96 | this.resendService.send({ 97 | from: `${fromData.subject} ${fromData.email}`, 98 | to, 99 | subject: 'Email verification', 100 | html, 101 | }); 102 | } 103 | 104 | @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) 105 | private async removeUnverifiedEmails() { 106 | this.logger.verbose('Removing unverified emails'); 107 | 108 | const date24HoursAgo = new Date(); 109 | date24HoursAgo.setHours(date24HoursAgo.getHours() - 24); 110 | 111 | await this.unverifiedEmailRepository.delete({ 112 | requestedVerification: LessThan(date24HoursAgo.getTime()), 113 | }); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /apps/web-app/src/pages/Home/components/feature/RecordingWindow/hooks/useRecorder.ts: -------------------------------------------------------------------------------- 1 | import { CWindowType } from '@/models' 2 | import { LoggerService } from '@/services/others' 3 | import { useWindowSystemStore } from '@/services/store/zustand' 4 | import { CustomMediaRecorder, type RecordingType } from '@/utils/others' 5 | import { useCallback, useEffect, useRef, useState } from 'react' 6 | import { type RecordingStatus } from '../models/recorder.model' 7 | 8 | export const useRecorder = (recordingType: RecordingType) => { 9 | const [recordingStatus, setRecordingStatus] = useState('off') 10 | const mediaRecorder = useRef() 11 | const [error, setError] = useState() 12 | const videoSourceRef = useRef() 13 | const [addWindow] = useWindowSystemStore((state) => [state.addWindow]) 14 | 15 | useEffect(() => { 16 | mediaRecorder.current = new CustomMediaRecorder(recordingType) 17 | return () => { 18 | void mediaRecorder.current?.stopStreaming() 19 | void mediaRecorder.current?.stopRecording() 20 | void mediaRecorder.current?.removeOnStopStreaming(cachedOnStopStreaming) 21 | mediaRecorder.current = undefined 22 | } 23 | // eslint-disable-next-line react-hooks/exhaustive-deps 24 | }, []) 25 | 26 | const cachedOnStopStreaming = useCallback( 27 | () => { 28 | void stopRecording() 29 | setRecordingStatus('off') 30 | } 31 | // eslint-disable-next-line react-hooks/exhaustive-deps 32 | , [] 33 | ) 34 | 35 | const toggleRecordingStatus = async (newStatus: RecordingStatus) => { 36 | if (mediaRecorder.current == null) { 37 | setError('Something went wrong: toggle recording status') 38 | return 39 | } 40 | 41 | try { 42 | if (newStatus === 'on') { 43 | await startRecording() 44 | } else if (newStatus === 'off') { 45 | await stopRecording() 46 | } else if (newStatus === 'paused') { 47 | await pauseRecording() 48 | } else if (newStatus === 'resumed') { 49 | await resumeRecording() 50 | } 51 | 52 | setRecordingStatus(newStatus) 53 | } catch (error) { 54 | LoggerService.message(error, 'useRecorder - toggleRecordingStatus', 'error') 55 | setError('Something went wrong: startRecording failed') 56 | } 57 | } 58 | 59 | const startRecording = async () => { 60 | if (mediaRecorder.current == null) { 61 | setError('Something went wrong: toggle recording status') 62 | return 63 | } 64 | const streamObject = await mediaRecorder.current?.startStreaming() 65 | await mediaRecorder.current.startRecording() 66 | 67 | // In case that the user stop the recording from the default browser bar provided by the browser, this will work. 68 | void mediaRecorder.current.onStopStreaming(cachedOnStopStreaming) 69 | 70 | if (videoSourceRef.current != null && streamObject != null) { 71 | videoSourceRef.current.srcObject = streamObject 72 | } 73 | } 74 | 75 | const stopRecording = async () => { 76 | if (mediaRecorder.current == null) { 77 | setError('Something went wrong: toggle recording status') 78 | return 79 | } 80 | 81 | void mediaRecorder.current?.removeOnStopStreaming(cachedOnStopStreaming) 82 | 83 | void mediaRecorder.current.stopStreaming() 84 | void mediaRecorder.current.stopRecording() 85 | 86 | const streamBlob = await getVideoAndAudioBlob() 87 | 88 | if (streamBlob == null) return 89 | 90 | // Opens a new window to watch the video recorded 91 | addWindow({ name: 'Watch recording', type: CWindowType.watchRecord, videoAndAudioBlob: streamBlob, id: crypto.randomUUID() }) 92 | 93 | if (videoSourceRef.current != null) { 94 | videoSourceRef.current.srcObject = null 95 | } 96 | } 97 | 98 | const pauseRecording = async () => { 99 | await mediaRecorder.current?.pauseRecording() 100 | } 101 | 102 | const resumeRecording = async () => { 103 | await mediaRecorder.current?.resumeRecording() 104 | } 105 | 106 | const getVideoAndAudioBlob = async () => { 107 | const videoBlob = await mediaRecorder.current?.getVideoAndAudioBlob() 108 | return videoBlob 109 | } 110 | 111 | return { toggleRecordingStatus, recordingStatus, error, videoSourceRef } 112 | } 113 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | jes015@web-capture.online. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /apps/web-app/src/assets/Icons.tsx: -------------------------------------------------------------------------------- 1 | import { type ComponentIcon } from '@/models' 2 | 3 | // icon:close-circle | Ionicons https://ionicons.com/ | Ionic Framework 4 | 5 | export const IconGithub: ComponentIcon = (props) => { 6 | return ( 7 | 14 | 15 | 16 | ) 17 | } 18 | 19 | // icon:record | Entypo http://entypo.com/ | Daniel Bruce 20 | export const IconRecording: ComponentIcon = (props) => { 21 | return ( 22 | 29 | 30 | 31 | ) 32 | } 33 | 34 | // icon:videocam | Ionicons https://ionicons.com/ | Ionic Framework 35 | export const IconVideoCam: ComponentIcon = (props) => { 36 | return ( 37 | 44 | 45 | 46 | ) 47 | } 48 | 49 | // icon:close | Ionicons https://ionicons.com/ | Ionic Framework 50 | export const IconClose: ComponentIcon = (props) => { 51 | return ( 52 | 59 | 60 | 61 | ) 62 | } 63 | 64 | export const IconHamburgerMenu: ComponentIcon = (props) => ( 65 | 72 | 73 | 74 | ) 75 | 76 | // icon:alert | Entypo http://entypo.com/ | Daniel Bruce 77 | export const IconAlert: ComponentIcon = (props) => ( 78 | 85 | 86 | 87 | ) 88 | 89 | // icon:check-circle-fill | Bootstrap https://icons.getbootstrap.com/ | Bootstrap 90 | export const IconCheckCircle: ComponentIcon = (props) => ( 91 | 98 | 99 | 100 | ) 101 | 102 | // icon:close-circle | Ionicons https://ionicons.com/ | Ionic Framework 103 | export const IconCloseCircle: ComponentIcon = (props) => ( 104 | 111 | 112 | 113 | ) 114 | 115 | // icon:padlock | Unicons https://iconscout.com/unicons | Iconscout 116 | export const IconPadlock: ComponentIcon = (props) => ( 117 | 124 | 125 | 126 | ) 127 | --------------------------------------------------------------------------------