├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── e2e-auth-tests.yml │ └── unit-tests.yml ├── LICENSE ├── README.md ├── client ├── .dockerignore ├── .eslintrc.json ├── .gitignore ├── Dockerfile ├── README.md ├── next-env.d.ts ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public │ ├── favicon.ico │ └── vercel.svg ├── src │ ├── components │ │ ├── Container.tsx │ │ ├── EditProfileForm.tsx │ │ ├── ErrorField.tsx │ │ ├── Footer.tsx │ │ ├── NavBar.tsx │ │ ├── SocialLogin.tsx │ │ └── chat │ │ │ ├── ChatButton.tsx │ │ │ ├── CreateChatForm.tsx │ │ │ ├── CreateConversationForm.tsx │ │ │ ├── EditChatForm.tsx │ │ │ ├── MemberCard.tsx │ │ │ ├── Members.tsx │ │ │ ├── Message.tsx │ │ │ └── MessageInput.tsx │ ├── pages │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ ├── account │ │ │ ├── confirm.tsx │ │ │ └── password │ │ │ │ ├── change.tsx │ │ │ │ ├── new.tsx │ │ │ │ └── reset.tsx │ │ ├── chat │ │ │ └── index.tsx │ │ ├── index.tsx │ │ ├── invite.tsx │ │ ├── login │ │ │ ├── error.tsx │ │ │ └── index.tsx │ │ └── me.tsx │ ├── store │ │ ├── auth.tsx │ │ ├── models │ │ │ ├── conversation.ts │ │ │ ├── index.ts │ │ │ ├── room.ts │ │ │ └── user.ts │ │ └── store.ts │ ├── styles │ │ └── globals.css │ └── utils │ │ ├── constants.ts │ │ ├── room.ts │ │ ├── types.ts │ │ ├── useSocket.ts │ │ └── withAuth.tsx ├── tailwind.config.js └── tsconfig.json ├── docker-compose.test.yml ├── docker-compose.yml ├── server ├── .dockerignore ├── .env.sample ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── Dockerfile ├── README.md ├── nest-cli.json ├── package.json ├── pnpm-lock.yaml ├── src │ ├── common │ │ ├── decorators │ │ │ ├── index.ts │ │ │ ├── roles.decorator.ts │ │ │ ├── user.decorator.ts │ │ │ └── verified.decorator.ts │ │ ├── dtos │ │ │ ├── create-account.dto.ts │ │ │ ├── index.ts │ │ │ ├── login.dto.ts │ │ │ ├── passport-values.dto.ts │ │ │ └── update-user.dto.ts │ │ ├── entities │ │ │ ├── abstract.entity.ts │ │ │ ├── index.ts │ │ │ └── user.entity.ts │ │ ├── enums │ │ │ ├── index.ts │ │ │ ├── postgres-errors.enum.ts │ │ │ ├── providers.enum.ts │ │ │ ├── role.enum.ts │ │ │ └── status.enum.ts │ │ ├── exceptions │ │ │ ├── index.ts │ │ │ ├── invalid-credentials.exception.ts │ │ │ ├── social-provider.exception.ts │ │ │ └── unique-violation.exception.ts │ │ ├── guards │ │ │ ├── facebook.-oauth.guard.ts │ │ │ ├── google-oauth.guard.ts │ │ │ ├── index.ts │ │ │ ├── jwt-auth.guard.ts │ │ │ ├── roles.guard.ts │ │ │ └── verified.guard.ts │ │ └── swagger │ │ │ ├── constants.ts │ │ │ └── index.ts │ ├── main.ts │ └── modules │ │ ├── app.controller.ts │ │ ├── app.module.ts │ │ └── v1 │ │ ├── auth │ │ ├── auth.controller.ts │ │ ├── auth.module.ts │ │ ├── auth.service.ts │ │ ├── strategies │ │ │ ├── facebook-oauth.strategy.ts │ │ │ ├── google-oauth.strategy.ts │ │ │ ├── index.ts │ │ │ └── jwt-auth.strategy.ts │ │ └── tests │ │ │ ├── auth.controller.spec.ts │ │ │ └── auth.service.spec.ts │ │ ├── chat │ │ ├── chat.adapter.ts │ │ ├── chat.gateway.ts │ │ ├── chat.module.ts │ │ ├── chat.service.ts │ │ └── ws-emitter.module.ts │ │ ├── conversation │ │ ├── conversation.controller.ts │ │ ├── conversation.entity.ts │ │ ├── conversation.module.ts │ │ └── conversation.service.ts │ │ ├── message │ │ ├── dto │ │ │ └── create-message.dto.ts │ │ ├── message.controller.ts │ │ ├── message.entity.ts │ │ ├── message.module.ts │ │ └── message.service.ts │ │ ├── room │ │ ├── dto │ │ │ ├── add-remove-user.dto.ts │ │ │ └── room.dto.ts │ │ ├── entities │ │ │ ├── index.ts │ │ │ ├── invitation.entity.ts │ │ │ └── room.entity.ts │ │ ├── guards │ │ │ ├── MembershipGuard.ts │ │ │ ├── ModGuard.ts │ │ │ └── OwnershipGuard.ts │ │ ├── room.controller.ts │ │ ├── room.module.ts │ │ └── room.service.ts │ │ ├── user │ │ ├── repositories │ │ │ └── user.repository.ts │ │ ├── tests │ │ │ ├── user.controller.spec.ts │ │ │ └── user.service.spec.ts │ │ ├── user.controller.ts │ │ ├── user.module.ts │ │ └── user.service.ts │ │ └── v1.module.ts ├── test │ ├── app.e2e-spec.ts │ ├── auth.e2e-spec.ts │ ├── jest-e2e.json │ ├── mocks │ │ └── user.mock.ts │ └── test-utils.ts ├── tsconfig.build.json └── tsconfig.json └── workers └── queues ├── .dockerignore ├── .env.sample ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── Dockerfile ├── nest-cli.json ├── package.json ├── pnpm-lock.yaml ├── src ├── app.module.ts ├── app.service.ts ├── mailer │ ├── mail-queue.processor.ts │ ├── mailer.module.ts │ └── templates │ │ └── pages │ │ ├── confirm-email.hbs │ │ └── reset-password.hbs └── main.ts ├── tsconfig.build.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | insert_final_newline = false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/workflows/e2e-auth-tests.yml: -------------------------------------------------------------------------------- 1 | name: E2e tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | - dev 9 | 10 | 11 | jobs: 12 | test: 13 | name: Running e2e tests 14 | runs-on: ubuntu-latest 15 | env: 16 | OAUTH_GOOGLE_ID: secret 17 | OAUTH_GOOGLE_SECRET: secret 18 | OAUTH_FACEBOOK_ID: secret 19 | OAUTH_FACEBOOK_SECRET: secret 20 | JWT_ACCESS_SECRET_KEY: secret 21 | JWT_ACCESS_EXPIRATION_TIME: 5m 22 | JWT_REFRESH_SECRET_KEY: secret 23 | JWT_REFRESH_EXPIRATION_TIME: 30d 24 | defaults: 25 | run: 26 | working-directory: ./server 27 | services: 28 | postgres-tests: 29 | image: postgres:alpine 30 | env: 31 | POSTGRES_USER: tests 32 | POSTGRES_PASSWORD: tests 33 | POSTGRES_DB: postgres-tests 34 | ports: 35 | - 5431:5432 36 | redis-tests: 37 | image: redis:alpine 38 | ports: 39 | - 6379:6379 40 | steps: 41 | - uses: actions/checkout@v3 42 | name: Postgres setup 43 | - name: Use Node.js ${{ matrix.node-version }} 44 | uses: actions/setup-node@v3 45 | with: 46 | node-version: ${{ matrix.node-version }} 47 | - name: Pnpm Setup 48 | run: npm install -g pnpm 49 | - name: Install 50 | run: pnpm install --no-frozen-lockfile 51 | - name: Test 52 | run: pnpm test:e2e 53 | 54 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | - dev 9 | 10 | 11 | jobs: 12 | test: 13 | name: Running units tests 14 | runs-on: ubuntu-latest 15 | defaults: 16 | run: 17 | working-directory: ./server 18 | services: 19 | postgres-tests: 20 | image: postgres:alpine 21 | env: 22 | POSTGRES_USER: tests 23 | POSTGRES_PASSWORD: tests 24 | POSTGRES_DB: postgres-tests 25 | ports: 26 | - 5431:5432 27 | steps: 28 | - uses: actions/checkout@v3 29 | name: Postgres setup 30 | - name: Use Node.js ${{ matrix.node-version }} 31 | uses: actions/setup-node@v3 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | - name: Pnpm Setup 35 | run: npm install -g pnpm 36 | - name: Install 37 | run: pnpm install --no-frozen-lockfile 38 | - name: Test 39 | run: pnpm test 40 | 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2023 PoProstuWitold 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nest-next-boilerplate 2 | Boilerplate for Nest.js, Next.js, TypeScript stack. Includes social logins, account verification, password change & recover, real-time chats and more. 3 | 4 | # Usage 5 | 6 | ### 0. Environmental variables 7 | 0.1. Create **``.env``** file in **``server``** root directory and fill with following: 8 | ```code 9 | # APP 10 | NODE_ENV='development' 11 | APP_PORT=4000 12 | ORIGIN='http://localhost:3000' 13 | API_PREFIX='/api' 14 | 15 | # JWT AUTH 16 | JWT_ACCESS_SECRET_KEY='long-unpredictable-secret1' 17 | JWT_ACCESS_EXPIRATION_TIME='5m' 18 | JWT_REFRESH_SECRET_KEY='long-unpredictable-secret2' 19 | JWT_REFRESH_EXPIRATION_TIME='30d' 20 | 21 | # DATABASE 22 | # change if you running in a different way than the one written in docker compose file 23 | DB_TYPE='postgres' 24 | DB_USERNAME='admin' 25 | DB_PASSWORD='admin' 26 | DB_HOST='postgres-main' 27 | DB_PORT=5432 28 | DB_DATABASE='postgres-nest' 29 | DB_SYNC=true 30 | 31 | # REDIS 32 | # change if you running in a different way than docker compose 33 | REDIS_HOST='redis-main' 34 | REDIS_PORT=6379 35 | 36 | # GOOGLE 37 | OAUTH_GOOGLE_ID=[YOUR_GOOGLE_OAUTH_ID] 38 | OAUTH_GOOGLE_SECRET=[YOUR_GOOGLE_SECRET] 39 | OAUTH_GOOGLE_REDIRECT_URL='/api/v1/auth/google/redirect' 40 | 41 | # FACEBOOK 42 | OAUTH_FACEBOOK_ID=[YOUR_FACEBOOK_ID] 43 | OAUTH_FACEBOOK_SECRET=[YOUR_FACEBOOK_SECRET] 44 | OAUTH_FACEBOOK_REDIRECT_URL='/api/v1/auth/facebook/redirect' 45 | ``` 46 | 0.2. Create **``.env``** file in **``workers/queues``** root directory and fill with following: 47 | 48 | ```code 49 | # MAIL 50 | SMTP_USER=[YOUR_SMTP_USER] 51 | SMPT_PASSWORD=[YOUR_SMTP_PASSWORD] 52 | 53 | # REDIS 54 | # change if you running in a different way than docker compose 55 | REDIS_HOST='redis-main' 56 | REDIS_PORT=6379 57 | ``` 58 | #### **Tip** 59 | For free email testing you can use service such as [Mailtrap](https://mailtrap.io/). 60 | 61 | ## With Docker 62 | 63 | ### 1. Run Docker containers 64 | ```bash 65 | docker compose up 66 | ``` 67 | 68 | ## Without Docker 69 | ### 1. Change contents of ``DATABASE`` and ``REDIS`` sections in ``env`` files 70 | **``server``** 71 | ```code 72 | ... 73 | 74 | # DATABASE 75 | DB_TYPE=[YOUR_DB_TYPE] 76 | DB_USERNAME=[YOUR_DB_USERNAME] 77 | DB_PASSWORD=[YOUR_DB_PASSWORD] 78 | DB_HOST=[YOUR_DB_HOST] 79 | DB_PORT=[YOUR_DB_PORT] 80 | DB_DATABASE=[YOUR_DB_DATABASE] 81 | DB_SYNC=[true or false in dev mode, false in prod] 82 | 83 | # REDIS 84 | REDIS_HOST=[YOUR_REDIS_HOST] 85 | REDIS_PORT=[YOUR_REDIS_PORT] 86 | 87 | ... 88 | ``` 89 | 90 | **``workers/queues``** 91 | ```code 92 | ... 93 | 94 | # REDIS 95 | REDIS_HOST=[YOUR_REDIS_HOST] 96 | REDIS_PORT=[YOUR_REDIS_PORT] 97 | ``` 98 | ### 2.1 Server setup 99 | ```bash 100 | cd server 101 | ``` 102 | ```bash 103 | npm install 104 | # OR 105 | pnpm install 106 | # OR 107 | yarn 108 | ``` 109 | 110 | ### 2.2 Worker 111 | ```bash 112 | cd workers/queues 113 | ``` 114 | ```bash 115 | npm install 116 | # OR 117 | pnpm install 118 | # OR 119 | yarn 120 | ``` 121 | 122 | ### 3. Client setup 123 | ```bash 124 | cd client 125 | ``` 126 | ```bash 127 | npm install 128 | # OR 129 | pnpm install 130 | # OR 131 | yarn 132 | ``` 133 | 134 | ## FEATURES 135 | - Local login & register 136 | - Social login & register using Google and Facebook 137 | - Jwt access token & refresh token 138 | - Account confirmation 139 | - Password recover 140 | - Profile update 141 | - Multiple themes with the ability to add your own 142 | - Group chat with basic permissions 143 | - Private chat (also with yourself) 144 | - Rate limiting 145 | 146 | ## TECH STACK 147 | - Backend: 148 | - Nest.js 149 | - PostgreSQL 150 | - Redis 151 | - WebSockets 152 | - JWT 153 | - Passport.js 154 | - Frontend 155 | - Next.js 156 | - Tailwind & DaisyUI 157 | - Redux ([rematch](https://rematchjs.org/)) 158 | ## TO DO 159 | - [x] Local login 160 | - [x] Google login 161 | - [x] Facebook login 162 | - [x] Client app routing 163 | - [x] Write tests for API 164 | - [x] Password recover & change features 165 | - [x] Queues 166 | - [x] Refresh tokens 167 | - [X] Chat 168 | - [ ] Make URL preview 169 | - [ ] Enable sending images and maybe videos 170 | - [ ] Public profile page 171 | - [ ] Refactor chat backend & UI 172 | 173 | ## License 174 | [MIT](https://choosealicense.com/licenses/mit/) 175 | -------------------------------------------------------------------------------- /client/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | node_modules 4 | npm-debug.log 5 | README.md 6 | .next 7 | .git -------------------------------------------------------------------------------- /client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/babel"] 3 | } 4 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | WORKDIR /poprostuwitold/client 4 | 5 | COPY package.json pnpm-lock.yaml ./ 6 | 7 | RUN npm install -g pnpm 8 | RUN pnpm install 9 | 10 | COPY . . 11 | 12 | EXPOSE 3000 13 | 14 | CMD ["pnpm", "run", "dev"] -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /client/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /client/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | images: { 5 | domains: ['www.gravatar.com', 'lh3.googleusercontent.com', 'platform-lookaside.fbsbx.com'], 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "1.0.0", 4 | "description": "React Next client for nest-next-boilerplate app", 5 | "author": "PoProstuWitold", 6 | "private": true, 7 | "license": "MIT", 8 | "scripts": { 9 | "dev": "next dev", 10 | "build": "next build", 11 | "start": "next start", 12 | "lint": "next lint" 13 | }, 14 | "dependencies": { 15 | "@headlessui/react": "^1.7.17", 16 | "@rematch/core": "^2.2.0", 17 | "@rematch/loading": "^2.1.2", 18 | "@rematch/persist": "^2.1.2", 19 | "@tailwindcss/typography": "^0.5.10", 20 | "axios": "^1.5.1", 21 | "daisyui": "^3.9.2", 22 | "formik": "^2.4.5", 23 | "next": "13.5.4", 24 | "next-themes": "^0.2.1", 25 | "react": "18.2.0", 26 | "react-dom": "18.2.0", 27 | "react-icons": "^4.11.0", 28 | "react-redux": "^8.1.3", 29 | "redux": "^4.2.1", 30 | "socket.io-client": "^4.7.2", 31 | "socket.io-react-hook": "^2.4.2", 32 | "swr": "^2.2.4", 33 | "yup": "^1.3.2" 34 | }, 35 | "devDependencies": { 36 | "@types/node": "^20.8.3", 37 | "@types/react": "^18.2.25", 38 | "autoprefixer": "^10.4.16", 39 | "eslint": "8.51.0", 40 | "eslint-config-next": "13.5.4", 41 | "postcss": "^8.4.31", 42 | "tailwindcss": "^3.3.3", 43 | "typescript": "5.2.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PoProstuWitold/nest-next-boilerplate/c5277b121b3cbe0169523f652abd6e27c3aaec8f/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /client/src/components/Container.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react' 2 | 3 | interface ContainerProps { 4 | children: React.ReactNode 5 | style?: CSSProperties | undefined 6 | className?: string 7 | } 8 | 9 | export const Container: React.FC = ({ children, style, className }) => { 10 | return ( 11 |
12 | {children} 13 |
14 | ) 15 | } -------------------------------------------------------------------------------- /client/src/components/ErrorField.tsx: -------------------------------------------------------------------------------- 1 | interface ErrorFieldProps { 2 | error: string | null 3 | } 4 | 5 | export const ErrorField: React.FC = ({ error }) => { 6 | return ( 7 | {error} 8 | ) 9 | } -------------------------------------------------------------------------------- /client/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | export const Footer = () => { 2 | return ( 3 |
4 |
5 |

© 2022-2023 by PoProstuWitold

6 |
7 |
8 | ) 9 | } -------------------------------------------------------------------------------- /client/src/components/SocialLogin.tsx: -------------------------------------------------------------------------------- 1 | import { BsGoogle, BsFacebook } from 'react-icons/bs' 2 | 3 | 4 | interface SocialLoginProps { 5 | provider: 'Google' | 'Facebook' 6 | url: string 7 | className?: string 8 | } 9 | 10 | export const SocialLogin: React.FC = ({ provider, url, className }) => { 11 | return ( 12 | 16 | {provider === 'Google' ? : null} 17 | {provider === 'Facebook' ? : null} 18 |

{provider}

19 |
20 | ) 21 | } -------------------------------------------------------------------------------- /client/src/components/chat/ChatButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { AiFillUnlock, AiFillLock } from 'react-icons/ai' 3 | import { useSelector } from 'react-redux' 4 | 5 | import { RootState } from '../../store/store' 6 | 7 | 8 | interface ChatButtonProps { 9 | chat: any 10 | type: 'room' | 'conversation' 11 | } 12 | 13 | export const ChatButton: React.FC = ({ chat, type }) => { 14 | 15 | let userState = useSelector((state: RootState) => state.user) 16 | const { user } = userState 17 | 18 | if(!user) { 19 | return <> 20 | } 21 | 22 | const getConversationName = (chat: any) => { 23 | 24 | if(chat.recipient.displayName === user.displayName && chat.creator.displayName === user.displayName) { 25 | return `${chat.creator.displayName} (you)` 26 | } 27 | if(chat.creator.displayName === user.displayName) { 28 | return chat.recipient.displayName 29 | } 30 | if(chat.recipient.displayName === user.displayName) { 31 | return chat.creator.displayName 32 | } 33 | } 34 | 35 | return ( 36 | 37 | username 38 |
39 | {type && type === 'room' && 40 | <> 41 |
42 | {chat.name} 43 | {chat.isPublic ? : } 44 |
45 | {chat.description} 46 | 47 | } 48 | {type && type === 'conversation' && 49 | <> 50 |
51 | {getConversationName(chat)} 52 |
53 | 54 | } 55 |
56 |
57 | ) 58 | } -------------------------------------------------------------------------------- /client/src/components/chat/CreateConversationForm.tsx: -------------------------------------------------------------------------------- 1 | import { Formik, Form, Field, FormikState, FormikHelpers } from 'formik' 2 | import * as Yup from 'yup' 3 | import { useSelector } from 'react-redux' 4 | import { AiTwotoneEdit } from 'react-icons/ai' 5 | 6 | import { RootState } from '../../store/store' 7 | import { ErrorField } from '../../components/ErrorField' 8 | import { useAuthenticatedSocket } from '../../utils/useSocket' 9 | import { useEffect, useState } from 'react' 10 | 11 | 12 | type ConversastionValues = { 13 | creator: string | null, 14 | participant: string | null 15 | } 16 | 17 | interface CreateConversationFormProps { 18 | 19 | } 20 | 21 | export const CreateConversationForm: React.FC = ({}) => { 22 | const { socket } = useAuthenticatedSocket('ws://localhost:4000/chat') 23 | 24 | let userState = useSelector((state: RootState) => state.user) 25 | const { user, authenticated } = userState 26 | 27 | if(!user) { 28 | return <> 29 | } 30 | 31 | const createConversationValues: ConversastionValues = { 32 | creator: user.displayName, 33 | participant: '', 34 | } 35 | 36 | const [wsError, setWsError] = useState('') 37 | 38 | const createConversationSchema = Yup.object().shape({ 39 | participant: Yup.string() 40 | }) 41 | 42 | useEffect(() => { 43 | if(socket) { 44 | socket.on('connect', () => { 45 | console.log('Socket connected') 46 | }) 47 | 48 | socket.on('disconnect', () => { 49 | console.log('Socket disconnected') 50 | }) 51 | 52 | socket.on('error:conversation-create', async (error) => { 53 | console.log('error:conversation-create', error) 54 | setWsError(error) 55 | }) 56 | 57 | return () => { 58 | socket.off('connect') 59 | socket.off('disconnect') 60 | socket.off('error:conversation-create') 61 | } 62 | } 63 | }, [socket]) 64 | 65 | const submitConversation = async (values: ConversastionValues, helpers: FormikHelpers) => { 66 | try { 67 | socket.emit('conversation:create', values) 68 | setTimeout(() => { 69 | helpers.resetForm() 70 | setWsError('') 71 | }, 3000) 72 | } catch (err) { 73 | console.error(err) 74 | } 75 | } 76 | 77 | return ( 78 | <> 79 |
80 |
81 |

PoProstuWitold

82 | {wsError ?

{wsError.name}

: null} 83 | {authenticated && user !== null ? 84 | 89 | {({ isSubmitting, errors, touched }: FormikState) => ( 90 |
91 |
92 |
93 | 96 | {user.displayName} 97 |
98 |
99 |
100 |
101 | 104 | 105 | 109 | 110 |
111 |
112 | 113 | 116 |
117 | )} 118 |
: null 119 | } 120 |
121 |
122 | 123 | ) 124 | } -------------------------------------------------------------------------------- /client/src/components/chat/Members.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import axios from 'axios' 3 | import { useSelector } from 'react-redux' 4 | 5 | import { MemberCard } from './MemberCard' 6 | import { useAuthenticatedSocket } from '../../utils/useSocket' 7 | import { RootState } from '../../store/store' 8 | 9 | 10 | interface MembersProps {} 11 | 12 | export const Members: React.FC = () => { 13 | let roomState = useSelector((state: RootState) => state.room) 14 | const { activeRoom } = roomState 15 | const { socket } = useAuthenticatedSocket('ws://localhost:4000/chat') 16 | const [invitationLink, setInvitationLink] = useState(null) 17 | 18 | useEffect(() => { 19 | if(socket) { 20 | socket.on('connect', () => { 21 | console.log('Socket connected') 22 | }) 23 | 24 | socket.on('disconnect', () => { 25 | console.log('Socket disconnected') 26 | }) 27 | 28 | if(invitationLink && activeRoom && invitationLink.roomId !== activeRoom.id) { 29 | setInvitationLink(null) 30 | } 31 | 32 | return () => { 33 | socket.off('connect') 34 | socket.off('disconnect') 35 | socket.off('error') 36 | } 37 | } 38 | }, [socket, invitationLink, activeRoom]) 39 | 40 | const createInvitation = async(room: any) => { 41 | try { 42 | const res = await axios.post(`/room/invite/${room.id}`) 43 | setInvitationLink(res.data) 44 | } catch (err) { 45 | 46 | } 47 | } 48 | 49 | if(!activeRoom) { 50 | return <> 51 | } 52 | 53 | return ( 54 | <> 55 |
56 |

Chat: {activeRoom.name}

57 |
58 |
59 |
60 | 61 | {invitationLink && 62 |
63 |

Copy & Send to people you want to invite

64 |

{`http://localhost:3000/invite?code=${invitationLink.code}`}

65 |
66 | } 67 |
68 |
    69 | { 70 | activeRoom && activeRoom.users.map((user: any, index: number) => { 71 | return ( 72 |
    73 | 74 |
    75 | ) 76 | }) 77 | } 78 |
79 |
80 |
81 |
82 | 83 | ) 84 | } -------------------------------------------------------------------------------- /client/src/components/chat/Message.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSelector } from 'react-redux' 3 | import { MdVerifiedUser } from 'react-icons/md' 4 | 5 | import { RootState } from '../../store/store' 6 | import { User } from '../../utils/types' 7 | 8 | 9 | interface MessageProps { 10 | msg: any 11 | author?: User 12 | nextMsg: any 13 | } 14 | 15 | export const Message: React.FC = ({ msg, author, nextMsg }) => { 16 | 17 | let userState = useSelector((state: RootState) => state.user) 18 | const { user } = userState 19 | 20 | if(!user) { 21 | return <> 22 | } 23 | 24 | const hour = new Date(msg.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) 25 | const date = new Date(msg.createdAt).toLocaleDateString().replace(/\//g, '.') 26 | 27 | return ( 28 |
  • 29 | {author && 30 | <> 31 |
    32 | {nextMsg && !nextMsg.author && 33 | {`${author.displayName}`}} 34 | {nextMsg && nextMsg.author && 35 | {author.displayName === nextMsg.author.displayName ? '' : `${author.displayName}`} 36 | } 37 | {!nextMsg && 38 | {`${author.displayName}`} 39 | } 40 |
    41 |
    42 |
    43 |
    44 |
    45 | {msg.text} 46 |
    47 |
    48 |
    49 |
    50 | 51 | } 52 | {!author && 53 | <> 54 |
    55 |
    56 | System 57 | Verified 58 |
    59 |
    60 |
    61 |
    62 |
    63 |
    64 | {msg.text} 65 |
    66 |
    67 |
    68 |
    69 | 70 | } 71 |
  • 72 | ) 73 | } -------------------------------------------------------------------------------- /client/src/components/chat/MessageInput.tsx: -------------------------------------------------------------------------------- 1 | import { Field, Form, Formik, FormikHelpers, FormikState } from 'formik' 2 | import * as Yup from 'yup' 3 | 4 | import { useAuthenticatedSocket } from '../../utils/useSocket' 5 | 6 | 7 | interface MessageInputProps { 8 | chatId: string 9 | type: 'room' | 'conversation' 10 | } 11 | 12 | type MessageValues = { 13 | text: string 14 | } 15 | 16 | export const MessageInput: React.FC = ({ chatId, type }) => { 17 | 18 | const { socket } = useAuthenticatedSocket('ws://localhost:4000/chat') 19 | const messageValues: MessageValues = { text: '' } 20 | const messageSchema = Yup.object().shape({ 21 | text: Yup.string().required('Email cannot be empty or whitespace') 22 | }) 23 | const submitMessage = async (values: MessageValues, helpers: FormikHelpers) => { 24 | helpers.resetForm() 25 | setTimeout(() => helpers.setSubmitting(false), 2000) 26 | try { 27 | socket.emit('message:create', { 28 | message: values.text, 29 | chatId, 30 | type 31 | }) 32 | } catch (err: any) { 33 | console.error('Submit Message Error', err) 34 | } 35 | } 36 | 37 | return ( 38 | <> 39 | {/* //TO DO - 'user' is typing feautre 40 |
    41 | typing 42 |
    */} 43 |
    44 | 49 | {({ isSubmitting }: FormikState) => ( 50 |
    51 | 52 | 55 | 56 | )} 57 |
    58 |
    59 | 60 | ) 61 | } -------------------------------------------------------------------------------- /client/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | import { SWRConfig } from 'swr' 4 | import axios from 'axios' 5 | import { PersistGate } from 'redux-persist/lib/integration/react' 6 | import { getPersistor } from '@rematch/persist' 7 | import { useRouter } from 'next/router' 8 | import { ThemeProvider } from 'next-themes' 9 | import { Provider } from 'react-redux' 10 | import { IoProvider } from 'socket.io-react-hook' 11 | 12 | import { AuthProvider } from '../store/auth' 13 | import { Footer } from '../components/Footer' 14 | import { store } from '../store/store' 15 | import { NavBar } from '../components/NavBar' 16 | 17 | 18 | const persistor = getPersistor() 19 | 20 | axios.defaults.baseURL = 'http://localhost:4000/api/v1' 21 | axios.defaults.withCredentials = true 22 | 23 | const fetcher = async (url: string) => { 24 | try { 25 | const res = await axios.get(url) 26 | return res.data 27 | } catch (err: any) { 28 | throw err.response.data 29 | } 30 | } 31 | 32 | const MyApp = ({ Component, pageProps }: AppProps) => { 33 | 34 | const { pathname } = useRouter() 35 | const authRoutes = ['/login', '/account/password/reset'] 36 | const authRoute = authRoutes.includes(pathname) 37 | 38 | return ( 39 | 43 | 44 | 45 | 46 | 47 | 48 | {!authRoute && } 49 | 50 | {!authRoute || pathname === '/chat' &&